-
Notifications
You must be signed in to change notification settings - Fork 638
/
Copy pathConditionalAssignment.swift
285 lines (251 loc) · 13.2 KB
/
ConditionalAssignment.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
//
// ConditionalAssignment.swift
// SwiftFormat
//
// Created by Cal Stephens on 2/14/23.
// Copyright © 2024 Nick Lockwood. All rights reserved.
//
import Foundation
public extension FormatRule {
static let conditionalAssignment = FormatRule(
help: "Assign properties using if / switch expressions.",
orderAfter: [.redundantReturn],
options: ["condassignment"]
) { formatter in
// If / switch expressions were added in Swift 5.9 (SE-0380)
guard formatter.options.swiftVersion >= "5.9" else {
return
}
formatter.forEach(.keyword) { startOfConditional, keywordToken in
// Look for an if/switch expression where the first branch starts with `identifier =`
guard ["if", "switch"].contains(keywordToken.string),
let conditionalBranches = formatter.conditionalBranches(at: startOfConditional),
var startOfFirstBranch = conditionalBranches.first?.startOfBranch
else { return }
// Traverse any nested if/switch branches until we find the first code branch
while let firstTokenInBranch = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: startOfFirstBranch),
["if", "switch"].contains(formatter.tokens[firstTokenInBranch].string),
let nestedConditionalBranches = formatter.conditionalBranches(at: firstTokenInBranch),
let startOfNestedBranch = nestedConditionalBranches.first?.startOfBranch
{
startOfFirstBranch = startOfNestedBranch
}
// Check if the first branch starts with the pattern `lvalue =`.
guard let firstTokenIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: startOfFirstBranch),
let lvalueRange = formatter.parseExpressionRange(startingAt: firstTokenIndex),
let equalsIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: lvalueRange.upperBound),
formatter.tokens[equalsIndex] == .operator("=", .infix)
else { return }
guard conditionalBranches.allSatisfy({ formatter.isExhaustiveSingleStatementAssignment($0, lvalueRange: lvalueRange) }),
formatter.conditionalBranchesAreExhaustive(conditionKeywordIndex: startOfConditional, branches: conditionalBranches)
else {
return
}
// If this expression follows a property like `let identifier: Type`, we just
// have to insert an `=` between property and the conditional.
// - Find the introducer (let/var), parse the property, and verify that the identifier
// matches the identifier assigned on each conditional branch.
if let introducerIndex = formatter.indexOfLastSignificantKeyword(at: startOfConditional, excluding: ["if", "switch"]),
["let", "var"].contains(formatter.tokens[introducerIndex].string),
let property = formatter.parsePropertyDeclaration(atIntroducerIndex: introducerIndex),
formatter.tokens[lvalueRange.lowerBound].string == property.identifier,
property.value == nil,
let typeRange = property.type?.range,
let nextTokenAfterProperty = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: typeRange.upperBound),
nextTokenAfterProperty == startOfConditional
{
formatter.removeAssignmentFromAllBranches(of: conditionalBranches)
let rangeBetweenTypeAndConditional = (typeRange.upperBound + 1) ..< startOfConditional
// If there are no comments between the type and conditional,
// we reformat it from:
//
// let foo: Foo\n
// if condition {
//
// to:
//
// let foo: Foo = if condition {
//
if formatter.tokens[rangeBetweenTypeAndConditional].allSatisfy(\.isSpaceOrLinebreak) {
formatter.replaceTokens(in: rangeBetweenTypeAndConditional, with: [
.space(" "),
.operator("=", .infix),
.space(" "),
])
}
// But if there are comments, then we shouldn't just delete them.
// Instead we just insert `= ` after the type.
else {
formatter.insert([.operator("=", .infix), .space(" ")], at: startOfConditional)
}
}
// Otherwise we insert an `identifier =` before the if/switch expression
else if !formatter.options.conditionalAssignmentOnlyAfterNewProperties {
// In this case we should only apply the conversion if this is a top-level condition,
// and not nested in some parent condition. In large complex if/switch conditions
// with multiple layers of nesting, for example, this prevents us from making any
// changes unless the entire set of nested conditions can be converted as a unit.
// - First attempt to find and parse a parent if / switch condition.
var startOfParentScope = formatter.startOfScope(at: startOfConditional)
// If we're inside a switch case, expand to look at the whole switch statement
while let currentStartOfParentScope = startOfParentScope,
formatter.tokens[currentStartOfParentScope] == .startOfScope(":"),
let caseToken = formatter.index(of: .endOfScope("case"), before: currentStartOfParentScope)
{
startOfParentScope = formatter.startOfScope(at: caseToken)
}
if let startOfParentScope = startOfParentScope,
let mostRecentIfOrSwitch = formatter.index(of: .keyword, before: startOfParentScope, if: { ["if", "switch"].contains($0.string) }),
let conditionalBranches = formatter.conditionalBranches(at: mostRecentIfOrSwitch),
let startOfFirstParentBranch = conditionalBranches.first?.startOfBranch,
let endOfLastParentBranch = conditionalBranches.last?.endOfBranch,
// If this condition is contained within a parent condition, do nothing.
// We should only convert the entire set of nested conditions together as a unit.
(startOfFirstParentBranch ... endOfLastParentBranch).contains(startOfConditional)
{ return }
let lvalueTokens = formatter.tokens[lvalueRange]
// Now we can remove the `identifier =` from each branch,
// and instead add it before the if / switch expression.
formatter.removeAssignmentFromAllBranches(of: conditionalBranches)
let identifierEqualsTokens = lvalueTokens + [
.space(" "),
.operator("=", .infix),
.space(" "),
]
formatter.insert(identifierEqualsTokens, at: startOfConditional)
}
}
} examples: {
"""
```diff
- let foo: String
- if condition {
+ let foo = if condition {
- foo = "foo"
+ "foo"
} else {
- foo = "bar"
+ "bar"
}
- let foo: String
- switch condition {
+ let foo = switch condition {
case true:
- foo = "foo"
+ "foo"
case false:
- foo = "bar"
+ "bar"
}
// With --condassignment always (disabled by default)
- switch condition {
+ foo.bar = switch condition {
case true:
- foo.bar = "baaz"
+ "baaz"
case false:
- foo.bar = "quux"
+ "quux"
}
```
"""
}
}
extension Formatter {
// Whether or not the conditional statement that starts at the given index
// has branches that are exhaustive
func conditionalBranchesAreExhaustive(
conditionKeywordIndex: Int,
branches: [Formatter.ConditionalBranch]
) -> Bool {
// Switch statements are compiler-guaranteed to be exhaustive
if tokens[conditionKeywordIndex] == .keyword("switch") {
return true
}
// If statements are only exhaustive if the last branch
// is `else` (not `else if`).
else if tokens[conditionKeywordIndex] == .keyword("if"),
let lastCondition = branches.last,
let tokenBeforeLastCondition = index(of: .nonSpaceOrCommentOrLinebreak, before: lastCondition.startOfBranch)
{
return tokens[tokenBeforeLastCondition] == .keyword("else")
}
return false
}
// Whether or not the given conditional branch body qualifies as a single statement
// that assigns a value to `identifier`. This is either:
// 1. a single assignment to `lvalue =`
// 2. a single `if` or `switch` statement where each of the branches also qualify,
// and the statement is exhaustive.
func isExhaustiveSingleStatementAssignment(_ branch: Formatter.ConditionalBranch, lvalueRange: ClosedRange<Int>) -> Bool {
guard let firstTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: branch.startOfBranch) else { return false }
// If this is an if/switch statement, verify that all of the branches are also
// single-statement assignments and that the statement is exhaustive.
if let conditionalBranches = conditionalBranches(at: firstTokenIndex),
let lastConditionalStatement = conditionalBranches.last
{
let allBranchesAreExhaustiveSingleStatement = conditionalBranches.allSatisfy { branch in
isExhaustiveSingleStatementAssignment(branch, lvalueRange: lvalueRange)
}
let isOnlyStatementInScope = next(.nonSpaceOrCommentOrLinebreak, after: lastConditionalStatement.endOfBranch)?.isEndOfScope == true
let isExhaustive = conditionalBranchesAreExhaustive(
conditionKeywordIndex: firstTokenIndex,
branches: conditionalBranches
)
return allBranchesAreExhaustiveSingleStatement
&& isOnlyStatementInScope
&& isExhaustive
}
// Otherwise we expect this to be of the pattern `lvalue = (statement)`
else if let firstExpressionRange = parseExpressionRange(startingAt: firstTokenIndex),
tokens[firstExpressionRange] == tokens[lvalueRange],
let equalsIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: firstExpressionRange.upperBound),
tokens[equalsIndex] == .operator("=", .infix),
let valueStartIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: equalsIndex)
{
// We know this branch starts with `identifier =`, but have to check that the
// remaining code in the branch is a single statement. To do that we can
// create a temporary formatter with the branch body _excluding_ `identifier =`.
let assignmentStatementRange = valueStartIndex ..< branch.endOfBranch
var tempScopeTokens = [Token]()
tempScopeTokens.append(.startOfScope("{"))
tempScopeTokens.append(contentsOf: tokens[assignmentStatementRange])
tempScopeTokens.append(.endOfScope("}"))
let tempFormatter = Formatter(tempScopeTokens, options: options)
guard tempFormatter.blockBodyHasSingleStatement(
atStartOfScope: 0,
includingConditionalStatements: true,
includingReturnStatements: false
) else {
return false
}
// In Swift 5.9, there's a bug that prevents you from writing an
// if or switch expression using an `as?` on one of the branches:
// https://github.com/apple/swift/issues/68764
//
// let result = if condition {
// foo as? String
// } else {
// "bar"
// }
//
if tempFormatter.conditionalBranchHasUnsupportedCastOperator(startOfScopeIndex: 0) {
return false
}
return true
}
return false
}
// Removes the `identifier =` from each conditional branch
func removeAssignmentFromAllBranches(of conditionalBranches: [ConditionalBranch]) {
forEachRecursiveConditionalBranch(in: conditionalBranches) { branch in
guard let firstTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: branch.startOfBranch),
let firstExpressionRange = parseExpressionRange(startingAt: firstTokenIndex),
let equalsIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: firstExpressionRange.upperBound),
tokens[equalsIndex] == .operator("=", .infix),
let valueStartIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: equalsIndex)
else { return }
removeTokens(in: firstTokenIndex ..< valueStartIndex)
}
}
}