@@ -18,54 +18,172 @@ import SwiftSyntax
18
18
/// `case .identifier(let x, let y)` instead.
19
19
///
20
20
/// Lint: `case let .identifier(...)` will yield a lint error.
21
+ ///
22
+ /// Format: `case let .identifier(x, y)` will be replaced by
23
+ /// `case .identifier(let x, let y)`.
21
24
@_spi ( Rules)
22
- public final class UseLetInEveryBoundCaseVariable : SyntaxLintRule {
25
+ public final class UseLetInEveryBoundCaseVariable : SyntaxFormatRule {
26
+ public override func visit( _ node: MatchingPatternConditionSyntax ) -> MatchingPatternConditionSyntax {
27
+ if let ( replacement, specifier) = distributeLetVarThroughPattern ( node. pattern) {
28
+ diagnose ( . useLetInBoundCaseVariables( specifier) , on: node. pattern)
29
+
30
+ var result = node
31
+ result. pattern = PatternSyntax ( replacement)
32
+ return result
33
+ }
34
+
35
+ return super. visit ( node)
36
+ }
37
+
38
+ public override func visit( _ node: SwitchCaseItemSyntax ) -> SwitchCaseItemSyntax {
39
+ if let ( replacement, specifier) = distributeLetVarThroughPattern ( node. pattern) {
40
+ diagnose ( . useLetInBoundCaseVariables( specifier) , on: node. pattern)
41
+
42
+ var result = node
43
+ result. pattern = PatternSyntax ( replacement)
44
+ return result
45
+ }
46
+
47
+ return super. visit ( node)
48
+ }
49
+
50
+ public override func visit( _ node: ForStmtSyntax ) -> StmtSyntax {
51
+ guard node. caseKeyword != nil else {
52
+ return super. visit ( node)
53
+ }
54
+
55
+ if let ( replacement, specifier) = distributeLetVarThroughPattern ( node. pattern) {
56
+ diagnose ( . useLetInBoundCaseVariables( specifier) , on: node. pattern)
23
57
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. pattern) {
28
- diagnose ( . useLetInBoundCaseVariables, on: node)
58
+ var result = node
59
+ result. pattern = PatternSyntax ( replacement)
60
+ return StmtSyntax ( result)
29
61
}
30
- return . visitChildren
62
+
63
+ return super. visit ( node)
64
+ }
65
+ }
66
+
67
+ extension UseLetInEveryBoundCaseVariable {
68
+ private enum OptionalPatternKind {
69
+ case chained
70
+ case forced
31
71
}
32
72
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 }
73
+ /// Wraps the given expression in the optional chaining and/or force
74
+ /// unwrapping expressions, as described by the specified stack.
75
+ private func restoreOptionalChainingAndForcing(
76
+ _ expr: ExprSyntax ,
77
+ patternStack: [ ( OptionalPatternKind , Trivia ) ]
78
+ ) -> ExprSyntax {
79
+ var patternStack = patternStack
80
+ var result = expr
81
+
82
+ // As we unwind the stack, wrap the expression in optional chaining
83
+ // or force unwrap expressions.
84
+ while let ( kind, trivia) = patternStack. popLast ( ) {
85
+ if kind == . chained {
86
+ result = ExprSyntax ( OptionalChainingExprSyntax (
87
+ expression: result, trailingTrivia: trivia) )
88
+ } else {
89
+ result = ExprSyntax ( ForceUnwrapExprSyntax (
90
+ expression: result, trailingTrivia: trivia) )
91
+ }
92
+ }
93
+
94
+ return result
95
+ }
96
+
97
+ /// Returns a rewritten version of the given pattern if bindings can be moved
98
+ /// into bound cases.
99
+ ///
100
+ /// - Parameter pattern: The pattern to rewrite.
101
+ /// - Returns: An optional tuple with the rewritten pattern and the binding
102
+ /// specifier used in `pattern`, for use in the diagnostic. If `pattern`
103
+ /// doesn't qualify for distributing the binding, then the result is `nil`.
104
+ private func distributeLetVarThroughPattern(
105
+ _ pattern: PatternSyntax
106
+ ) -> ( ExpressionPatternSyntax , TokenSyntax ) ? {
107
+ guard let bindingPattern = pattern. as ( ValueBindingPatternSyntax . self) ,
108
+ let exprPattern = bindingPattern. pattern. as ( ExpressionPatternSyntax . self)
109
+ else { return nil }
37
110
111
+ // Grab the `let` or `var` used in the binding pattern.
112
+ let specifier = bindingPattern. bindingSpecifier
113
+ let identifierBinder = BindIdentifiersRewriter ( bindingSpecifier: specifier)
114
+
38
115
// Drill down into any optional patterns that we encounter (e.g., `case let .foo(x)?`).
116
+ var patternStack : [ ( OptionalPatternKind , Trivia ) ] = [ ]
39
117
var expression = exprPattern. expression
40
118
while true {
41
119
if let optionalExpr = expression. as ( OptionalChainingExprSyntax . self) {
42
120
expression = optionalExpr. expression
121
+ patternStack. append ( ( . chained, optionalExpr. questionMark. trailingTrivia) )
43
122
} else if let forcedExpr = expression. as ( ForceUnwrapExprSyntax . self) {
44
123
expression = forcedExpr. expression
124
+ patternStack. append ( ( . forced, forcedExpr. exclamationMark. trailingTrivia) )
45
125
} else {
46
126
break
47
127
}
48
128
}
49
129
50
130
// Enum cases are written as function calls on member access expressions. The arguments
51
131
// are the associated values, so the `let/var` can be distributed into those.
52
- if let functionCall = expression. as ( FunctionCallExprSyntax . self) ,
132
+ if var functionCall = expression. as ( FunctionCallExprSyntax . self) ,
53
133
functionCall. calledExpression. is ( MemberAccessExprSyntax . self)
54
134
{
55
- return true
135
+ var result = exprPattern
136
+ let newArguments = identifierBinder. rewrite ( functionCall. arguments)
137
+ functionCall. arguments = newArguments. as ( LabeledExprListSyntax . self) !
138
+ result. expression = restoreOptionalChainingAndForcing (
139
+ ExprSyntax ( functionCall) ,
140
+ patternStack: patternStack)
141
+ return ( result, specifier)
56
142
}
57
143
58
144
// A tuple expression can have the `let/var` distributed into the elements.
59
- if expression. is ( TupleExprSyntax . self) {
60
- return true
145
+ if var tupleExpr = expression. as ( TupleExprSyntax . self) {
146
+ var result = exprPattern
147
+ let newElements = identifierBinder. rewrite ( tupleExpr. elements)
148
+ tupleExpr. elements = newElements. as ( LabeledExprListSyntax . self) !
149
+ result. expression = restoreOptionalChainingAndForcing (
150
+ ExprSyntax ( tupleExpr) ,
151
+ patternStack: patternStack)
152
+ return ( result, specifier)
61
153
}
62
154
63
155
// Otherwise, we're not sure this is a pattern we can distribute through.
64
- return false
156
+ return nil
65
157
}
66
158
}
67
159
68
160
extension Finding . Message {
69
- fileprivate static let useLetInBoundCaseVariables : Finding . Message =
70
- " move this 'let' keyword inside the 'case' pattern, before each of the bound variables "
161
+ fileprivate static func useLetInBoundCaseVariables(
162
+ _ specifier: TokenSyntax
163
+ ) -> Finding . Message {
164
+ " move this ' \( specifier. text) ' keyword inside the 'case' pattern, before each of the bound variables "
165
+ }
166
+ }
167
+
168
+ /// A syntax rewriter that converts identifier patterns to bindings
169
+ /// with the given specifier.
170
+ private final class BindIdentifiersRewriter : SyntaxRewriter {
171
+ var bindingSpecifier : TokenSyntax
172
+
173
+ init ( bindingSpecifier: TokenSyntax ) {
174
+ self . bindingSpecifier = bindingSpecifier
175
+ }
176
+
177
+ override func visit( _ node: PatternExprSyntax ) -> ExprSyntax {
178
+ guard let identifier = node. pattern. as ( IdentifierPatternSyntax . self) else {
179
+ return super. visit ( node)
180
+ }
181
+
182
+ let binding = ValueBindingPatternSyntax (
183
+ bindingSpecifier: bindingSpecifier,
184
+ pattern: identifier)
185
+ var result = node
186
+ result. pattern = PatternSyntax ( binding)
187
+ return ExprSyntax ( result)
188
+ }
71
189
}
0 commit comments