Skip to content

Commit d49f14e

Browse files
LaurenWhiteallevato
authored andcommitted
Implement no cases with only fallthrough (swiftlang#68)
1 parent f449cbf commit d49f14e

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed

Sources/Rules/NoCasesWithOnlyFallthrough.swift

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,136 @@ import SwiftSyntax
1111
///
1212
/// - SeeAlso: https://google.github.io/swift#fallthrough-in-switch-statements
1313
public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
14+
15+
public override func visit(_ node: SwitchStmtSyntax) -> StmtSyntax {
16+
var newCases: [SwitchCaseSyntax] = []
17+
var violations: [SwitchCaseLabelSyntax] = []
18+
19+
for switchCase in node.cases {
20+
guard let switchCase = switchCase as? SwitchCaseSyntax else { continue }
21+
guard let label = switchCase.label as? SwitchCaseLabelSyntax else {
22+
newCases.append(switchCase)
23+
continue
24+
}
25+
26+
if switchCase.statements.count == 1,
27+
let only = switchCase.statements.first,
28+
only.item is FallthroughStmtSyntax {
29+
diagnose(.collapseCase(name: "\(label)"), on: switchCase)
30+
violations.append(label)
31+
} else {
32+
guard violations.count > 0 else {
33+
newCases.append(switchCase)
34+
continue
35+
}
1436

37+
if retrieveNumericCaseValue(caseLabel: label) != nil {
38+
let newCase = collapseIntegerCases(violations: violations,
39+
validCaseLabel: label,
40+
validCase: switchCase)
41+
newCases.append(newCase)
42+
} else {
43+
let newCase = collapseNonIntegerCases(violations: violations,
44+
validCaseLabel: label,
45+
validCase: switchCase)
46+
newCases.append(newCase)
47+
}
48+
violations = []
49+
}
50+
}
51+
return node.withCases(SyntaxFactory.makeSwitchCaseList(newCases))
52+
}
53+
54+
// Puts all given cases on one line with range operator or commas
55+
func collapseIntegerCases(violations: [SwitchCaseLabelSyntax],
56+
validCaseLabel: SwitchCaseLabelSyntax, validCase: SwitchCaseSyntax) -> SwitchCaseSyntax {
57+
var isConsecutive = true
58+
var index = 0
59+
var caseNums: [Int] = []
60+
61+
for item in violations {
62+
guard let caseNum = retrieveNumericCaseValue(caseLabel: item) else { continue }
63+
caseNums.append(caseNum)
64+
}
65+
66+
guard let validCaseNum = retrieveNumericCaseValue(caseLabel: validCaseLabel) else {
67+
return validCase
68+
}
69+
caseNums.append(validCaseNum)
70+
71+
while index <= caseNums.count - 2, isConsecutive {
72+
isConsecutive = caseNums[index] + 1 == caseNums[index + 1]
73+
index += 1
74+
}
75+
76+
var newCaseItems: [CaseItemSyntax] = []
77+
let first = caseNums[0]
78+
let last = caseNums[caseNums.count - 1]
79+
if isConsecutive {
80+
// Create a case with a sequence expression based on the new range
81+
let start = SyntaxFactory.makeIntegerLiteralExpr(
82+
digits: SyntaxFactory.makeIntegerLiteral("\(first)"))
83+
let end = SyntaxFactory.makeIntegerLiteralExpr(
84+
digits: SyntaxFactory.makeIntegerLiteral("\(last)"))
85+
let newExpList = SyntaxFactory.makeExprList(
86+
[start,
87+
SyntaxFactory.makeBinaryOperatorExpr(operatorToken:
88+
SyntaxFactory.makeUnspacedBinaryOperator("...")),
89+
end])
90+
let newExpPat = SyntaxFactory.makeExpressionPattern(
91+
expression: SyntaxFactory.makeSequenceExpr(elements: newExpList))
92+
newCaseItems.append(
93+
SyntaxFactory.makeCaseItem(pattern: newExpPat, whereClause: nil, trailingComma: nil))
94+
} else {
95+
// Add each case item separated by a comma
96+
for num in caseNums {
97+
let newExpPat = SyntaxFactory.makeExpressionPattern(
98+
expression: SyntaxFactory.makeIntegerLiteralExpr(
99+
digits: SyntaxFactory.makeIntegerLiteral("\(num)")))
100+
let trailingComma = SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1))
101+
let newCaseItem = SyntaxFactory.makeCaseItem(
102+
pattern: newExpPat,
103+
whereClause: nil,
104+
trailingComma: num == last ? nil : trailingComma
105+
)
106+
newCaseItems.append(newCaseItem)
107+
}
108+
}
109+
let caseItemList = SyntaxFactory.makeCaseItemList(newCaseItems)
110+
return validCase.withLabel(validCaseLabel.withCaseItems(caseItemList))
111+
}
112+
113+
// Gets integer value from case label, if possible
114+
func retrieveNumericCaseValue(caseLabel: SwitchCaseLabelSyntax) -> Int? {
115+
if let firstTok = caseLabel.caseItems.firstToken,
116+
let num = Int(firstTok.text) {
117+
return num
118+
}
119+
return nil
120+
}
121+
122+
// Puts all given cases on one line separated by commas
123+
func collapseNonIntegerCases(violations: [SwitchCaseLabelSyntax],
124+
validCaseLabel: SwitchCaseLabelSyntax, validCase: SwitchCaseSyntax) -> SwitchCaseSyntax {
125+
var newCaseItems: [CaseItemSyntax] = []
126+
for violation in violations {
127+
for item in violation.caseItems {
128+
let newCaseItem = item.withTrailingComma(
129+
SyntaxFactory.makeCommaToken(trailingTrivia: .spaces(1)))
130+
newCaseItems.append(newCaseItem)
131+
}
132+
}
133+
for item in validCaseLabel.caseItems {
134+
newCaseItems.append(item)
135+
}
136+
let caseItemList = SyntaxFactory.makeCaseItemList(newCaseItems)
137+
return validCase.withLabel(validCaseLabel.withCaseItems(caseItemList))
138+
}
139+
}
140+
141+
extension Diagnostic.Message {
142+
static func collapseCase(name: String) -> Diagnostic.Message {
143+
return .init(.warning,
144+
"\(name) only contains 'fallthrough' and can be combined with a following case")
145+
}
15146
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Foundation
2+
import XCTest
3+
import SwiftSyntax
4+
5+
@testable import Rules
6+
7+
public class NoCasesWithOnlyFallthroughTests: DiagnosingTestCase {
8+
func testFallthroughCases() {
9+
XCTAssertFormatting(NoCasesWithOnlyFallthrough.self,
10+
input: """
11+
switch numbers {
12+
case 1: print("one")
13+
case 2: fallthrough
14+
case 3: fallthrough
15+
case 4: print("two to four")
16+
case 5: fallthrough
17+
case 7: print("five or seven")
18+
default: break
19+
}
20+
switch letters {
21+
case "a": fallthrough
22+
case "b", "c": fallthrough
23+
case "d": print("abcd")
24+
case "e": print("e")
25+
case "f": fallthrough
26+
case "z": print("fz")
27+
default: break
28+
}
29+
switch tokens {
30+
case .comma: print(",")
31+
case .rightBrace: fallthrough
32+
case .leftBrace: fallthrough
33+
case .braces: print("{}")
34+
case .period: print(".")
35+
case .empty: fallthrough
36+
default: break
37+
}
38+
""",
39+
expected: """
40+
switch numbers {
41+
case 1: print("one")
42+
case 2...4: print("two to four")
43+
case 5, 7: print("five or seven")
44+
default: break
45+
}
46+
switch letters {
47+
case "a", "b", "c", "d": print("abcd")
48+
case "e": print("e")
49+
case "f", "z": print("fz")
50+
default: break
51+
}
52+
switch tokens {
53+
case .comma: print(",")
54+
case .rightBrace, .leftBrace, .braces: print("{}")
55+
case .period: print(".")
56+
default: break
57+
}
58+
""")
59+
}
60+
61+
#if !os(macOS)
62+
static let allTests = [
63+
NoCasesWithOnlyFallthroughTests.testFallthroughCases,
64+
]
65+
#endif
66+
}

0 commit comments

Comments
 (0)