Add new format rule for wrapping case bodies (#2476)

This commit is contained in:
Kim de Vos
2026-03-24 01:02:20 +01:00
committed by Cal Stephens
parent 93004fb971
commit d798a8a5e3
5 changed files with 313 additions and 0 deletions
+17
View File
@@ -136,6 +136,7 @@
* [unusedPrivateDeclarations](#unusedPrivateDeclarations)
* [urlMacro](#urlMacro)
* [validateTestCases](#validateTestCases)
* [wrapCaseBodies](#wrapCaseBodies)
* [wrapConditionalBodies](#wrapConditionalBodies)
* [wrapEnumCases](#wrapEnumCases)
* [wrapMultilineConditionalAssignment](#wrapMultilineConditionalAssignment)
@@ -4338,6 +4339,22 @@ Option | Description
</details>
<br/>
## wrapCaseBodies
Wrap the bodies of inline switch cases onto a new line.
<details>
<summary>Examples</summary>
```diff
- case .foo: return bar
+ case .foo:
+ return bar
```
</details>
<br/>
## wrapConditionalBodies
Wrap the bodies of inline conditional statements onto a new line.
+1
View File
@@ -141,6 +141,7 @@ let ruleRegistry: [String: FormatRule] = [
"wrap": .wrap,
"wrapArguments": .wrapArguments,
"wrapAttributes": .wrapAttributes,
"wrapCaseBodies": .wrapCaseBodies,
"wrapConditionalBodies": .wrapConditionalBodies,
"wrapEnumCases": .wrapEnumCases,
"wrapFunctionBodies": .wrapFunctionBodies,
+53
View File
@@ -0,0 +1,53 @@
//
// WrapCaseBodies.swift
// SwiftFormat
//
// Created by Kim de Vos on 3/23/26.
// Copyright © 2026 Nick Lockwood. All rights reserved.
//
import Foundation
public extension FormatRule {
static let wrapCaseBodies = FormatRule(
help: "Wrap the bodies of inline switch cases onto a new line.",
disabledByDefault: true,
sharedOptions: ["linebreaks", "indent"]
) { formatter in
formatter.forEach(.endOfScope("case")) { i, _ in
formatter.wrapCaseBody(at: i)
}
formatter.forEach(.endOfScope("default")) { i, _ in
formatter.wrapCaseBody(at: i)
}
} examples: {
"""
```diff
- case .foo: return bar
+ case .foo:
+ return bar
```
"""
}
}
extension Formatter {
func wrapCaseBody(at caseIndex: Int) {
guard let colonIndex = index(of: .startOfScope(":"), after: caseIndex),
var firstTokenIndex = index(of: .nonSpaceOrComment, after: colonIndex),
!tokens[firstTokenIndex].isLinebreak,
!tokens[firstTokenIndex].isEndOfScope
else { return }
insertLinebreak(at: firstTokenIndex)
if tokens[firstTokenIndex - 1].isSpace {
removeToken(at: firstTokenIndex - 1)
firstTokenIndex -= 1
}
let movedTokenIndex = firstTokenIndex + 1
let indent = currentIndentForLine(at: caseIndex) + options.indent
insertSpace(indent, at: movedTokenIndex)
}
}
+241
View File
@@ -0,0 +1,241 @@
//
// WrapCaseBodiesTests.swift
// SwiftFormatTests
//
// Created by Kim de Vos on 3/23/26.
// Copyright © 2026 Nick Lockwood. All rights reserved.
//
import XCTest
@testable import SwiftFormat
final class WrapCaseBodiesTests: XCTestCase {
func testWrapSingleLineCaseBody() {
let input = """
switch foo {
case .bar: return bar
default: return baz
}
"""
let output = """
switch foo {
case .bar:
return bar
default:
return baz
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies)
}
func testAlreadyWrappedCaseBodiesUnchanged() {
let input = """
switch foo {
case .bar:
return bar
default:
return baz
}
"""
testFormatting(for: input, rule: .wrapCaseBodies)
}
func testWrapDefaultCaseBody() {
let input = """
switch foo {
case .bar: break
default: return baz
}
"""
let output = """
switch foo {
case .bar:
break
default:
return baz
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies)
}
func testWrapMultiPatternCaseBody() {
let input = """
switch foo {
case .bar, .baz: return quux
default: break
}
"""
let output = """
switch foo {
case .bar, .baz:
return quux
default:
break
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies,
exclude: [.wrapSwitchCases])
}
func testWrapCaseWithWhereClause() {
let input = """
switch foo {
case let x where x > 0: return x
default: return 0
}
"""
let output = """
switch foo {
case let x where x > 0:
return x
default:
return 0
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies)
}
func testCaseWithCommentAfterColonUnchanged() {
let input = """
switch foo {
case .bar: // comment
return bar
default:
return baz
}
"""
testFormatting(for: input, rule: .wrapCaseBodies,
exclude: [.blankLineAfterSwitchCase])
}
func testWrapUnknownDefaultCaseBody() {
let input = """
switch foo {
case .bar: return bar
@unknown default: return baz
}
"""
let output = """
switch foo {
case .bar:
return bar
@unknown default:
return baz
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies)
}
func testWrapNestedSwitchCaseBodies() {
let input = """
switch foo {
case .bar:
switch baz {
case .a: return a
case .b: return b
default: return c
}
default: return other
}
"""
let output = """
switch foo {
case .bar:
switch baz {
case .a:
return a
case .b:
return b
default:
return c
}
default:
return other
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies,
exclude: [.blankLineAfterSwitchCase])
}
func testWrapCaseBodiesFullExample() {
let input = """
extension Int {
var foo: String {
switch self {
case 0: return "zero"
case 1: return "one"
case 2: return "two"
default: return "other"
}
}
}
"""
let output = """
extension Int {
var foo: String {
switch self {
case 0:
return "zero"
case 1:
return "one"
case 2:
return "two"
default:
return "other"
}
}
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies)
}
func testWrapCaseBodyWithAssignment() {
let input = """
switch foo {
case .bar: baz = true
default: baz = false
}
"""
let output = """
switch foo {
case .bar:
baz = true
default:
baz = false
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies)
}
func testWrapSwitchExpressionCaseBodies() {
let input = """
extension Int {
var foo: String {
return switch self {
case 0: "zero"
case 1: "one"
case 2: "two"
default: "other"
}
}
}
"""
let output = """
extension Int {
var foo: String {
return switch self {
case 0:
"zero"
case 1:
"one"
case 2:
"two"
default:
"other"
}
}
}
"""
testFormatting(for: input, output, rule: .wrapCaseBodies)
}
}
+1
View File
@@ -84,6 +84,7 @@ extension XCTestCase {
.unusedPrivateDeclarations,
.preferFinalClasses,
.preferExplicitFalse,
.wrapCaseBodies,
]
let exclude = exclude + defaultExclusions.filter { !rules.contains($0) }
let formatResult: (output: String, changes: [SwiftFormat.Formatter.Change])