import SourceKittenFramework public struct VerticalParameterAlignmentOnCallRule: ASTRule, ConfigurationProviderRule, OptInRule { public var configuration = SeverityConfiguration(.warning) public init() {} public static let description = RuleDescription( identifier: "vertical_parameter_alignment_on_call", name: "Vertical Parameter Alignment On Call", description: "Function parameters should be aligned vertically if they're in multiple lines in a method call.", kind: .style, nonTriggeringExamples: [ Example(""" foo(param1: 1, param2: bar param3: false, param4: true) """), Example(""" foo(param1: 1, param2: bar) """), Example(""" foo(param1: 1, param2: bar param3: false, param4: true) """), Example(""" foo( param1: 1 ) { _ in } """), Example(""" UIView.animate(withDuration: 0.4, animations: { blurredImageView.alpha = 1 }, completion: { _ in self.hideLoading() }) """), Example(""" UIView.animate(withDuration: 0.4, animations: { blurredImageView.alpha = 1 }, completion: { _ in self.hideLoading() }) """), Example(""" foo(param1: 1, param2: { _ in }, param3: false, param4: true) """), Example(""" foo({ _ in bar() }, completion: { _ in baz() } ) """), Example(""" foo(param1: 1, param2: [ 0, 1 ], param3: 0) """), Example(""" myFunc(foo: 0, bar: baz == 0) """) ], triggeringExamples: [ Example(""" foo(param1: 1, param2: bar ↓param3: false, param4: true) """), Example(""" foo(param1: 1, param2: bar ↓param3: false, param4: true) """), Example(""" foo(param1: 1, param2: bar ↓param3: false, ↓param4: true) """), Example(""" foo(param1: 1, ↓param2: { _ in }) """), Example(""" foo(param1: 1, param2: { _ in }, param3: 2, ↓param4: 0) """), Example(""" foo(param1: 1, param2: { _ in }, ↓param3: false, param4: true) """), Example(""" myFunc(foo: 0, ↓bar: baz == 0) """) ] ) public func validate(file: SwiftLintFile, kind: SwiftExpressionKind, dictionary: SourceKittenDictionary) -> [StyleViolation] { guard kind == .call, case let arguments = dictionary.enclosedArguments, arguments.count > 1, let firstArgumentOffset = arguments.first?.offset, case let contents = file.stringView, var firstArgumentPosition = contents.lineAndCharacter(forByteOffset: firstArgumentOffset) else { return [] } var visitedLines = Set() var previousArgumentWasMultiline = false let lastIndex = arguments.count - 1 let violatingOffsets: [ByteCount] = arguments.enumerated().compactMap { idx, argument in defer { previousArgumentWasMultiline = isMultiline(argument: argument, file: file) } guard let offset = argument.offset, let (line, character) = contents.lineAndCharacter(forByteOffset: offset), line > firstArgumentPosition.line else { return nil } let (firstVisit, _) = visitedLines.insert(line) guard character != firstArgumentPosition.character && firstVisit else { return nil } // if this is the first element on a new line after a closure with multiple lines, // we reset the reference position if previousArgumentWasMultiline && firstVisit { firstArgumentPosition = (line, character) return nil } // never trigger on a trailing closure if idx == lastIndex, isTrailingClosure(dictionary: dictionary, file: file) { return nil } return offset } return violatingOffsets.map { StyleViolation(ruleDescription: Self.description, severity: configuration.severity, location: Location(file: file, byteOffset: $0)) } } private func isMultiline(argument: SourceKittenDictionary, file: SwiftLintFile) -> Bool { guard let offset = argument.bodyOffset, let length = argument.bodyLength, case let contents = file.stringView, let (startLine, _) = contents.lineAndCharacter(forByteOffset: offset), let (endLine, _) = contents.lineAndCharacter(forByteOffset: offset + length) else { return false } return endLine > startLine } private func isTrailingClosure(dictionary: SourceKittenDictionary, file: SwiftLintFile) -> Bool { guard let offset = dictionary.offset, let length = dictionary.length, case let start = min(offset, offset + length - 1), case let byteRange = ByteRange(location: start, length: length), let text = file.stringView.substringWithByteRange(byteRange) else { return false } return !text.hasSuffix(")") } }