SwiftLint/Source/SwiftLintFramework/Rules/Style/ClosureParameterPositionRul...

163 lines
5.7 KiB
Swift

import Foundation
import SourceKittenFramework
public struct ClosureParameterPositionRule: ASTRule, ConfigurationProviderRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "closure_parameter_position",
name: "Closure Parameter Position",
description: "Closure parameters should be on the same line as opening brace.",
kind: .style,
nonTriggeringExamples: [
Example("[1, 2].map { $0 + 1 }\n"),
Example("[1, 2].map({ $0 + 1 })\n"),
Example("[1, 2].map { number in\n number + 1 \n}\n"),
Example("[1, 2].map { number -> Int in\n number + 1 \n}\n"),
Example("[1, 2].map { (number: Int) -> Int in\n number + 1 \n}\n"),
Example("[1, 2].map { [weak self] number in\n number + 1 \n}\n"),
Example("[1, 2].something(closure: { number in\n number + 1 \n})\n"),
Example("let isEmpty = [1, 2].isEmpty()\n"),
Example("""
rlmConfiguration.migrationBlock.map { rlmMigration in
return { migration, schemaVersion in
rlmMigration(migration.rlmMigration, schemaVersion)
}
}
"""),
Example("""
let mediaView: UIView = { [weak self] index in
return UIView()
}(index)
""")
],
triggeringExamples: [
Example("""
[1, 2].map {
↓number in
number + 1
}
"""),
Example("""
[1, 2].map {
↓number -> Int in
number + 1
}
"""),
Example("""
[1, 2].map {
(↓number: Int) -> Int in
number + 1
}
"""),
Example("""
[1, 2].map {
[weak ↓self] ↓number in
number + 1
}
"""),
Example("""
[1, 2].map { [weak self]
↓number in
number + 1
}
"""),
Example("""
[1, 2].map({
↓number in
number + 1
})
"""),
Example("""
[1, 2].something(closure: {
↓number in
number + 1
})
"""),
Example("""
[1, 2].reduce(0) {
↓sum, ↓number in
number + sum
})
"""),
Example("""
f.completionHandler = {
↓thing in
doStuff()
}
"""),
Example("""
foo {
[weak ↓self] in
self?.bar()
}
""")
]
)
private static let openBraceRegex = regex("\\{")
public func validate(file: SwiftLintFile) -> [StyleViolation] {
return file.structureDictionary.traverseDepthFirst { subDict in
guard let kind = self.kind(from: subDict) else { return nil }
return validate(file: file, kind: kind, dictionary: subDict)
}.unique.sorted(by: { $0.location < $1.location })
}
public func validate(file: SwiftLintFile, kind: SwiftExpressionKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard kind == .closure || kind == .call else {
return []
}
guard let bodyLength = dictionary.bodyLength,
bodyLength > 0
else {
return []
}
let nameOffset = dictionary.nameOffset ?? 0
let nameLength = dictionary.nameLength ?? 0
let captureLists = dictionary.substructure.flatMap { dict -> [SourceKittenDictionary] in
if SwiftVersion.current >= .fiveDotSix, dict.expressionKind == .argument {
return dict.substructure.filter { $0.declarationKind == .varLocal }
}
return dict.declarationKind == .varLocal ? [dict] : []
}
let parameters = dictionary.enclosedVarParameters + captureLists
let rangeStart = nameOffset + nameLength
let regex = Self.openBraceRegex
// parameters from inner closures are reported on the top-level one, so we can't just
// use the first and last parameters to check, we need to check all of them
return parameters.compactMap { param -> StyleViolation? in
guard let paramOffset = param.offset, paramOffset > rangeStart else {
return nil
}
let rangeLength = paramOffset - rangeStart
let contents = file.stringView
let byteRange = ByteRange(location: rangeStart, length: rangeLength)
guard let range = contents.byteRangeToNSRange(byteRange),
let match = regex.matches(in: file.contents, options: [], range: range).last?.range,
match.location != NSNotFound,
let braceOffset = contents.NSRangeToByteRange(start: match.location, length: match.length)?.location,
let (braceLine, _) = contents.lineAndCharacter(forByteOffset: braceOffset),
let (paramLine, _) = contents.lineAndCharacter(forByteOffset: paramOffset),
braceLine != paramLine
else {
return nil
}
return StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: paramOffset))
}
}
}