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