Add --blank-line-after-switch-case always option to blankLineAfterSwitchCase rule (#2150)

This commit is contained in:
Louis Prud’homme
2025-07-18 15:50:41 +02:00
committed by Cal Stephens
parent 142386035d
commit 0e6282849c
5 changed files with 169 additions and 6 deletions
+59 -1
View File
@@ -277,12 +277,18 @@ Insert blank line after import statements.
## blankLineAfterSwitchCase
Insert a blank line after multiline switch cases (excluding the last case,
Insert a blank line after switch cases (excluding the last case,
which is followed by a closing brace).
Option | Description
--- | ---
`--blank-line-after-switch-case` | Insert line After switch cases: "always" or "multiline-only" (default)
<details>
<summary>Examples</summary>
`--blank-line-after-switch-case multiline-only` (default)
```diff
func handle(_ action: SpaceshipAction) {
switch action {
@@ -304,6 +310,58 @@ which is followed by a closing brace).
}
```
```diff
func handle(_ action: SpaceshipAction) {
switch action {
case .engageWarpDrive:
warpDrive.activate()
case let .scanPlanet(planet):
scanner.scanForArticialLife()
case .handleIncomingEnergyBlast:
energyShields.engage()
}
}
```
`--blank-line-after-switch-case always`
```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()
}
}
```
```diff
func handle(_ action: SpaceshipAction) {
switch action {
case .engageWarpDrive:
warpDrive.activate()
+
case let .scanPlanet(planet):
scanner.scanForArticialLife()
+
case .handleIncomingEnergyBlast:
energyShields.engage()
}
}
```
</details>
<br/>
+6
View File
@@ -1347,6 +1347,12 @@ struct _Descriptors {
trueValues: ["true"],
falseValues: ["false"]
)
let blankLineAfterSwitchCase = OptionDescriptor(
argumentName: "blank-line-after-switch-case",
displayName: "Blank Line After Switch Cases",
help: "Insert line After switch cases: \"always\" or \"multiline-only\" (default)",
keyPath: \.blankLineAfterSwitchCase
)
// MARK: - Internal
+10 -1
View File
@@ -647,6 +647,13 @@ public enum EquatableMacro: Equatable, RawRepresentable, CustomStringConvertible
}
}
public enum BlankLineAfterSwitchCase: String, CaseIterable {
/// Add blank lines after multiline switch cases only
case multilineOnly = "multiline-only"
/// Always add blank lines after switch cases
case always
}
public enum URLMacro: Equatable, RawRepresentable, CustomStringConvertible {
/// No URL macro
case none
@@ -795,6 +802,7 @@ public struct FormatOptions: CustomStringConvertible {
public var urlMacro: URLMacro
public var preferFileMacro: Bool
public var lineBetweenConsecutiveGuards: Bool
public var blankLineAfterSwitchCase: BlankLineAfterSwitchCase
/// Deprecated
public var indentComments: Bool
@@ -928,6 +936,7 @@ public struct FormatOptions: CustomStringConvertible {
urlMacro: URLMacro = .none,
preferFileMacro: Bool = true,
lineBetweenConsecutiveGuards: Bool = false,
blankLineAfterSwitchCase: BlankLineAfterSwitchCase = .multilineOnly,
// Doesn't really belong here, but hard to put elsewhere
fragment: Bool = false,
ignoreConflictMarkers: Bool = false,
@@ -1050,7 +1059,7 @@ public struct FormatOptions: CustomStringConvertible {
self.urlMacro = urlMacro
self.preferFileMacro = preferFileMacro
self.lineBetweenConsecutiveGuards = lineBetweenConsecutiveGuards
// Doesn't really belong here, but hard to put elsewhere
self.blankLineAfterSwitchCase = blankLineAfterSwitchCase
self.indentComments = indentComments
self.fragment = fragment
self.ignoreConflictMarkers = ignoreConflictMarkers
+61 -4
View File
@@ -11,19 +11,22 @@ import Foundation
public extension FormatRule {
static let blankLineAfterSwitchCase = FormatRule(
help: """
Insert a blank line after multiline switch cases (excluding the last case,
Insert a blank line after switch cases (excluding the last case,
which is followed by a closing brace).
""",
disabledByDefault: true,
orderAfter: [.redundantBreak]
orderAfter: [.redundantBreak],
options: ["blank-line-after-switch-case"]
) { formatter in
formatter.forEach(.keyword("switch")) { switchIndex, _ in
guard let switchCases = formatter.switchStatementBranchesWithSpacingInfo(at: switchIndex) else { return }
let shouldAlwaysInsertBlankLineAfterSwitchCase = formatter.options.blankLineAfterSwitchCase == .always
for switchCase in switchCases.reversed() {
// Any switch statement that spans multiple lines should be followed by a blank line
// Any switch statement should be followed by a blank line, depending on the
// `blankLineAfterSwitchCase` option.
// (excluding the last case, which is followed by a closing brace).
if switchCase.spansMultipleLines,
if shouldAlwaysInsertBlankLineAfterSwitchCase || switchCase.spansMultipleLines,
!switchCase.isLastCase,
!switchCase.isFollowedByBlankLine
{
@@ -41,6 +44,8 @@ public extension FormatRule {
}
} examples: {
#"""
`--blank-line-after-switch-case multiline-only` (default)
```diff
func handle(_ action: SpaceshipAction) {
switch action {
@@ -61,6 +66,58 @@ public extension FormatRule {
}
}
```
```diff
func handle(_ action: SpaceshipAction) {
switch action {
case .engageWarpDrive:
warpDrive.activate()
case let .scanPlanet(planet):
scanner.scanForArticialLife()
case .handleIncomingEnergyBlast:
energyShields.engage()
}
}
```
`--blank-line-after-switch-case always`
```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()
}
}
```
```diff
func handle(_ action: SpaceshipAction) {
switch action {
case .engageWarpDrive:
warpDrive.activate()
+
case let .scanPlanet(planet):
scanner.scanForArticialLife()
+
case .handleIncomingEnergyBlast:
energyShields.engage()
}
}
```
"""#
}
}
@@ -46,6 +46,39 @@ class BlankLineAfterSwitchCaseTests: XCTestCase {
testFormatting(for: input, output, rule: .blankLineAfterSwitchCase)
}
func testAddsBlankLineAfterSingleSwitchCasesWhenBlankLineAroundSingleLineCases() {
let input = """
func handle(_ action: SpaceshipAction) {
switch action {
// The warp drive can be engaged by pressing a button on the control panel
case .engageWarpDrive:
warpDrive.activate()
// Triggered automatically whenever we detect an energy blast was fired in our direction
case .handleIncomingEnergyBlast:
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:
warpDrive.activate()
// Triggered automatically whenever we detect an energy blast was fired in our direction
case .handleIncomingEnergyBlast:
energyShields.engage()
}
}
"""
testFormatting(for: input,
output,
rule: .blankLineAfterSwitchCase,
options: FormatOptions(blankLineAfterSwitchCase: .always))
}
func testRemovesBlankLineAfterLastSwitchCase() {
let input = """
func handle(_ action: SpaceshipAction) {