diff --git a/Rules.md b/Rules.md
index 161ef3ffa..fd495dffc 100644
--- a/Rules.md
+++ b/Rules.md
@@ -91,8 +91,10 @@
# Opt-in Rules (disabled by default)
* [acronyms](#acronyms)
+* [blankLineAfterMultilineSwitchCase](#blankLineAfterMultilineSwitchCase)
* [blankLinesBetweenImports](#blankLinesBetweenImports)
* [blockComments](#blockComments)
+* [consistentSwitchStatementSpacing](#consistentSwitchStatementSpacing)
* [docComments](#docComments)
* [isEmpty](#isEmpty)
* [markTypes](#markTypes)
@@ -238,6 +240,38 @@ Insert blank line after import statements.
+## blankLineAfterMultilineSwitchCase
+
+Insert a blank line after multiline switch cases (excluding the last case,
+which is followed by a closing brace).
+
+
+Examples
+
+```diff
+ func handle(_ action: SpaceshipAction) {
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
++
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArticialLife()
++
+ case .handleIncomingEnergyBlast:
+ await energyShields.prepare()
+ energyShields.engage()
+ }
+ }
+```
+
+
+
+
## blankLinesAroundMark
Insert blank line before and after `MARK:` comments.
@@ -543,6 +577,63 @@ Replace consecutive spaces with a single space.
+## consistentSwitchStatementSpacing
+
+Ensures consistent spacing among all of the cases in a switch statement.
+
+
+Examples
+
+```diff
+ func handle(_ action: SpaceshipAction) {
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
++
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArtificialLife()
+
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ }
+```
+
+```diff
+ var name: PlanetType {
+ switch self {
+ case .mercury:
+ "Mercury"
+-
+ case .venus:
+ "Venus"
+ case .earth:
+ "Earth"
+ case .mars:
+ "Mars"
+-
+ case .jupiter:
+ "Jupiter"
+ case .saturn:
+ "Saturn"
+ case .uranus:
+ "Uranus"
+ case .neptune:
+ "Neptune"
+ }
+```
+
+
+
+
## docComments
Use doc comments for API declarations, otherwise use regular comments.
diff --git a/Sources/Examples.swift b/Sources/Examples.swift
index df8ff0bb6..c508ea6a7 100644
--- a/Sources/Examples.swift
+++ b/Sources/Examples.swift
@@ -1754,6 +1754,29 @@ private struct Examples {
```
"""
+ let blankLineAfterMultilineSwitchCase = #"""
+ ```diff
+ func handle(_ action: SpaceshipAction) {
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+ +
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArticialLife()
+ +
+ case .handleIncomingEnergyBlast:
+ await energyShields.prepare()
+ energyShields.engage()
+ }
+ }
+ ```
+ """#
+
let wrapMultilineConditionalAssignment = #"""
```diff
- let planetLocation = if let star = planet.star {
@@ -1769,4 +1792,53 @@ private struct Examples {
+ }
```
"""#
+
+ let consistentSwitchStatementSpacing = #"""
+ ```diff
+ func handle(_ action: SpaceshipAction) {
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+ +
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArtificialLife()
+
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ }
+ ```
+
+ ```diff
+ var name: PlanetType {
+ switch self {
+ case .mercury:
+ "Mercury"
+ -
+ case .venus:
+ "Venus"
+ case .earth:
+ "Earth"
+ case .mars:
+ "Mars"
+ -
+ case .jupiter:
+ "Jupiter"
+ case .saturn:
+ "Saturn"
+ case .uranus:
+ "Uranus"
+ case .neptune:
+ "Neptune"
+ }
+ ```
+ """#
}
diff --git a/Sources/FormattingHelpers.swift b/Sources/FormattingHelpers.swift
index 3fa7d9f3f..62ad2a53b 100644
--- a/Sources/FormattingHelpers.swift
+++ b/Sources/FormattingHelpers.swift
@@ -1347,7 +1347,7 @@ extension Formatter {
{
branches.append((startOfBranch: startOfBody, endOfBranch: endOfBody))
- if tokens[endOfBody].isSwitchCaseOrDefault {
+ if tokens[endOfBody].isSwitchCaseOrDefault || tokens[endOfBody] == .keyword("@unknown") {
nextConditionalBranchIndex = endOfBody
} else if tokens[startOfBody ..< endOfBody].contains(.startOfScope("#if")) {
return nil
@@ -1449,6 +1449,105 @@ extension Formatter {
return allSatisfy
}
+ /// Context describing the structure of a case in a switch statement
+ struct SwitchStatementBranchWithSpacingInfo {
+ let startOfBranchExcludingLeadingComments: Int
+ let endOfBranchExcludingTrailingComments: Int
+ let spansMultipleLines: Bool
+ let isLastCase: Bool
+ let isFollowedByBlankLine: Bool
+ let linebreakBeforeEndOfScope: Int?
+ let linebreakBeforeBlankLine: Int?
+
+ /// Inserts a blank line at the end of the switch case
+ func insertTrailingBlankLine(using formatter: Formatter) {
+ guard let linebreakBeforeEndOfScope = linebreakBeforeEndOfScope else {
+ return
+ }
+
+ formatter.insertLinebreak(at: linebreakBeforeEndOfScope)
+ }
+
+ /// Removes the trailing blank line from the switch case if present
+ func removeTrailingBlankLine(using formatter: Formatter) {
+ guard let linebreakBeforeEndOfScope = linebreakBeforeEndOfScope,
+ let linebreakBeforeBlankLine = linebreakBeforeBlankLine
+ else { return }
+
+ formatter.removeTokens(in: (linebreakBeforeBlankLine + 1) ... linebreakBeforeEndOfScope)
+ }
+ }
+
+ /// Finds all of the branch bodies in a switch statement, and derives additional information
+ /// about the structure of each branch / case.
+ func switchStatementBranchesWithSpacingInfo(at switchIndex: Int) -> [SwitchStatementBranchWithSpacingInfo]? {
+ guard let switchStatementBranches = switchStatementBranches(at: switchIndex) else { return nil }
+
+ return switchStatementBranches.enumerated().compactMap { caseIndex, switchCase -> SwitchStatementBranchWithSpacingInfo? in
+ // Exclude any comments when considering if this is a single line or multi-line branch
+ var startOfBranchExcludingLeadingComments = switchCase.startOfBranch
+ while let tokenAfterStartOfScope = index(of: .nonSpace, after: startOfBranchExcludingLeadingComments),
+ tokens[tokenAfterStartOfScope].isLinebreak,
+ let commentAfterStartOfScope = index(of: .nonSpace, after: tokenAfterStartOfScope),
+ tokens[commentAfterStartOfScope].isComment,
+ let endOfComment = endOfScope(at: commentAfterStartOfScope),
+ let tokenBeforeEndOfComment = index(of: .nonSpace, before: endOfComment)
+ {
+ if tokens[endOfComment].isLinebreak {
+ startOfBranchExcludingLeadingComments = tokenBeforeEndOfComment
+ } else {
+ startOfBranchExcludingLeadingComments = endOfComment
+ }
+ }
+
+ var endOfBranchExcludingTrailingComments = switchCase.endOfBranch
+ while let tokenBeforeEndOfScope = index(of: .nonSpace, before: endOfBranchExcludingTrailingComments),
+ tokens[tokenBeforeEndOfScope].isLinebreak,
+ let commentBeforeEndOfScope = index(of: .nonSpace, before: tokenBeforeEndOfScope),
+ tokens[commentBeforeEndOfScope].isComment,
+ let startOfComment = startOfScope(at: commentBeforeEndOfScope),
+ tokens[startOfComment].isComment
+ {
+ endOfBranchExcludingTrailingComments = startOfComment
+ }
+
+ guard let firstTokenInBody = index(of: .nonSpaceOrLinebreak, after: startOfBranchExcludingLeadingComments),
+ let lastTokenInBody = index(of: .nonSpaceOrLinebreak, before: endOfBranchExcludingTrailingComments)
+ else { return nil }
+
+ let isLastCase = caseIndex == switchStatementBranches.indices.last
+ let spansMultipleLines = !onSameLine(firstTokenInBody, lastTokenInBody)
+
+ var isFollowedByBlankLine = false
+ var linebreakBeforeEndOfScope: Int?
+ var linebreakBeforeBlankLine: Int?
+
+ if let tokenBeforeEndOfScope = index(of: .nonSpace, before: endOfBranchExcludingTrailingComments),
+ tokens[tokenBeforeEndOfScope].isLinebreak
+ {
+ linebreakBeforeEndOfScope = tokenBeforeEndOfScope
+ }
+
+ if let linebreakBeforeEndOfScope = linebreakBeforeEndOfScope,
+ let tokenBeforeBlankLine = index(of: .nonSpace, before: linebreakBeforeEndOfScope),
+ tokens[tokenBeforeBlankLine].isLinebreak
+ {
+ linebreakBeforeBlankLine = tokenBeforeBlankLine
+ isFollowedByBlankLine = true
+ }
+
+ return SwitchStatementBranchWithSpacingInfo(
+ startOfBranchExcludingLeadingComments: startOfBranchExcludingLeadingComments,
+ endOfBranchExcludingTrailingComments: endOfBranchExcludingTrailingComments,
+ spansMultipleLines: spansMultipleLines,
+ isLastCase: isLastCase,
+ isFollowedByBlankLine: isFollowedByBlankLine,
+ linebreakBeforeEndOfScope: linebreakBeforeEndOfScope,
+ linebreakBeforeBlankLine: linebreakBeforeBlankLine
+ )
+ }
+ }
+
/// Whether the given index is in a function call (not declaration)
func isFunctionCall(at index: Int) -> Bool {
if let openingParenIndex = self.index(of: .startOfScope("("), before: index + 1) {
diff --git a/Sources/Rules.swift b/Sources/Rules.swift
index a51088c4f..abd7fc80f 100644
--- a/Sources/Rules.swift
+++ b/Sources/Rules.swift
@@ -7757,4 +7757,73 @@ public struct _FormatRules {
}
}
}
+
+ public let blankLineAfterMultilineSwitchCase = FormatRule(
+ help: """
+ Insert a blank line after multiline switch cases (excluding the last case,
+ which is followed by a closing brace).
+ """,
+ disabledByDefault: true,
+ orderAfter: ["redundantBreak"]
+ ) { formatter in
+ formatter.forEach(.keyword("switch")) { switchIndex, _ in
+ guard let switchCases = formatter.switchStatementBranchesWithSpacingInfo(at: switchIndex) else { return }
+
+ for switchCase in switchCases.reversed() {
+ // Any switch statement that spans multiple lines should be followed by a blank line
+ // (excluding the last case, which is followed by a closing brace).
+ if switchCase.spansMultipleLines,
+ !switchCase.isLastCase,
+ !switchCase.isFollowedByBlankLine
+ {
+ switchCase.insertTrailingBlankLine(using: formatter)
+ }
+
+ // The last case should never be followed by a blank line, since it's
+ // already followed by a closing brace.
+ if switchCase.isLastCase,
+ switchCase.isFollowedByBlankLine
+ {
+ switchCase.removeTrailingBlankLine(using: formatter)
+ }
+ }
+ }
+ }
+
+ public let consistentSwitchStatementSpacing = FormatRule(
+ help: "Ensures consistent spacing among all of the cases in a switch statement.",
+ disabledByDefault: true,
+ orderAfter: ["blankLineAfterMultilineSwitchCase"]
+ ) { formatter in
+ formatter.forEach(.keyword("switch")) { switchIndex, _ in
+ guard let switchCases = formatter.switchStatementBranchesWithSpacingInfo(at: switchIndex) else { return }
+
+ // When counting the switch cases, exclude the last case (which should never have a trailing blank line).
+ let countWithTrailingBlankLine = switchCases.filter { $0.isFollowedByBlankLine && !$0.isLastCase }.count
+ let countWithoutTrailingBlankLine = switchCases.filter { !$0.isFollowedByBlankLine && !$0.isLastCase }.count
+
+ // We want the spacing to be consistent for all switch cases,
+ // so use whichever formatting is used for the majority of cases.
+ var allCasesShouldHaveBlankLine = countWithTrailingBlankLine >= countWithoutTrailingBlankLine
+
+ // When the `blankLinesBetweenChainedFunctions` rule is enabled, and there is a switch case
+ // that is required to span multiple lines, then all cases must span multiple lines.
+ // (Since if this rule removed the blank line from that case, it would contradict the other rule)
+ if formatter.options.enabledRules.contains(FormatRules.blankLineAfterMultilineSwitchCase.name),
+ switchCases.contains(where: { $0.spansMultipleLines && !$0.isLastCase })
+ {
+ allCasesShouldHaveBlankLine = true
+ }
+
+ for switchCase in switchCases.reversed() {
+ if !switchCase.isFollowedByBlankLine, allCasesShouldHaveBlankLine, !switchCase.isLastCase {
+ switchCase.insertTrailingBlankLine(using: formatter)
+ }
+
+ if switchCase.isFollowedByBlankLine, !allCasesShouldHaveBlankLine || switchCase.isLastCase {
+ switchCase.removeTrailingBlankLine(using: formatter)
+ }
+ }
+ }
+ }
}
diff --git a/Tests/RulesTests+Indentation.swift b/Tests/RulesTests+Indentation.swift
index f3cf7ac31..f9e2a55a4 100644
--- a/Tests/RulesTests+Indentation.swift
+++ b/Tests/RulesTests+Indentation.swift
@@ -553,18 +553,18 @@ class IndentTests: RulesTests {
func testIndentSwitchAfterRangeCase() {
let input = "switch x {\ncase 0 ..< 2:\n switch y {\n default:\n break\n }\ndefault:\n break\n}"
- testFormatting(for: input, rule: FormatRules.indent)
+ testFormatting(for: input, rule: FormatRules.indent, exclude: ["blankLineAfterMultilineSwitchCase"])
}
func testIndentEnumDeclarationInsideSwitchCase() {
let input = "switch x {\ncase y:\nenum Foo {\ncase z\n}\nbar()\ndefault: break\n}"
let output = "switch x {\ncase y:\n enum Foo {\n case z\n }\n bar()\ndefault: break\n}"
- testFormatting(for: input, output, rule: FormatRules.indent)
+ testFormatting(for: input, output, rule: FormatRules.indent, exclude: ["blankLineAfterMultilineSwitchCase"])
}
func testIndentEnumCaseBodyAfterWhereClause() {
let input = "switch foo {\ncase _ where baz < quux:\n print(1)\n print(2)\ndefault:\n break\n}"
- testFormatting(for: input, rule: FormatRules.indent)
+ testFormatting(for: input, rule: FormatRules.indent, exclude: ["blankLineAfterMultilineSwitchCase"])
}
func testIndentSwitchCaseCommentsCorrectly() {
@@ -590,7 +590,7 @@ class IndentTests: RulesTests {
break
}
"""
- testFormatting(for: input, output, rule: FormatRules.indent)
+ testFormatting(for: input, output, rule: FormatRules.indent, exclude: ["blankLineAfterMultilineSwitchCase"])
}
func testIndentMultilineSwitchCaseCommentsCorrectly() {
@@ -2949,14 +2949,14 @@ class IndentTests: RulesTests {
let input = "switch foo {\ncase .bar:\n#if x\nbar()\n#endif\nbaz()\ncase .baz: break\n}"
let output = "switch foo {\ncase .bar:\n #if x\n bar()\n #endif\n baz()\ncase .baz: break\n}"
let options = FormatOptions(indentCase: false)
- testFormatting(for: input, output, rule: FormatRules.indent, options: options)
+ testFormatting(for: input, output, rule: FormatRules.indent, options: options, exclude: ["blankLineAfterMultilineSwitchCase"])
}
func testSwitchIfEndifInsideCaseIndenting2() {
let input = "switch foo {\ncase .bar:\n#if x\nbar()\n#endif\nbaz()\ncase .baz: break\n}"
let output = "switch foo {\n case .bar:\n #if x\n bar()\n #endif\n baz()\n case .baz: break\n}"
let options = FormatOptions(indentCase: true)
- testFormatting(for: input, output, rule: FormatRules.indent, options: options)
+ testFormatting(for: input, output, rule: FormatRules.indent, options: options, exclude: ["blankLineAfterMultilineSwitchCase"])
}
func testIfUnknownCaseEndifIndenting() {
@@ -3191,14 +3191,14 @@ class IndentTests: RulesTests {
let input = "switch foo {\ncase .bar:\n#if x\nbar()\n#endif\nbaz()\ncase .baz: break\n}"
let output = "switch foo {\ncase .bar:\n #if x\n bar()\n #endif\n baz()\ncase .baz: break\n}"
let options = FormatOptions(indentCase: false, ifdefIndent: .noIndent)
- testFormatting(for: input, output, rule: FormatRules.indent, options: options)
+ testFormatting(for: input, output, rule: FormatRules.indent, options: options, exclude: ["blankLineAfterMultilineSwitchCase"])
}
func testIfEndifInsideCaseNoIndenting2() {
let input = "switch foo {\ncase .bar:\n#if x\nbar()\n#endif\nbaz()\ncase .baz: break\n}"
let output = "switch foo {\n case .bar:\n #if x\n bar()\n #endif\n baz()\n case .baz: break\n}"
let options = FormatOptions(indentCase: true, ifdefIndent: .noIndent)
- testFormatting(for: input, output, rule: FormatRules.indent, options: options)
+ testFormatting(for: input, output, rule: FormatRules.indent, options: options, exclude: ["blankLineAfterMultilineSwitchCase"])
}
func testSwitchCaseInIfEndif() {
diff --git a/Tests/RulesTests+Organization.swift b/Tests/RulesTests+Organization.swift
index 19f4a2b07..a6c4d4a85 100644
--- a/Tests/RulesTests+Organization.swift
+++ b/Tests/RulesTests+Organization.swift
@@ -2790,6 +2790,7 @@ class OrganizationTests: RulesTests {
case .value:
print("value")
}
+
case .failure:
guard self.bar else {
print(self.bar)
diff --git a/Tests/RulesTests+Redundancy.swift b/Tests/RulesTests+Redundancy.swift
index 45a5b06e4..12cd889ba 100644
--- a/Tests/RulesTests+Redundancy.swift
+++ b/Tests/RulesTests+Redundancy.swift
@@ -1337,6 +1337,7 @@ class RedundancyTests: RulesTests {
} else {
Foo("bar")
}
+
case false:
Foo("baaz")
}
@@ -1353,6 +1354,7 @@ class RedundancyTests: RulesTests {
} else {
Foo("bar")
}
+
case false:
Foo("baaz")
}
@@ -1374,6 +1376,7 @@ class RedundancyTests: RulesTests {
} else {
Foo("bar")
}
+
case false:
Foo("baaz")
}
@@ -1390,6 +1393,7 @@ class RedundancyTests: RulesTests {
} else {
.init("bar")
}
+
case false:
.init("baaz")
}
@@ -1851,6 +1855,7 @@ class RedundancyTests: RulesTests {
case .bar:
var foo: String?
Text(foo ?? "")
+
default:
EmptyView()
}
@@ -1951,6 +1956,7 @@ class RedundancyTests: RulesTests {
let _ = {
foo = "\\(max)"
}()
+
default:
EmptyView()
}
@@ -2798,6 +2804,7 @@ class RedundancyTests: RulesTests {
case true:
// foo
return "foo"
+
default:
/* bar */
return "bar"
@@ -2810,6 +2817,7 @@ class RedundancyTests: RulesTests {
case true:
// foo
"foo"
+
default:
/* bar */
"bar"
@@ -2880,6 +2888,7 @@ class RedundancyTests: RulesTests {
return "baaz"
}
}
+
case false:
return "quux"
}
@@ -2899,6 +2908,7 @@ class RedundancyTests: RulesTests {
"baaz"
}
}
+
case false:
"quux"
}
@@ -6204,6 +6214,7 @@ class RedundancyTests: RulesTests {
print(self.bar)
}
}
+
case .failure:
if self.bar {
print(self.bar)
@@ -6231,6 +6242,7 @@ class RedundancyTests: RulesTests {
}
}
self.method()
+
case .failure:
break
}
@@ -6261,6 +6273,7 @@ class RedundancyTests: RulesTests {
case .value:
print("value")
}
+
case .failure:
guard self.bar else {
print(self.bar)
diff --git a/Tests/RulesTests+Spacing.swift b/Tests/RulesTests+Spacing.swift
index 864f6853c..b6e20cf79 100644
--- a/Tests/RulesTests+Spacing.swift
+++ b/Tests/RulesTests+Spacing.swift
@@ -1588,4 +1588,524 @@ class SpacingTests: RulesTests {
let options = FormatOptions(emptyBracesSpacing: .linebreak)
testFormatting(for: input, rule: FormatRules.emptyBraces, options: options)
}
+
+ // MARK: - blankLineAfterMultilineSwitchCase
+
+ func testAddsBlankLineAfterMultilineSwitchCases() {
+ let input = """
+ func handle(_ action: SpaceshipAction) {
+ switch action {
+ // The warp drive can be engaged by pressing a button on the control panel
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+ // Triggered automatically whenever we detect an energy blast was fired in our direction
+ case .handleIncomingEnergyBlast:
+ await energyShields.prepare()
+ energyShields.engage()
+ }
+ }
+ """
+
+ let output = """
+ func handle(_ action: SpaceshipAction) {
+ switch action {
+ // The warp drive can be engaged by pressing a button on the control panel
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ // Triggered automatically whenever we detect an energy blast was fired in our direction
+ case .handleIncomingEnergyBlast:
+ await energyShields.prepare()
+ energyShields.engage()
+ }
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.blankLineAfterMultilineSwitchCase)
+ }
+
+ func testRemovesBlankLineAfterLastSwitchCase() {
+ let input = """
+ func handle(_ action: SpaceshipAction) {
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArticialLife()
+
+ case .handleIncomingEnergyBlast:
+ await energyShields.prepare()
+ energyShields.engage()
+
+ }
+ }
+ """
+
+ let output = """
+ func handle(_ action: SpaceshipAction) {
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArticialLife()
+
+ case .handleIncomingEnergyBlast:
+ await energyShields.prepare()
+ energyShields.engage()
+ }
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.blankLineAfterMultilineSwitchCase)
+ }
+
+ func testDoesntAddBlankLineAfterSingleLineSwitchCase() {
+ let input = """
+ var planetType: PlanetType {
+ switch self {
+ case .mercury, .venus, .earth, .mars:
+ // The terrestrial planets are smaller and have a solid, rocky surface
+ .terrestrial
+ case .jupiter, .saturn, .uranus, .neptune:
+ // The gas giants are huge and lack a solid surface
+ .gasGiant
+ }
+ }
+
+ var planetType: PlanetType {
+ switch self {
+ // The terrestrial planets are smaller and have a solid, rocky surface
+ case .mercury, .venus, .earth, .mars:
+ .terrestrial
+ // The gas giants are huge and lack a solid surface
+ case .jupiter, .saturn, .uranus, .neptune:
+ .gasGiant
+ }
+ }
+
+ var name: PlanetType {
+ switch self {
+ // The planet closest to the sun
+ case .mercury:
+ "Mercury"
+ case .venus:
+ "Venus"
+ // The best planet, where everything cool happens
+ case .earth:
+ "Earth"
+ // This planet is entirely inhabited by robots.
+ // There are cool landers, rovers, and even a helicopter.
+ case .mars:
+ "Mars"
+ case .jupiter:
+ "Jupiter"
+ case .saturn:
+ // Other planets have rings, but satun's are the best.
+ // It's rings are the only once that are usually visible in photos.
+ "Saturn"
+ case .uranus:
+ /*
+ * The pronunciation of this planet's name is subject of scholarly debate
+ */
+ "Uranus"
+ case .neptune:
+ "Neptune"
+ }
+ }
+ """
+
+ testFormatting(for: input, rule: FormatRules.blankLineAfterMultilineSwitchCase, exclude: ["sortSwitchCases", "wrapSwitchCases", "blockComments"])
+ }
+
+ func testMixedSingleLineAndMultiLineCases() {
+ let input = """
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArtificialLife()
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+
+ let output = """
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArtificialLife()
+
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.blankLineAfterMultilineSwitchCase, exclude: ["consistentSwitchStatementSpacing"])
+ }
+
+ func testAllowsBlankLinesAfterSingleLineCases() {
+ let input = """
+ switch action {
+ case .engageWarpDrive:
+ warpDrive.engage()
+
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+
+ case let .scanPlanet(planet):
+ scanner.scan(planet)
+
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+
+ testFormatting(for: input, rule: FormatRules.blankLineAfterMultilineSwitchCase)
+ }
+
+ // MARK: - consistentSwitchStatementSpacing
+
+ func testInsertsBlankLinesToMakeSwitchStatementSpacingConsistent1() {
+ let input = """
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArtificialLife()
+
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+
+ let output = """
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+
+ case let .scanPlanet(planet):
+ scanner.target = planet
+ scanner.scanAtmosphere()
+ scanner.scanBiosphere()
+ scanner.scanForArtificialLife()
+
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.consistentSwitchStatementSpacing)
+ }
+
+ func testInsertsBlankLinesToMakeSwitchStatementSpacingConsistent2() {
+ let input = """
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+
+ let output = """
+ switch action {
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.consistentSwitchStatementSpacing)
+ }
+
+ func testInsertsBlankLinesToMakeSwitchStatementSpacingConsistent3() {
+ let input = """
+ var name: PlanetType {
+ switch self {
+ // The planet closest to the sun
+ case .mercury:
+ "Mercury"
+ // Similar to Earth but way more deadly
+ case .venus:
+ "Venus"
+
+ // The best planet, where everything cool happens
+ case .earth:
+ "Earth"
+
+ // This planet is entirely inhabited by robots.
+ // There are cool landers, rovers, and even a helicopter.
+ case .mars:
+ "Mars"
+
+ // The biggest planet with the most moons
+ case .jupiter:
+ "Jupiter"
+
+ // Other planets have rings, but satun's are the best.
+ case .saturn:
+ "Saturn"
+ case .uranus:
+ "Uranus"
+ case .neptune:
+ "Neptune"
+ }
+ }
+ """
+
+ let output = """
+ var name: PlanetType {
+ switch self {
+ // The planet closest to the sun
+ case .mercury:
+ "Mercury"
+
+ // Similar to Earth but way more deadly
+ case .venus:
+ "Venus"
+
+ // The best planet, where everything cool happens
+ case .earth:
+ "Earth"
+
+ // This planet is entirely inhabited by robots.
+ // There are cool landers, rovers, and even a helicopter.
+ case .mars:
+ "Mars"
+
+ // The biggest planet with the most moons
+ case .jupiter:
+ "Jupiter"
+
+ // Other planets have rings, but satun's are the best.
+ case .saturn:
+ "Saturn"
+
+ case .uranus:
+ "Uranus"
+
+ case .neptune:
+ "Neptune"
+ }
+ }
+ """
+ testFormatting(for: input, output, rule: FormatRules.consistentSwitchStatementSpacing)
+ }
+
+ func testRemovesBlankLinesToMakeSwitchStatementConsistent() {
+ let input = """
+ var name: PlanetType {
+ switch self {
+ // The planet closest to the sun
+ case .mercury:
+ "Mercury"
+
+ case .venus:
+ "Venus"
+ // The best planet, where everything cool happens
+ case .earth:
+ "Earth"
+ // This planet is entirely inhabited by robots.
+ // There are cool landers, rovers, and even a helicopter.
+ case .mars:
+ "Mars"
+ case .jupiter:
+ "Jupiter"
+ // Other planets have rings, but satun's are the best.
+ case .saturn:
+ "Saturn"
+ case .uranus:
+ "Uranus"
+ case .neptune:
+ "Neptune"
+ }
+ }
+ """
+
+ let output = """
+ var name: PlanetType {
+ switch self {
+ // The planet closest to the sun
+ case .mercury:
+ "Mercury"
+ case .venus:
+ "Venus"
+ // The best planet, where everything cool happens
+ case .earth:
+ "Earth"
+ // This planet is entirely inhabited by robots.
+ // There are cool landers, rovers, and even a helicopter.
+ case .mars:
+ "Mars"
+ case .jupiter:
+ "Jupiter"
+ // Other planets have rings, but satun's are the best.
+ case .saturn:
+ "Saturn"
+ case .uranus:
+ "Uranus"
+ case .neptune:
+ "Neptune"
+ }
+ }
+ """
+
+ testFormatting(for: input, output, rule: FormatRules.consistentSwitchStatementSpacing)
+ }
+
+ func testSingleLineAndMultiLineSwitchCase1() {
+ let input = """
+ switch planetType {
+ case .terrestrial:
+ if options.treatPlutoAsPlanet {
+ [.mercury, .venus, .earth, .mars, .pluto]
+ } else {
+ [.mercury, .venus, .earth, .mars]
+ }
+ case .gasGiant:
+ [.jupiter, .saturn, .uranus, .neptune]
+ }
+ """
+
+ let output = """
+ switch planetType {
+ case .terrestrial:
+ if options.treatPlutoAsPlanet {
+ [.mercury, .venus, .earth, .mars, .pluto]
+ } else {
+ [.mercury, .venus, .earth, .mars]
+ }
+
+ case .gasGiant:
+ [.jupiter, .saturn, .uranus, .neptune]
+ }
+ """
+
+ testFormatting(for: input, [output], rules: [FormatRules.blankLineAfterMultilineSwitchCase, FormatRules.consistentSwitchStatementSpacing])
+ }
+
+ func testSingleLineAndMultiLineSwitchCase2() {
+ let input = """
+ switch planetType {
+ case .gasGiant:
+ [.jupiter, .saturn, .uranus, .neptune]
+ case .terrestrial:
+ if options.treatPlutoAsPlanet {
+ [.mercury, .venus, .earth, .mars, .pluto]
+ } else {
+ [.mercury, .venus, .earth, .mars]
+ }
+ }
+ """
+
+ testFormatting(for: input, rule: FormatRules.consistentSwitchStatementSpacing)
+ }
+
+ func testSwitchStatementWithSingleMultilineCase_blankLineAfterMultilineSwitchCaseEnabled() {
+ let input = """
+ switch action {
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+ case let .scanPlanet(planet):
+ scanner.scan(planet)
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+
+ let output = """
+ switch action {
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+
+ case let .scanPlanet(planet):
+ scanner.scan(planet)
+
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+
+ testFormatting(for: input, [output], rules: [FormatRules.consistentSwitchStatementSpacing, FormatRules.blankLineAfterMultilineSwitchCase])
+ }
+
+ func testSwitchStatementWithSingleMultilineCase_blankLineAfterMultilineSwitchCaseDisabled() {
+ let input = """
+ switch action {
+ case .enableArtificialGravity:
+ artificialGravityEngine.enable(strength: .oneG)
+ case .engageWarpDrive:
+ navigationComputer.destination = targetedDestination
+ await warpDrive.spinUp()
+ warpDrive.activate()
+ case let .scanPlanet(planet):
+ scanner.scan(planet)
+ case .handleIncomingEnergyBlast:
+ energyShields.engage()
+ }
+ """
+
+ testFormatting(for: input, rule: FormatRules.consistentSwitchStatementSpacing, exclude: ["blankLineAfterMultilineSwitchCase"])
+ }
}
diff --git a/Tests/RulesTests+Syntax.swift b/Tests/RulesTests+Syntax.swift
index 0de53eff6..eb45dc336 100644
--- a/Tests/RulesTests+Syntax.swift
+++ b/Tests/RulesTests+Syntax.swift
@@ -3403,6 +3403,7 @@ class SyntaxTests: RulesTests {
case .bar:
// bar
let bar = baz()
+
default:
// baz
let baz = quux()
@@ -3537,10 +3538,12 @@ class SyntaxTests: RulesTests {
} else {
foo = Foo("bar")
}
+
case false:
switch condition {
case true:
foo = Foo("baaz")
+
case false:
if condition {
foo = Foo("quux")
@@ -3558,10 +3561,12 @@ class SyntaxTests: RulesTests {
} else {
Foo("bar")
}
+
case false:
switch condition {
case true:
Foo("baaz")
+
case false:
if condition {
Foo("quux")
@@ -3677,6 +3682,7 @@ class SyntaxTests: RulesTests {
case true:
foo = Foo("foo")
print("Multi-statement")
+
case false:
foo = Foo("bar")
}
@@ -3714,6 +3720,7 @@ class SyntaxTests: RulesTests {
foo = Foo("baaz")
}
print("Multi-statement")
+
case false:
foo = Foo("bar")
}