259 lines
8.6 KiB
Swift
259 lines
8.6 KiB
Swift
import SwiftSyntax
|
|
|
|
struct OptionalEnumCaseMatchingRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule, OptInRule {
|
|
var configuration = SeverityConfiguration(.warning)
|
|
|
|
init() {}
|
|
|
|
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
|
|
}
|
|
"""),
|
|
// https://github.com/apple/swift/issues/61817
|
|
Example("""
|
|
switch bool {
|
|
case true?:
|
|
break
|
|
case false?:
|
|
break
|
|
case .none:
|
|
break
|
|
}
|
|
""", excludeFromDocumentation: true)
|
|
],
|
|
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
|
|
}
|
|
""")
|
|
]
|
|
)
|
|
|
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
Visitor(viewMode: .sourceAccurate)
|
|
}
|
|
|
|
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
|
Rewriter(
|
|
locationConverter: file.locationConverter,
|
|
disabledRegions: disabledRegions(file: file)
|
|
)
|
|
}
|
|
}
|
|
|
|
private extension OptionalEnumCaseMatchingRule {
|
|
final class Visitor: ViolationsSyntaxVisitor {
|
|
override func visitPost(_ node: CaseItemSyntax) {
|
|
guard let pattern = node.pattern.as(ExpressionPatternSyntax.self) else {
|
|
return
|
|
}
|
|
|
|
if let expression = pattern.expression.as(OptionalChainingExprSyntax.self),
|
|
!expression.expression.isDiscardAssignmentOrBoolLiteral {
|
|
violations.append(expression.questionMark.positionAfterSkippingLeadingTrivia)
|
|
} else if let expression = pattern.expression.as(TupleExprSyntax.self) {
|
|
let optionalChainingExpressions = expression.optionalChainingExpressions()
|
|
for optionalChainingExpression in optionalChainingExpressions {
|
|
violations.append(optionalChainingExpression.questionMark.positionAfterSkippingLeadingTrivia)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
|
|
private(set) var correctionPositions: [AbsolutePosition] = []
|
|
let locationConverter: SourceLocationConverter
|
|
let disabledRegions: [SourceRange]
|
|
|
|
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
|
|
self.locationConverter = locationConverter
|
|
self.disabledRegions = disabledRegions
|
|
}
|
|
|
|
override func visit(_ node: CaseItemSyntax) -> CaseItemSyntax {
|
|
guard
|
|
let pattern = node.pattern.as(ExpressionPatternSyntax.self),
|
|
pattern.expression.is(OptionalChainingExprSyntax.self) || pattern.expression.is(TupleExprSyntax.self),
|
|
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
|
else {
|
|
return super.visit(node)
|
|
}
|
|
|
|
if let expression = pattern.expression.as(OptionalChainingExprSyntax.self),
|
|
!expression.expression.isDiscardAssignmentOrBoolLiteral {
|
|
let violationPosition = expression.questionMark.positionAfterSkippingLeadingTrivia
|
|
correctionPositions.append(violationPosition)
|
|
let newPattern = PatternSyntax(pattern.with(\.expression, expression.expression))
|
|
let newNode = node
|
|
.with(\.pattern, newPattern)
|
|
.with(\.whereClause,
|
|
node.whereClause?.with(\.leadingTrivia, expression.questionMark.trailingTrivia))
|
|
return super.visit(newNode)
|
|
} else if let expression = pattern.expression.as(TupleExprSyntax.self) {
|
|
var newExpression = expression
|
|
for (index, element) in expression.elementList.enumerated() {
|
|
guard
|
|
let optionalChainingExpression = element.expression.as(OptionalChainingExprSyntax.self),
|
|
!optionalChainingExpression.expression.is(DiscardAssignmentExprSyntax.self)
|
|
else {
|
|
continue
|
|
}
|
|
|
|
let violationPosition = optionalChainingExpression.questionMark.positionAfterSkippingLeadingTrivia
|
|
correctionPositions.append(violationPosition)
|
|
|
|
let newElement = element.with(\.expression, optionalChainingExpression.expression)
|
|
newExpression.elementList = newExpression.elementList
|
|
.replacing(childAt: index, with: newElement)
|
|
}
|
|
|
|
let newPattern = PatternSyntax(pattern.with(\.expression, ExprSyntax(newExpression)))
|
|
let newNode = node.with(\.pattern, newPattern)
|
|
return super.visit(newNode)
|
|
}
|
|
|
|
return super.visit(node)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension TupleExprSyntax {
|
|
func optionalChainingExpressions() -> [OptionalChainingExprSyntax] {
|
|
elementList
|
|
.compactMap { $0.expression.as(OptionalChainingExprSyntax.self) }
|
|
.filter { !$0.expression.isDiscardAssignmentOrBoolLiteral }
|
|
}
|
|
}
|
|
|
|
private extension ExprSyntax {
|
|
var isDiscardAssignmentOrBoolLiteral: Bool {
|
|
`is`(DiscardAssignmentExprSyntax.self) || `is`(BooleanLiteralExprSyntax.self)
|
|
}
|
|
}
|