Skip to content

Commit 88b8bc4

Browse files
committed
Convert UseLetInEveryBoundCaseVariable to be a formatter
Uses the existing logic for determining whether a binding can be moved into a tuple or function call context, and then rewrites the tuple element list or argument list to including the binding keyword. Additionally, improves the diagnostic by using the binding specifier in the source code (i.e. `let` vs `var`).
1 parent 8cb0e35 commit 88b8bc4

File tree

4 files changed

+328
-84
lines changed

4 files changed

+328
-84
lines changed

Documentation/RuleDocumentation.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,10 @@ For example, `case let .identifier(x, y)` is forbidden. Use
505505

506506
Lint: `case let .identifier(...)` will yield a lint error.
507507

508-
`UseLetInEveryBoundCaseVariable` is a linter-only rule.
508+
Format: `case let .identifier(x, y)` will be replaced by
509+
`case .identifier(let x, let y)`.
510+
511+
`UseLetInEveryBoundCaseVariable` rule can format your code automatically.
509512

510513
### UseShorthandTypeNames
511514

Sources/SwiftFormat/Core/Pipelines+Generated.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,12 @@ class LintPipeline: SyntaxVisitor {
225225
}
226226

227227
override func visit(_ node: ForStmtSyntax) -> SyntaxVisitorContinueKind {
228+
visitIfEnabled(UseLetInEveryBoundCaseVariable.visit, for: node)
228229
visitIfEnabled(UseWhereClausesInForLoops.visit, for: node)
229230
return .visitChildren
230231
}
231232
override func visitPost(_ node: ForStmtSyntax) {
233+
onVisitPost(rule: UseLetInEveryBoundCaseVariable.self, for: node)
232234
onVisitPost(rule: UseWhereClausesInForLoops.self, for: node)
233235
}
234236

@@ -386,6 +388,14 @@ class LintPipeline: SyntaxVisitor {
386388
onVisitPost(rule: NoPlaygroundLiterals.self, for: node)
387389
}
388390

391+
override func visit(_ node: MatchingPatternConditionSyntax) -> SyntaxVisitorContinueKind {
392+
visitIfEnabled(UseLetInEveryBoundCaseVariable.visit, for: node)
393+
return .visitChildren
394+
}
395+
override func visitPost(_ node: MatchingPatternConditionSyntax) {
396+
onVisitPost(rule: UseLetInEveryBoundCaseVariable.self, for: node)
397+
}
398+
389399
override func visit(_ node: MemberBlockItemListSyntax) -> SyntaxVisitorContinueKind {
390400
visitIfEnabled(DoNotUseSemicolons.visit, for: node)
391401
return .visitChildren
@@ -510,6 +520,14 @@ class LintPipeline: SyntaxVisitor {
510520
onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node)
511521
}
512522

523+
override func visit(_ node: SwitchCaseItemSyntax) -> SyntaxVisitorContinueKind {
524+
visitIfEnabled(UseLetInEveryBoundCaseVariable.visit, for: node)
525+
return .visitChildren
526+
}
527+
override func visitPost(_ node: SwitchCaseItemSyntax) {
528+
onVisitPost(rule: UseLetInEveryBoundCaseVariable.self, for: node)
529+
}
530+
513531
override func visit(_ node: SwitchCaseLabelSyntax) -> SyntaxVisitorContinueKind {
514532
visitIfEnabled(NoLabelsInCasePatterns.visit, for: node)
515533
return .visitChildren
@@ -568,14 +586,6 @@ class LintPipeline: SyntaxVisitor {
568586
onVisitPost(rule: UseTripleSlashForDocumentationComments.self, for: node)
569587
}
570588

571-
override func visit(_ node: ValueBindingPatternSyntax) -> SyntaxVisitorContinueKind {
572-
visitIfEnabled(UseLetInEveryBoundCaseVariable.visit, for: node)
573-
return .visitChildren
574-
}
575-
override func visitPost(_ node: ValueBindingPatternSyntax) {
576-
onVisitPost(rule: UseLetInEveryBoundCaseVariable.self, for: node)
577-
}
578-
579589
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
580590
visitIfEnabled(AllPublicDeclarationsHaveDocumentation.visit, for: node)
581591
visitIfEnabled(AlwaysUseLowerCamelCase.visit, for: node)
@@ -627,6 +637,7 @@ extension FormatPipeline {
627637
node = ReturnVoidInsteadOfEmptyTuple(context: context).rewrite(node)
628638
node = UseEarlyExits(context: context).rewrite(node)
629639
node = UseExplicitNilCheckInConditions(context: context).rewrite(node)
640+
node = UseLetInEveryBoundCaseVariable(context: context).rewrite(node)
630641
node = UseShorthandTypeNames(context: context).rewrite(node)
631642
node = UseSingleLinePropertyGetter(context: context).rewrite(node)
632643
node = UseTripleSlashForDocumentationComments(context: context).rewrite(node)

Sources/SwiftFormat/Rules/UseLetInEveryBoundCaseVariable.swift

Lines changed: 136 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,54 +18,172 @@ 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+
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)
2357

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)
2961
}
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
3171
}
3272

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 }
37110

111+
// Grab the `let` or `var` used in the binding pattern.
112+
let specifier = bindingPattern.bindingSpecifier
113+
let identifierBinder = BindIdentifiersRewriter(bindingSpecifier: specifier)
114+
38115
// Drill down into any optional patterns that we encounter (e.g., `case let .foo(x)?`).
116+
var patternStack: [(OptionalPatternKind, Trivia)] = []
39117
var expression = exprPattern.expression
40118
while true {
41119
if let optionalExpr = expression.as(OptionalChainingExprSyntax.self) {
42120
expression = optionalExpr.expression
121+
patternStack.append((.chained, optionalExpr.questionMark.trailingTrivia))
43122
} else if let forcedExpr = expression.as(ForceUnwrapExprSyntax.self) {
44123
expression = forcedExpr.expression
124+
patternStack.append((.forced, forcedExpr.exclamationMark.trailingTrivia))
45125
} else {
46126
break
47127
}
48128
}
49129

50130
// Enum cases are written as function calls on member access expressions. The arguments
51131
// 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),
53133
functionCall.calledExpression.is(MemberAccessExprSyntax.self)
54134
{
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)
56142
}
57143

58144
// 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)
61153
}
62154

63155
// Otherwise, we're not sure this is a pattern we can distribute through.
64-
return false
156+
return nil
65157
}
66158
}
67159

68160
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+
}
71189
}

0 commit comments

Comments
 (0)