Files
SwiftLint/Source/SwiftLintFramework/Rules/LargeTupleRule.swift
T
JP Simard b83e0991b9 Remove all file headers
The MIT license doesn't require that all files be prepended with this
licensing or copyright information. Realm confirmed that they're ok with this
change. This will enable some companies to contribute to SwiftLint and the
date & authorship information will remain accessible via git source control.
2018-05-04 13:42:02 -07:00

209 lines
8.4 KiB
Swift

import Foundation
import SourceKittenFramework
private enum LargeTupleRuleError: Error {
case unbalencedParentheses
}
private enum RangeKind {
case tuple
case generic
}
public struct LargeTupleRule: ASTRule, ConfigurationProviderRule {
public var configuration = SeverityLevelsConfiguration(warning: 2, error: 3)
public init() {}
public static let description = RuleDescription(
identifier: "large_tuple",
name: "Large Tuple",
description: "Tuples shouldn't have too many members. Create a custom type instead.",
kind: .metrics,
nonTriggeringExamples: [
"let foo: (Int, Int)\n",
"let foo: (start: Int, end: Int)\n",
"let foo: (Int, (Int, String))\n",
"func foo() -> (Int, Int)\n",
"func foo() -> (Int, Int) {}\n",
"func foo(bar: String) -> (Int, Int)\n",
"func foo(bar: String) -> (Int, Int) {}\n",
"func foo() throws -> (Int, Int)\n",
"func foo() throws -> (Int, Int) {}\n",
"let foo: (Int, Int, Int) -> Void\n",
"let foo: (Int, Int, Int) throws -> Void\n",
"func foo(bar: (Int, String, Float) -> Void)\n",
"func foo(bar: (Int, String, Float) throws -> Void)\n",
"var completionHandler: ((_ data: Data?, _ resp: URLResponse?, _ e: NSError?) -> Void)!\n",
"func getDictionaryAndInt() -> (Dictionary<Int, String>, Int)?\n",
"func getGenericTypeAndInt() -> (Type<Int, String, Float>, Int)?\n"
],
triggeringExamples: [
"↓let foo: (Int, Int, Int)\n",
"↓let foo: (start: Int, end: Int, value: String)\n",
"↓let foo: (Int, (Int, Int, Int))\n",
"func foo(↓bar: (Int, Int, Int))\n",
"func foo() -> ↓(Int, Int, Int)\n",
"func foo() -> ↓(Int, Int, Int) {}\n",
"func foo(bar: String) -> ↓(Int, Int, Int)\n",
"func foo(bar: String) -> ↓(Int, Int, Int) {}\n",
"func foo() throws -> ↓(Int, Int, Int)\n",
"func foo() throws -> ↓(Int, Int, Int) {}\n",
"func foo() throws -> ↓(Int, ↓(String, String, String), Int) {}\n",
"func getDictionaryAndInt() -> (Dictionary<Int, ↓(String, String, String)>, Int)?\n"
]
)
public func validate(file: File, kind: SwiftDeclarationKind,
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
let offsets = violationOffsetsForTypes(in: file, dictionary: dictionary, kind: kind) +
violationOffsetsForFunctions(in: file, dictionary: dictionary, kind: kind)
return offsets.compactMap { location, size in
for parameter in configuration.params where size > parameter.value {
let reason = "Tuples should have at most \(configuration.warning) members."
return StyleViolation(ruleDescription: type(of: self).description,
severity: parameter.severity,
location: Location(file: file, byteOffset: location),
reason: reason)
}
return nil
}
}
private func violationOffsetsForTypes(in file: File, dictionary: [String: SourceKitRepresentable],
kind: SwiftDeclarationKind) -> [(offset: Int, size: Int)] {
let kinds = SwiftDeclarationKind.variableKinds.subtracting([.varLocal])
guard kinds.contains(kind),
let type = dictionary.typeName,
let offset = dictionary.offset else {
return []
}
let sizes = violationOffsets(for: type).map { $0.1 }
return sizes.max().flatMap { [(offset: offset, size: $0)] } ?? []
}
private func violationOffsetsForFunctions(in file: File, dictionary: [String: SourceKitRepresentable],
kind: SwiftDeclarationKind) -> [(offset: Int, size: Int)] {
let contents = file.contents.bridge()
guard SwiftDeclarationKind.functionKinds.contains(kind),
let returnRange = returnRangeForFunction(dictionary: dictionary),
let returnSubstring = contents.substringWithByteRange(start: returnRange.location,
length: returnRange.length) else {
return []
}
let offsets = violationOffsets(for: returnSubstring, initialOffset: returnRange.location)
return offsets.sorted { $0.offset < $1.offset }
}
private func violationOffsets(for text: String, initialOffset: Int = 0) -> [(offset: Int, size: Int)] {
guard let ranges = try? parenthesesRanges(in: text) else {
return []
}
var text = text.bridge()
var offsets = [(offset: Int, size: Int)]()
for (range, kind) in ranges {
let substring = text.substring(with: range)
if kind != .generic,
let byteRange = text.NSRangeToByteRange(start: range.location, length: range.length),
!containsReturnArrow(in: text.bridge(), range: range) {
let size = substring.components(separatedBy: ",").count
let offset = byteRange.location + initialOffset
offsets.append((offset: offset, size: size))
}
let replacement = String(repeating: " ", count: substring.bridge().length)
text = text.replacingCharacters(in: range, with: replacement).bridge()
}
return offsets
}
private func returnRangeForFunction(dictionary: [String: SourceKitRepresentable]) -> NSRange? {
guard let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
let length = dictionary.length,
let offset = dictionary.offset else {
return nil
}
let start = nameOffset + nameLength
let end = dictionary.bodyOffset ?? length + offset
guard end - start > 0 else {
return nil
}
return NSRange(location: start, length: end - start)
}
private func parenthesesRanges(in text: String) throws -> [(NSRange, RangeKind)] {
var stack = [(Int, String)]()
var balanced = true
var ranges = [(NSRange, RangeKind)]()
let nsText = text.bridge()
let parenthesesAndAngleBrackets = CharacterSet(charactersIn: "()<>")
var index = 0
let length = nsText.length
while balanced {
let searchRange = NSRange(location: index, length: length - index)
let range = nsText.rangeOfCharacter(from: parenthesesAndAngleBrackets,
options: [.literal], range: searchRange)
if range.location == NSNotFound {
break
}
index = NSMaxRange(range)
let symbol = nsText.substring(with: range)
// skip return arrows
if symbol == ">",
case let arrowRange = nsText.range(of: "->", options: [.literal], range: searchRange),
arrowRange.intersects(range) {
continue
}
if symbol == "(" || symbol == "<" {
stack.append((range.location, symbol))
} else if let (startIdx, previousSymbol) = stack.popLast(),
isBalanced(currentSymbol: symbol, previousSymbol: previousSymbol) {
let range = NSRange(location: startIdx, length: range.location - startIdx + 1)
let kind: RangeKind = symbol == ")" ? .tuple : .generic
ranges.append((range, kind))
} else {
balanced = false
}
}
guard balanced && stack.isEmpty else {
throw LargeTupleRuleError.unbalencedParentheses
}
return ranges
}
private func isBalanced(currentSymbol: String, previousSymbol: String) -> Bool {
return (currentSymbol == ")" && previousSymbol == "(") ||
(currentSymbol == ">" && previousSymbol == "<")
}
private func containsReturnArrow(in text: String, range: NSRange) -> Bool {
let arrowRegex = regex("\\A(?:\\s*throws)?\\s*->")
let start = NSMaxRange(range)
let restOfStringRange = NSRange(location: start, length: text.bridge().length - start)
return arrowRegex.firstMatch(in: text, options: [], range: restOfStringRange) != nil
}
}