Skip to content

Commit 6104398

Browse files
committed
Rewrite and fix OneVariableDeclarationPerLine.
The old rule implementation assumed that if there were multiple bindings in a single var decl, the last one contained the type that should be applied to all of them. The rules are actually more complex than that, because they don't apply to bindings that have initializer clauses (from which the type is inferred), and they also allow multiple sequences of bindings in the same decl (e.g., `var a, b: Int, c, d: String`). This change also ensures proper handling of leading comments (does not duplicate them) and that nested declarations are transformed correctly. Fixes SR-13051.
1 parent 470b634 commit 6104398

File tree

2 files changed

+379
-116
lines changed

2 files changed

+379
-116
lines changed

Sources/SwiftFormatRules/OneVariableDeclarationPerLine.swift

+165-65
Original file line numberDiff line numberDiff line change
@@ -13,96 +13,196 @@
1313
import SwiftFormatCore
1414
import SwiftSyntax
1515

16-
/// Each variable declaration, with the exception of tuple destructuring, should declare 1 variable.
16+
/// Each variable declaration, with the exception of tuple destructuring, should
17+
/// declare 1 variable.
1718
///
18-
/// Lint: If a variable declaration declares multiple variables, a lint error is raised.
19+
/// Lint: If a variable declaration declares multiple variables, a lint error is
20+
/// raised.
1921
///
20-
/// Format: If a variable declaration declares multiple variables, it will be split into multiple
21-
/// declarations, each declaring one of the variables.
22+
/// Format: If a variable declaration declares multiple variables, it will be
23+
/// split into multiple declarations, each declaring one of the variables, as
24+
/// long as the result would still be syntactically valid.
2225
public final class OneVariableDeclarationPerLine: SyntaxFormatRule {
23-
private func splitVariableDecls(
24-
_ items: CodeBlockItemListSyntax
25-
) -> CodeBlockItemListSyntax? {
26-
// If we're here, then there's at least one VariableDeclSyntax that
27-
// needs to be split.
28-
var needsWork = false
29-
for codeBlockItem in items {
30-
if let varDecl = codeBlockItem.item.as(VariableDeclSyntax.self), varDecl.bindings.count > 1 {
31-
needsWork = true
32-
}
26+
public override func visit(_ node: CodeBlockItemListSyntax) -> Syntax {
27+
guard node.contains(where: codeBlockItemHasMultipleVariableBindings) else {
28+
return super.visit(node)
3329
}
34-
if !needsWork { return nil }
3530

3631
var newItems = [CodeBlockItemSyntax]()
37-
for codeBlockItem in items {
38-
// If we're not looking at a VariableDecl with more than 1 binding, visit the item and
39-
// skip it.
32+
for codeBlockItem in node {
4033
guard let varDecl = codeBlockItem.item.as(VariableDeclSyntax.self),
4134
varDecl.bindings.count > 1
4235
else {
43-
newItems.append(codeBlockItem)
36+
// It's not a variable declaration with multiple bindings, so visit it
37+
// recursively (in case it's something that contains bindings that need
38+
// to be split) but otherwise do nothing.
39+
let newItem = super.visit(codeBlockItem).as(CodeBlockItemSyntax.self)!
40+
newItems.append(newItem)
4441
continue
4542
}
4643

4744
diagnose(.onlyOneVariableDeclaration, on: varDecl)
4845

49-
// The first binding corresponds to the original `var`/`let`
50-
// declaration, so it should not have its trivia replaced.
51-
var isFirst = true
52-
// The last binding only has type annotation when multiple
53-
// declarations in one line, so make a new binding with its type
54-
let typeAnnotation = varDecl.bindings.last?.typeAnnotation
55-
for binding in varDecl.bindings {
56-
var newBinding = binding.withTrailingComma(nil)
57-
if typeAnnotation != nil && binding.typeAnnotation == nil {
58-
newBinding = newBinding.withTypeAnnotation(typeAnnotation)
59-
}
60-
let newDecl = varDecl.withBindings(
61-
SyntaxFactory.makePatternBindingList([newBinding]))
62-
var finalDecl = Syntax(newDecl)
63-
// Only add a newline if this is a brand new binding.
64-
if !isFirst {
65-
let firstTok = newDecl.firstToken
66-
let origLeading = firstTok?.leadingTrivia.withoutNewlines() ?? []
67-
finalDecl = replaceTrivia(
68-
on: finalDecl,
69-
token: newDecl.firstToken,
70-
leadingTrivia: .newlines(1) + origLeading
71-
)
72-
}
73-
let newCodeBlockItem = codeBlockItem.withItem(finalDecl)
74-
newItems.append(newCodeBlockItem)
75-
isFirst = false
46+
// Visit the decl recursively to make sure nested code block items in the
47+
// bindings (for example, an initializer expression that contains a
48+
// closure expression) are transformed first before we rewrite the decl
49+
// itself.
50+
let visitedDecl = super.visit(varDecl).as(VariableDeclSyntax.self)!
51+
var splitter = VariableDeclSplitter {
52+
SyntaxFactory.makeCodeBlockItem(
53+
item: Syntax($0),
54+
semicolon: nil,
55+
errorTokens: nil)
7656
}
57+
newItems.append(contentsOf: splitter.nodes(bySplitting: visitedDecl))
7758
}
78-
return SyntaxFactory.makeCodeBlockItemList(newItems)
79-
}
8059

81-
public override func visit(_ node: CodeBlockSyntax) -> Syntax {
82-
guard let newStmts = splitVariableDecls(node.statements) else {
83-
return super.visit(node)
84-
}
85-
return Syntax(node.withStatements(newStmts))
60+
return Syntax(SyntaxFactory.makeCodeBlockItemList(newItems))
8661
}
8762

88-
public override func visit(_ node: ClosureExprSyntax) -> ExprSyntax {
89-
guard let newStmts = splitVariableDecls(node.statements) else {
90-
return super.visit(node)
63+
/// Returns true if the given `CodeBlockItemSyntax` contains a `let` or `var`
64+
/// declaration with multiple bindings.
65+
private func codeBlockItemHasMultipleVariableBindings(
66+
_ node: CodeBlockItemSyntax
67+
) -> Bool {
68+
if let varDecl = node.item.as(VariableDeclSyntax.self),
69+
varDecl.bindings.count > 1
70+
{
71+
return true
9172
}
92-
return ExprSyntax(node.withStatements(newStmts))
93-
}
94-
95-
public override func visit(_ node: SourceFileSyntax) -> Syntax {
96-
guard let newStmts = splitVariableDecls(node.statements) else {
97-
return super.visit(node)
98-
}
99-
return Syntax(node.withStatements(newStmts))
73+
return false
10074
}
10175
}
10276

10377
extension Diagnostic.Message {
10478
public static let onlyOneVariableDeclaration = Diagnostic.Message(
10579
.warning,
106-
"split variable binding into multiple declarations"
80+
"split this variable declaration to have one variable per declaration"
10781
)
10882
}
83+
84+
/// Splits a variable declaration with multiple bindings into individual
85+
/// declarations.
86+
///
87+
/// Swift's grammar allows each identifier in a variable declaration to have a
88+
/// type annotation, an initializer expression, both, or neither. Stricter
89+
/// checks occur after parsing, however; a lone identifier may only be followed
90+
/// by zero or more other lone identifiers and then an identifier with *only* a
91+
/// type annotation (and the type annotation is applied to all of them). If we
92+
/// have something else, we should handle them gracefully (i.e., not destroy
93+
/// them) but we don't need to try to fix them since they didn't compile in the
94+
/// first place so we can't guess what the user intended.
95+
///
96+
/// So, this algorithm works by scanning forward and collecting lone identifiers
97+
/// in a queue until we reach a binding that has an initializer or a type
98+
/// annotation. If we see a type annotation (without an initializer), we can
99+
/// create individual variable declarations for each entry in the queue by
100+
/// projecting that type annotation onto each of them. If we reach a case that
101+
/// isn't valid, we just flush the queue contents as a single declaration, to
102+
/// effectively preserve what the user originally had.
103+
private struct VariableDeclSplitter<Node: SyntaxProtocol> {
104+
/// A function that takes a `VariableDeclSyntax` and returns a new node, such
105+
/// as a `CodeBlockItemSyntax`, that wraps it.
106+
private let generator: (VariableDeclSyntax) -> Node
107+
108+
/// Bindings that have been collected so far.
109+
private var bindingQueue = [PatternBindingSyntax]()
110+
111+
/// The variable declaration being split.
112+
///
113+
/// This is an implicitly-unwrapped optional because it isn't initialized
114+
/// until `nodes(bySplitting:)` is called.
115+
private var varDecl: VariableDeclSyntax!
116+
117+
/// The list of nodes generated by splitting the variable declaration into
118+
/// individual bindings.
119+
private var nodes = [Node]()
120+
121+
/// Tracks whether the trivia of `varDecl` has already been fixed up for nodes
122+
/// after the first.
123+
private var fixedUpTrivia = false
124+
125+
/// Creates a new variable declaration splitter.
126+
///
127+
/// - Parameter generator: A function that takes a `VariableDeclSyntax` and
128+
/// returns a new node, such as a `CodeBlockItemSyntax`, that wraps it.
129+
init(generator: @escaping (VariableDeclSyntax) -> Node) {
130+
self.generator = generator
131+
}
132+
133+
/// Returns an array of nodes generated by splitting the given variable
134+
/// declaration into individual bindings.
135+
mutating func nodes(bySplitting varDecl: VariableDeclSyntax) -> [Node] {
136+
self.varDecl = varDecl
137+
self.nodes = []
138+
139+
for binding in varDecl.bindings {
140+
if binding.initializer != nil {
141+
// If this is the only initializer in the queue so far, that's ok. If
142+
// it's an initializer following other un-flushed lone identifier
143+
// bindings, that's not valid Swift. But in either case, we'll flush
144+
// them as a single decl.
145+
bindingQueue.append(binding.withTrailingComma(nil))
146+
flushRemaining()
147+
} else if let typeAnnotation = binding.typeAnnotation {
148+
bindingQueue.append(binding)
149+
flushIndividually(typeAnnotation: typeAnnotation)
150+
} else {
151+
bindingQueue.append(binding)
152+
}
153+
}
154+
flushRemaining()
155+
156+
return nodes
157+
}
158+
159+
/// Replaces the original variable declaration with a copy of itself with
160+
/// updates trivia appropriate for subsequent declarations inserted by the
161+
/// rule.
162+
private mutating func fixOriginalVarDeclTrivia() {
163+
guard !fixedUpTrivia else { return }
164+
165+
// We intentionally don't try to infer the indentation for subsequent
166+
// lines because the pretty printer will re-indent them correctly; we just
167+
// need to ensure that a newline is inserted before new decls.
168+
varDecl = replaceTrivia(
169+
on: varDecl, token: varDecl.firstToken, leadingTrivia: .newlines(1))
170+
fixedUpTrivia = true
171+
}
172+
173+
/// Flushes any remaining bindings as a single variable declaration.
174+
private mutating func flushRemaining() {
175+
guard !bindingQueue.isEmpty else { return }
176+
177+
let newDecl =
178+
varDecl.withBindings(SyntaxFactory.makePatternBindingList(bindingQueue))
179+
nodes.append(generator(newDecl))
180+
181+
fixOriginalVarDeclTrivia()
182+
183+
bindingQueue = []
184+
}
185+
186+
/// Flushes any remaining bindings as individual variable declarations where
187+
/// each has the given type annotation.
188+
private mutating func flushIndividually(
189+
typeAnnotation: TypeAnnotationSyntax
190+
) {
191+
assert(!bindingQueue.isEmpty)
192+
193+
for binding in bindingQueue {
194+
assert(binding.initializer == nil)
195+
196+
let newBinding =
197+
binding.withTrailingComma(nil).withTypeAnnotation(typeAnnotation)
198+
let newDecl =
199+
varDecl.withBindings(SyntaxFactory.makePatternBindingList([newBinding]))
200+
nodes.append(generator(newDecl))
201+
202+
fixOriginalVarDeclTrivia()
203+
}
204+
205+
bindingQueue = []
206+
}
207+
}
208+

0 commit comments

Comments
 (0)