@@ -43,10 +43,8 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
43
43
continue
44
44
}
45
45
46
- if isFallthroughOnly ( switchCase) , let label = switchCase. label. as ( SwitchCaseLabelSyntax . self) {
47
- // If the case is fallthrough-only, store it as a violation that we will merge later.
48
- diagnose (
49
- . collapseCase( name: label. caseItems. withoutTrivia ( ) . description) , on: switchCase)
46
+ if isMergeableFallthroughOnly ( switchCase) {
47
+ // Keep track of `fallthrough`-only cases so we can merge and diagnose them later.
50
48
fallthroughOnlyCases. append ( switchCase)
51
49
} else {
52
50
guard !fallthroughOnlyCases. isEmpty else {
@@ -56,33 +54,27 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
56
54
continue
57
55
}
58
56
59
- // If the case is not a `case ...`, then it must be a `default`. Under *most* circumstances,
60
- // we could simply remove the immediately preceding `fallthrough`-only cases because they
61
- // would end up falling through to the `default`, which would match them anyway. However,
62
- // if any of the patterns in those cases have side effects, removing those cases would
63
- // change the program's behavior. Nobody should ever write code like this, but we don't want
64
- // to risk changing behavior just by reformatting.
65
- guard switchCase. label. is ( SwitchCaseLabelSyntax . self) else {
66
- flushViolations ( )
57
+ if canMergeWithPreviousCases ( switchCase) {
58
+ // If the current case can be merged with the ones before it, merge them all, leaving no
59
+ // `fallthrough`-only cases behind.
60
+ newChildren. append ( visit ( mergedCases ( fallthroughOnlyCases + [ switchCase] ) ) )
61
+ } else {
62
+ // If the current case can't be merged with the ones before it, merge the previous ones
63
+ // into a single `fallthrough`-only case and then append the current one. This could
64
+ // happen in one of two situations:
65
+ //
66
+ // 1. The current case has a value binding pattern.
67
+ // 2. The current case is the `default`. Under most circumstances, we could simply remove
68
+ // the immediately preceding `fallthrough`-only cases because they would end up
69
+ // falling through to the `default` which would match them anyway. However, if any of
70
+ // the patterns in those cases have side effects, removing those cases would change
71
+ // the program's behavior.
72
+ // 3. The current case is `@unknown default`, which can't be merged notwithstanding the
73
+ // side-effect issues discussed above.
74
+ newChildren. append ( visit ( mergedCases ( fallthroughOnlyCases) ) )
67
75
newChildren. append ( visit ( switchCase) )
68
- continue
69
- }
70
-
71
- // We have a case that's not fallthrough-only, and a list of fallthrough-only cases before
72
- // it. Merge them and add the result to the new list.
73
- var newCase = mergedCase ( violations: fallthroughOnlyCases, validCase: switchCase)
74
-
75
- // Only the first violation case can have displaced trivia, because any non-whitespace
76
- // trivia in the other violation cases would've prevented collapsing.
77
- if let displacedLeadingTrivia =
78
- fallthroughOnlyCases. first? . leadingTrivia? . withoutLastLine ( )
79
- {
80
- let existingLeadingTrivia = newCase. leadingTrivia ?? [ ]
81
- let mergedLeadingTrivia = displacedLeadingTrivia + existingLeadingTrivia
82
- newCase = newCase. withLeadingTrivia ( mergedLeadingTrivia)
83
76
}
84
77
85
- newChildren. append ( visit ( newCase) )
86
78
fallthroughOnlyCases. removeAll ( )
87
79
}
88
80
}
@@ -94,17 +86,48 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
94
86
return Syntax ( SwitchCaseListSyntax ( newChildren) )
95
87
}
96
88
97
- /// Returns whether the given `SwitchCaseSyntax` contains only a fallthrough statement.
89
+ /// Returns true if this case can definitely be merged with any that come before it.
90
+ private func canMergeWithPreviousCases( _ node: SwitchCaseSyntax ) -> Bool {
91
+ return node. label. is ( SwitchCaseLabelSyntax . self) && !containsValueBindingPattern( node. label)
92
+ }
93
+
94
+ /// Returns true if this node or one of its descendents is a `ValueBindingPatternSyntax`.
95
+ private func containsValueBindingPattern( _ node: Syntax ) -> Bool {
96
+ if node. is ( ValueBindingPatternSyntax . self) {
97
+ return true
98
+ }
99
+ for child in node. children ( viewMode: . sourceAccurate) {
100
+ if containsValueBindingPattern ( child) {
101
+ return true
102
+ }
103
+ }
104
+ return false
105
+ }
106
+
107
+ /// Returns whether the given `SwitchCaseSyntax` contains only a fallthrough statement and is
108
+ /// able to be merged with other cases.
98
109
///
99
110
/// - Parameter switchCase: A syntax node describing a case in a switch statement.
100
- private func isFallthroughOnly( _ switchCase: SwitchCaseSyntax ) -> Bool {
111
+ private func isMergeableFallthroughOnly( _ switchCase: SwitchCaseSyntax ) -> Bool {
112
+ // Ignore anything that isn't a `SwitchCaseLabelSyntax`, like a `default`.
113
+ guard switchCase. label. is ( SwitchCaseLabelSyntax . self) else {
114
+ return false
115
+ }
116
+
101
117
// When there are any additional or non-fallthrough statements, it isn't only a fallthrough.
102
118
guard let onlyStatement = switchCase. statements. firstAndOnly,
103
119
onlyStatement. item. is ( FallthroughStmtSyntax . self)
104
120
else {
105
121
return false
106
122
}
107
123
124
+ // We cannot merge cases that contain a value pattern binding, even if the body is `fallthrough`
125
+ // only. For example, `case .foo(let x)` cannot be combined with other cases unless they all
126
+ // bind the same variables and types.
127
+ if containsValueBindingPattern ( switchCase. label) {
128
+ return false
129
+ }
130
+
108
131
// Check for any comments that are adjacent to the case or fallthrough statement.
109
132
if let leadingTrivia = switchCase. leadingTrivia,
110
133
leadingTrivia. drop ( while: { !$0. isNewline } ) . contains ( where: { $0. isComment } )
@@ -127,31 +150,46 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
127
150
return true
128
151
}
129
152
130
- /// Returns a copy of the given valid case (and its statements) but with the case items from the
131
- /// violations merged with its own case items.
132
- private func mergedCase( violations: [ SwitchCaseSyntax ] , validCase: SwitchCaseSyntax )
133
- -> SwitchCaseSyntax
134
- {
135
- var newCaseItems : [ CaseItemSyntax ] = [ ]
153
+ /// Returns a merged case whose body is derived from the last case in the array, and the labels
154
+ /// of all the cases are merged into a single comma-delimited list.
155
+ private func mergedCases( _ cases: [ SwitchCaseSyntax ] ) -> SwitchCaseSyntax {
156
+ precondition ( !cases. isEmpty, " Must have at least one case to merge " )
136
157
137
- for label in violations. lazy. compactMap ( { $0. label. as ( SwitchCaseLabelSyntax . self) } ) {
138
- let caseItems = Array ( label. caseItems)
158
+ // If there's only one case, just return it.
159
+ if cases. count == 1 {
160
+ return cases. first!
161
+ }
139
162
163
+ var newCaseItems : [ CaseItemSyntax ] = [ ]
164
+ let labels = cases. lazy. compactMap ( { $0. label. as ( SwitchCaseLabelSyntax . self) } )
165
+ for label in labels. dropLast ( ) {
140
166
// We can blindly append all but the last case item because they must already have a trailing
141
167
// comma. Then, we need to add a trailing comma to the last one, since it will be followed by
142
168
// more items.
143
- newCaseItems. append ( contentsOf: caseItems. dropLast ( ) )
169
+ newCaseItems. append ( contentsOf: label . caseItems. dropLast ( ) )
144
170
newCaseItems. append (
145
- caseItems. last!. withTrailingComma (
171
+ label . caseItems. last!. withTrailingComma (
146
172
TokenSyntax . commaToken ( trailingTrivia: . spaces( 1 ) ) ) )
147
- }
148
173
149
- let validCaseLabel = validCase. label. as ( SwitchCaseLabelSyntax . self) !
150
- newCaseItems. append ( contentsOf: validCaseLabel. caseItems)
151
-
152
- let label = validCaseLabel. withCaseItems (
153
- CaseItemListSyntax ( newCaseItems) )
154
- return validCase. withLabel ( Syntax ( label) )
174
+ // Diagnose the cases being collapsed. We do this for all but the last one in the array; the
175
+ // last one isn't diagnosed because it will contain the body that applies to all the previous
176
+ // cases.
177
+ diagnose ( . collapseCase( name: label. caseItems. withoutTrivia ( ) . description) , on: label)
178
+ }
179
+ newCaseItems. append ( contentsOf: labels. last!. caseItems)
180
+
181
+ let newCase = cases. last!. withLabel (
182
+ Syntax ( labels. last!. withCaseItems ( CaseItemListSyntax ( newCaseItems) ) ) )
183
+
184
+ // Only the first violation case can have displaced trivia, because any non-whitespace
185
+ // trivia in the other violation cases would've prevented collapsing.
186
+ if let displacedLeadingTrivia = cases. first!. leadingTrivia? . withoutLastLine ( ) {
187
+ let existingLeadingTrivia = newCase. leadingTrivia ?? [ ]
188
+ let mergedLeadingTrivia = displacedLeadingTrivia + existingLeadingTrivia
189
+ return newCase. withLeadingTrivia ( mergedLeadingTrivia)
190
+ } else {
191
+ return newCase
192
+ }
155
193
}
156
194
}
157
195
0 commit comments