107 lines
3.8 KiB
Swift
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
|
|
}
|
|
}
|