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

89 lines
3.9 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: [
"[1, 2].map { $0 + 1 }\n",
"[1, 2].map({ $0 + 1 })\n",
"[1, 2].map { number in\n number + 1 \n}\n",
"[1, 2].map { number -> Int in\n number + 1 \n}\n",
"[1, 2].map { (number: Int) -> Int in\n number + 1 \n}\n",
"[1, 2].map { [weak self] number in\n number + 1 \n}\n",
"[1, 2].something(closure: { number in\n number + 1 \n})\n",
"let isEmpty = [1, 2].isEmpty()\n",
"rlmConfiguration.migrationBlock.map { rlmMigration in\n" +
"return { migration, schemaVersion in\n" +
"rlmMigration(migration.rlmMigration, schemaVersion)\n" +
"}\n" +
"}",
"let mediaView: UIView = { [weak self] index in\n" +
" return UIView()\n" +
"}(index)\n"
],
triggeringExamples: [
"[1, 2].map {\n ↓number in\n number + 1 \n}\n",
"[1, 2].map {\n ↓number -> Int in\n number + 1 \n}\n",
"[1, 2].map {\n (↓number: Int) -> Int in\n number + 1 \n}\n",
"[1, 2].map {\n [weak self] ↓number in\n number + 1 \n}\n",
"[1, 2].map { [weak self]\n ↓number in\n number + 1 \n}\n",
"[1, 2].map({\n ↓number in\n number + 1 \n})\n",
"[1, 2].something(closure: {\n ↓number in\n number + 1 \n})\n",
"[1, 2].reduce(0) {\n ↓sum, ↓number in\n number + sum \n}\n"
]
)
private static let openBraceRegex = regex("\\{")
public func validate(file: SwiftLintFile, kind: SwiftExpressionKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard kind == .call else {
return []
}
guard let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
let bodyLength = dictionary.bodyLength,
bodyLength > 0 else {
return []
}
let parameters = dictionary.enclosedVarParameters
let rangeStart = nameOffset + nameLength
let regex = ClosureParameterPositionRule.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
guard let range = contents.byteRangeToNSRange(start: rangeStart, length: rangeLength),
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: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: paramOffset))
}
}
}