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") }