238 lines
7.3 KiB
Swift
238 lines
7.3 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
public struct OptionalEnumCaseMatchingRule: SubstitutionCorrectableASTRule, ConfigurationProviderRule,
|
|
AutomaticTestableRule, OptInRule {
|
|
public var configuration = SeverityConfiguration(.warning)
|
|
|
|
public init() {}
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "optional_enum_case_matching",
|
|
name: "Optional Enum Case Match",
|
|
description: "Matching an enum case against an optional enum without '?' is supported on Swift 5.1 and above.",
|
|
kind: .style,
|
|
minSwiftVersion: .fiveDotOne,
|
|
nonTriggeringExamples: [
|
|
Example("""
|
|
switch foo {
|
|
case .bar: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case (.bar, .baz): break
|
|
case (.bar, _): break
|
|
case (_, .baz): break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch (x, y) {
|
|
case (.c, _?):
|
|
break
|
|
case (.c, nil):
|
|
break
|
|
case (_, _):
|
|
break
|
|
}
|
|
""")
|
|
],
|
|
triggeringExamples: [
|
|
Example("""
|
|
switch foo {
|
|
case .bar↓?: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case Foo.bar↓?: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case .bar↓?, .baz↓?: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case .bar↓? where x > 1: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case (.bar↓?, .baz↓?): break
|
|
case (.bar↓?, _): break
|
|
case (_, .bar↓?): break
|
|
default: break
|
|
}
|
|
""")
|
|
],
|
|
corrections: [
|
|
Example("""
|
|
switch foo {
|
|
case .bar↓?: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""): Example("""
|
|
switch foo {
|
|
case .bar: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case Foo.bar↓?: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""): Example("""
|
|
switch foo {
|
|
case Foo.bar: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case .bar↓?, .baz↓?: break
|
|
default: break
|
|
}
|
|
"""): Example("""
|
|
switch foo {
|
|
case .bar, .baz: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case .bar↓? where x > 1: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""): Example("""
|
|
switch foo {
|
|
case .bar where x > 1: break
|
|
case .baz: break
|
|
default: break
|
|
}
|
|
"""),
|
|
Example("""
|
|
switch foo {
|
|
case (.bar↓?, .baz↓?): break
|
|
case (.bar↓?, _): break
|
|
case (_, .bar↓?): break
|
|
default: break
|
|
}
|
|
"""): Example("""
|
|
switch foo {
|
|
case (.bar, .baz): break
|
|
case (.bar, _): break
|
|
case (_, .bar): break
|
|
default: break
|
|
}
|
|
""")
|
|
]
|
|
)
|
|
|
|
// MARK: - ASTRule
|
|
|
|
public func validate(file: SwiftLintFile,
|
|
kind: StatementKind,
|
|
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
|
return violationRanges(in: file, kind: kind, dictionary: dictionary).map {
|
|
StyleViolation(ruleDescription: type(of: self).description,
|
|
severity: configuration.severity,
|
|
location: Location(file: file, characterOffset: $0.location))
|
|
}
|
|
}
|
|
|
|
// MARK: - SubstitutionCorrectableASTRule
|
|
|
|
public func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
|
|
return (violationRange, "")
|
|
}
|
|
|
|
public func violationRanges(in file: SwiftLintFile,
|
|
kind: StatementKind,
|
|
dictionary: SourceKittenDictionary) -> [NSRange] {
|
|
guard SwiftVersion.current >= type(of: self).description.minSwiftVersion, kind == .case else {
|
|
return []
|
|
}
|
|
|
|
let contents = file.stringView
|
|
return dictionary.elements
|
|
.filter { $0.kind == "source.lang.swift.structure.elem.pattern" }
|
|
.flatMap { dictionary -> [NSRange] in
|
|
guard let byteRange = dictionary.byteRange else {
|
|
return []
|
|
}
|
|
|
|
let pattern = contents.substringWithByteRange(byteRange)
|
|
let tupleCommaByteOffsets = pattern?.tupleCommaByteOffsets ?? []
|
|
|
|
let tokensToCheck = (tupleCommaByteOffsets + [byteRange.length]).compactMap { length in
|
|
return file.syntaxMap
|
|
.tokens(inByteRange: ByteRange(location: byteRange.location, length: length))
|
|
.prefix { $0.kind != .keyword || file.isTokenUnderscoreKeyword($0) }
|
|
.last
|
|
}
|
|
|
|
return tokensToCheck.compactMap { tokenToCheck in
|
|
guard !file.isTokenUnderscoreKeyword(tokenToCheck) else {
|
|
return nil
|
|
}
|
|
let questionMarkByteRange = ByteRange(location: tokenToCheck.range.upperBound, length: 1)
|
|
guard contents.substringWithByteRange(questionMarkByteRange) == "?" else {
|
|
return nil
|
|
}
|
|
return contents.byteRangeToNSRange(questionMarkByteRange)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension String {
|
|
func ranges(of substring: String) -> [Range<Index>] {
|
|
var ranges = [Range<Index>]()
|
|
while let range = range(of: substring, range: (ranges.last?.upperBound ?? startIndex)..<endIndex) {
|
|
ranges.append(range)
|
|
}
|
|
return ranges
|
|
}
|
|
|
|
var isTuple: Bool {
|
|
return first == "(" && last == ")" && contains(",")
|
|
}
|
|
|
|
var tupleCommaByteOffsets: [ByteCount] {
|
|
guard isTuple else {
|
|
return []
|
|
}
|
|
|
|
let stringView = StringView(self)
|
|
return ranges(of: ",").map { range in
|
|
return stringView.byteOffset(fromLocation: distance(from: startIndex, to: range.lowerBound))
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension SwiftLintFile {
|
|
func isTokenUnderscoreKeyword(_ token: SwiftLintSyntaxToken) -> Bool {
|
|
return token.kind == .keyword &&
|
|
token.length == 1 &&
|
|
contents(for: token) == "_"
|
|
}
|
|
}
|