Skip to content

Commit f8e608e

Browse files
authored
Merge pull request #926 from natecook1000/bound-case-formatter
Convert `UseLetInEveryBoundCaseVariable` to be a formatter
2 parents 8cb0e35 + bc0530c commit f8e608e

File tree

5 files changed

+390
-84
lines changed

5 files changed

+390
-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: 149 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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

68172
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+
}
71202
}

0 commit comments

Comments
 (0)