SwiftLint/Source/SwiftLintFramework/Rules/Style/OptionalEnumCaseMatchingRul...

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) == "_"
}
}