SwiftLint/Source/SwiftLintFramework/Rules/Idiomatic/FunctionDefaultParameterAtE...

107 lines
3.8 KiB
Swift

import SourceKittenFramework
public struct FunctionDefaultParameterAtEndRule: ASTRule, ConfigurationProviderRule, OptInRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "function_default_parameter_at_end",
name: "Function Default Parameter at End",
description: "Prefer to locate parameters with defaults toward the end of the parameter list.",
kind: .idiomatic,
nonTriggeringExamples: [
"func foo(baz: String, bar: Int = 0) {}",
"func foo(x: String, y: Int = 0, z: CGFloat = 0) {}",
"func foo(bar: String, baz: Int = 0, z: () -> Void) {}",
"func foo(bar: String, z: () -> Void, baz: Int = 0) {}",
"func foo(bar: Int = 0) {}",
"func foo() {}",
"""
class A: B {
override func foo(bar: Int = 0, baz: String) {}
""",
"func foo(bar: Int = 0, completion: @escaping CompletionHandler) {}",
"""
func foo(a: Int, b: CGFloat = 0) {
let block = { (error: Error?) in }
}
""",
"""
func foo(a: String, b: String? = nil,
c: String? = nil, d: @escaping AlertActionHandler = { _ in }) {}
"""
],
triggeringExamples: [
"↓func foo(bar: Int = 0, baz: String) {}"
]
)
public func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard SwiftDeclarationKind.functionKinds.contains(kind),
let offset = dictionary.offset,
let bodyOffset = dictionary.bodyOffset,
!dictionary.enclosedSwiftAttributes.contains(.override) else {
return []
}
let isNotClosure = { !self.isClosureParameter(dictionary: $0) }
let params = dictionary.substructure
.flatMap { subDict -> [SourceKittenDictionary] in
guard subDict.declarationKind == .varParameter else {
return []
}
return [subDict]
}
.filter(isNotClosure)
.filter { param in
guard let paramOffset = param.offset else {
return false
}
return paramOffset < bodyOffset
}
guard !params.isEmpty else {
return []
}
let containsDefaultValue = { self.isDefaultParameter(file: file, dictionary: $0) }
let defaultParams = params.filter(containsDefaultValue)
guard !defaultParams.isEmpty else {
return []
}
let lastParameters = params.suffix(defaultParams.count)
let lastParametersWithDefaultValue = lastParameters.filter(containsDefaultValue)
guard lastParameters.count != lastParametersWithDefaultValue.count else {
return []
}
return [
StyleViolation(ruleDescription: type(of: self).description,
severity: configuration.severity,
location: Location(file: file, byteOffset: offset))
]
}
private func isClosureParameter(dictionary: SourceKittenDictionary) -> Bool {
guard let typeName = dictionary.typeName else {
return false
}
return typeName.contains("->") || typeName.contains("@escaping")
}
private func isDefaultParameter(file: SwiftLintFile, dictionary: SourceKittenDictionary) -> Bool {
guard let range = dictionary.byteRange.flatMap(file.stringView.byteRangeToNSRange) else {
return false
}
return regex("=").firstMatch(in: file.contents, options: [], range: range) != nil
}
}