Skip to content

Commit c3c17ad

Browse files
authored
Merge pull request #244 from allevato/distributed-let
Improve pattern recognition in `UseLetInEveryBoundCaseVariable`.
2 parents 842f22a + 49136e6 commit c3c17ad

File tree

2 files changed

+146
-21
lines changed

2 files changed

+146
-21
lines changed

Sources/SwiftFormatRules/UseLetInEveryBoundCaseVariable.swift

+44-8
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,60 @@
1313
import SwiftFormatCore
1414
import SwiftSyntax
1515

16-
/// Every element bound in a `case` must have its own `let`.
16+
/// Every variable bound in a `case` pattern must have its own `let/var`.
1717
///
18-
/// e.g. `case let .label(foo, bar)` is forbidden.
18+
/// For example, `case let .identifier(x, y)` is forbidden. Use
19+
/// `case .identifier(let x, let y)` instead.
1920
///
20-
/// Lint: `case let ...` will yield a lint error.
21+
/// Lint: `case let .identifier(...)` will yield a lint error.
2122
public final class UseLetInEveryBoundCaseVariable: SyntaxLintRule {
2223

23-
public override func visit(_ node: SwitchCaseLabelSyntax) -> SyntaxVisitorContinueKind {
24-
for item in node.caseItems {
25-
guard item.pattern.is(ValueBindingPatternSyntax.self) else { continue }
24+
public override func visit(_ node: ValueBindingPatternSyntax) -> SyntaxVisitorContinueKind {
25+
// Diagnose a pattern binding if it is a function call and the callee is a member access
26+
// expression (e.g., `case let .x(y)` or `case let T.x(y)`).
27+
if canDistributeLetVarThroughPattern(node.valuePattern) {
2628
diagnose(.useLetInBoundCaseVariables, on: node)
2729
}
28-
return .skipChildren
30+
return .visitChildren
31+
}
32+
33+
/// Returns true if the given pattern is one that allows a `let/var` to be distributed
34+
/// through to subpatterns.
35+
private func canDistributeLetVarThroughPattern(_ pattern: PatternSyntax) -> Bool {
36+
guard let exprPattern = pattern.as(ExpressionPatternSyntax.self) else { return false }
37+
38+
// Drill down into any optional patterns that we encounter (e.g., `case let .foo(x)?`).
39+
var expression = exprPattern.expression
40+
while true {
41+
if let optionalExpr = expression.as(OptionalChainingExprSyntax.self) {
42+
expression = optionalExpr.expression
43+
} else if let forcedExpr = expression.as(ForcedValueExprSyntax.self) {
44+
expression = forcedExpr.expression
45+
} else {
46+
break
47+
}
48+
}
49+
50+
// Enum cases are written as function calls on member access expressions. The arguments
51+
// are the associated values, so the `let/var` can be distributed into those.
52+
if let functionCall = expression.as(FunctionCallExprSyntax.self),
53+
functionCall.calledExpression.is(MemberAccessExprSyntax.self)
54+
{
55+
return true
56+
}
57+
58+
// A tuple expression can have the `let/var` distributed into the elements.
59+
if expression.is(TupleExprSyntax.self) {
60+
return true
61+
}
62+
63+
// Otherwise, we're not sure this is a pattern we can distribute through.
64+
return false
2965
}
3066
}
3167

3268
extension Diagnostic.Message {
3369
public static let useLetInBoundCaseVariables = Diagnostic.Message(
3470
.warning,
35-
"distribute 'let' to each bound case variable")
71+
"move 'let' keyword to precede each variable bound in the `case` pattern")
3672
}
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,115 @@
11
import SwiftFormatRules
22

33
final class UseLetInEveryBoundCaseVariableTests: LintOrFormatRuleTestCase {
4-
func testInvalidLetBoundCase() {
4+
override func setUp() {
5+
super.setUp()
6+
self.shouldCheckForUnassertedDiagnostics = true
7+
}
8+
9+
func testSwitchCase() {
510
let input =
611
"""
712
switch DataPoint.labeled("hello", 100) {
8-
case let .labeled(label, value):
9-
break
13+
case let .labeled(label, value): break
14+
case .labeled(label, let value): break
15+
case .labeled(let label, let value): break
16+
case let .labeled(label, value)?: break
17+
case let .labeled(label, value)!: break
18+
case let .labeled(label, value)??: break
19+
case let (label, value): break
20+
case let x as SomeType: break
1021
}
22+
"""
23+
performLint(UseLetInEveryBoundCaseVariable.self, input: input)
1124

12-
switch DataPoint.labeled("hello", 100) {
13-
case .labeled(label, let value):
14-
break
15-
}
25+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 2, column: 6)
26+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 5, column: 6)
27+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 6, column: 6)
28+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 7, column: 6)
29+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 8, column: 6)
30+
}
1631

17-
switch DataPoint.labeled("hello", 100) {
18-
case .labeled(let label, let value):
19-
break
20-
}
32+
func testIfCase() {
33+
let input =
34+
"""
35+
if case let .labeled(label, value) = DataPoint.labeled("hello", 100) {}
36+
if case .labeled(label, let value) = DataPoint.labeled("hello", 100) {}
37+
if case .labeled(let label, let value) = DataPoint.labeled("hello", 100) {}
38+
if case let .labeled(label, value)? = DataPoint.labeled("hello", 100) {}
39+
if case let .labeled(label, value)! = DataPoint.labeled("hello", 100) {}
40+
if case let .labeled(label, value)?? = DataPoint.labeled("hello", 100) {}
41+
if case let (label, value) = DataPoint.labeled("hello", 100) {}
42+
if case let x as SomeType = someValue {}
2143
"""
2244
performLint(UseLetInEveryBoundCaseVariable.self, input: input)
23-
XCTAssertDiagnosed(.useLetInBoundCaseVariables)
24-
XCTAssertNotDiagnosed(.useLetInBoundCaseVariables)
45+
46+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 1, column: 9)
47+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 4, column: 9)
48+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 5, column: 9)
49+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 6, column: 9)
50+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 7, column: 9)
51+
}
52+
53+
func testGuardCase() {
54+
let input =
55+
"""
56+
guard case let .labeled(label, value) = DataPoint.labeled("hello", 100) else {}
57+
guard case .labeled(label, let value) = DataPoint.labeled("hello", 100) else {}
58+
guard case .labeled(let label, let value) = DataPoint.labeled("hello", 100) else {}
59+
guard case let .labeled(label, value)? = DataPoint.labeled("hello", 100) else {}
60+
guard case let .labeled(label, value)! = DataPoint.labeled("hello", 100) else {}
61+
guard case let .labeled(label, value)?? = DataPoint.labeled("hello", 100) else {}
62+
guard case let (label, value) = DataPoint.labeled("hello", 100) else {}
63+
guard case let x as SomeType = someValue else {}
64+
"""
65+
performLint(UseLetInEveryBoundCaseVariable.self, input: input)
66+
67+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 1, column: 12)
68+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 4, column: 12)
69+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 5, column: 12)
70+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 6, column: 12)
71+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 7, column: 12)
72+
}
73+
74+
func testForCase() {
75+
let input =
76+
"""
77+
for case let .labeled(label, value) in dataPoints {}
78+
for case .labeled(label, let value) in dataPoints {}
79+
for case .labeled(let label, let value) in dataPoints {}
80+
for case let .labeled(label, value)? in dataPoints {}
81+
for case let .labeled(label, value)! in dataPoints {}
82+
for case let .labeled(label, value)?? in dataPoints {}
83+
for case let (label, value) in dataPoints {}
84+
for case let x as SomeType in {}
85+
"""
86+
performLint(UseLetInEveryBoundCaseVariable.self, input: input)
87+
88+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 1, column: 10)
89+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 4, column: 10)
90+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 5, column: 10)
91+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 6, column: 10)
92+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 7, column: 10)
93+
}
94+
95+
func testWhileCase() {
96+
let input =
97+
"""
98+
while case let .labeled(label, value) = iter.next() {}
99+
while case .labeled(label, let value) = iter.next() {}
100+
while case .labeled(let label, let value) = iter.next() {}
101+
while case let .labeled(label, value)? = iter.next() {}
102+
while case let .labeled(label, value)! = iter.next() {}
103+
while case let .labeled(label, value)?? = iter.next() {}
104+
while case let (label, value) = iter.next() {}
105+
while case let x as SomeType = iter.next() {}
106+
"""
107+
performLint(UseLetInEveryBoundCaseVariable.self, input: input)
108+
109+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 1, column: 12)
110+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 4, column: 12)
111+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 5, column: 12)
112+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 6, column: 12)
113+
XCTAssertDiagnosed(.useLetInBoundCaseVariables, line: 7, column: 12)
25114
}
26115
}

0 commit comments

Comments
 (0)