@@ -18,54 +18,185 @@ import SwiftSyntax
1818/// `case .identifier(let x, let y)` instead.
1919///
2020/// 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)`.
2124@_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+ }
2373
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+ }
29101 }
30- return . visitChildren
102+
103+ return result
31104 }
32105
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)
37124
38125 // Drill down into any optional patterns that we encounter (e.g., `case let .foo(x)?`).
126+ var patternStack : [ ( OptionalPatternKind , Trivia ) ] = [ ]
39127 var expression = exprPattern. expression
40128 while true {
41129 if let optionalExpr = expression. as ( OptionalChainingExprSyntax . self) {
42130 expression = optionalExpr. expression
131+ patternStack. append ( ( . chained, optionalExpr. questionMark. trailingTrivia) )
43132 } else if let forcedExpr = expression. as ( ForceUnwrapExprSyntax . self) {
44133 expression = forcedExpr. expression
134+ patternStack. append ( ( . forced, forcedExpr. exclamationMark. trailingTrivia) )
45135 } else {
46136 break
47137 }
48138 }
49139
50140 // Enum cases are written as function calls on member access expressions. The arguments
51141 // 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) ,
53143 functionCall. calledExpression. is ( MemberAccessExprSyntax . self)
54144 {
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)
56153 }
57154
58155 // 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)
61165 }
62166
63167 // Otherwise, we're not sure this is a pattern we can distribute through.
64- return false
168+ return nil
65169 }
66170}
67171
68172extension 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+ }
71202}
0 commit comments