|
13 | 13 | import SwiftFormatCore
|
14 | 14 | import SwiftSyntax
|
15 | 15 |
|
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. |
17 | 18 | ///
|
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. |
19 | 21 | ///
|
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. |
22 | 25 | 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) |
33 | 29 | }
|
34 |
| - if !needsWork { return nil } |
35 | 30 |
|
36 | 31 | 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 { |
40 | 33 | guard let varDecl = codeBlockItem.item.as(VariableDeclSyntax.self),
|
41 | 34 | varDecl.bindings.count > 1
|
42 | 35 | 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) |
44 | 41 | continue
|
45 | 42 | }
|
46 | 43 |
|
47 | 44 | diagnose(.onlyOneVariableDeclaration, on: varDecl)
|
48 | 45 |
|
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) |
76 | 56 | }
|
| 57 | + newItems.append(contentsOf: splitter.nodes(bySplitting: visitedDecl)) |
77 | 58 | }
|
78 |
| - return SyntaxFactory.makeCodeBlockItemList(newItems) |
79 |
| - } |
80 | 59 |
|
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)) |
86 | 61 | }
|
87 | 62 |
|
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 |
91 | 72 | }
|
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 |
100 | 74 | }
|
101 | 75 | }
|
102 | 76 |
|
103 | 77 | extension Diagnostic.Message {
|
104 | 78 | public static let onlyOneVariableDeclaration = Diagnostic.Message(
|
105 | 79 | .warning,
|
106 |
| - "split variable binding into multiple declarations" |
| 80 | + "split this variable declaration to have one variable per declaration" |
107 | 81 | )
|
108 | 82 | }
|
| 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