diff --git a/Rules.md b/Rules.md index 12cc82f1..9a5eda9d 100644 --- a/Rules.md +++ b/Rules.md @@ -136,6 +136,7 @@ * [unusedPrivateDeclarations](#unusedPrivateDeclarations) * [urlMacro](#urlMacro) * [validateTestCases](#validateTestCases) +* [wrapCaseBodies](#wrapCaseBodies) * [wrapConditionalBodies](#wrapConditionalBodies) * [wrapEnumCases](#wrapEnumCases) * [wrapMultilineConditionalAssignment](#wrapMultilineConditionalAssignment) @@ -4338,6 +4339,22 @@ Option | Description
+## wrapCaseBodies + +Wrap the bodies of inline switch cases onto a new line. + +
+Examples + +```diff +- case .foo: return bar ++ case .foo: ++ return bar +``` + +
+
+ ## wrapConditionalBodies Wrap the bodies of inline conditional statements onto a new line. diff --git a/Sources/RuleRegistry.generated.swift b/Sources/RuleRegistry.generated.swift index 5408455e..799b312a 100644 --- a/Sources/RuleRegistry.generated.swift +++ b/Sources/RuleRegistry.generated.swift @@ -141,6 +141,7 @@ let ruleRegistry: [String: FormatRule] = [ "wrap": .wrap, "wrapArguments": .wrapArguments, "wrapAttributes": .wrapAttributes, + "wrapCaseBodies": .wrapCaseBodies, "wrapConditionalBodies": .wrapConditionalBodies, "wrapEnumCases": .wrapEnumCases, "wrapFunctionBodies": .wrapFunctionBodies, diff --git a/Sources/Rules/WrapCaseBodies.swift b/Sources/Rules/WrapCaseBodies.swift new file mode 100644 index 00000000..e50eebb1 --- /dev/null +++ b/Sources/Rules/WrapCaseBodies.swift @@ -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) + } +} diff --git a/Tests/Rules/WrapCaseBodiesTests.swift b/Tests/Rules/WrapCaseBodiesTests.swift new file mode 100644 index 00000000..cc00ce5e --- /dev/null +++ b/Tests/Rules/WrapCaseBodiesTests.swift @@ -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) + } +} diff --git a/Tests/XCTestCase+testFormatting.swift b/Tests/XCTestCase+testFormatting.swift index 4cf05e73..7d5c4de4 100644 --- a/Tests/XCTestCase+testFormatting.swift +++ b/Tests/XCTestCase+testFormatting.swift @@ -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])