diff --git a/CHANGELOG.md b/CHANGELOG.md index f48da29a3..cd247e545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ [gibachan](https://github.com/gibachan) [#5233](https://github.com/realm/SwiftLint/pull/5233) +* Make `unneeded_break_in_switch` auto correctable. + [KS1019](https://github.com/KS1019/) + #### Bug Fixes * Fix false positive in `unused_import` rule that triggered on diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift index b4fa0fce2..1d9ca9ad5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift @@ -12,7 +12,7 @@ private func embedInSwitch( """, file: file, line: line) } -struct UnneededBreakInSwitchRule: SwiftSyntaxRule, ConfigurationProviderRule { +struct UnneededBreakInSwitchRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -45,23 +45,121 @@ struct UnneededBreakInSwitchRule: SwiftSyntaxRule, ConfigurationProviderRule { embedInSwitch("something()\n ↓break // comment"), embedInSwitch("something()\n ↓break", case: "default"), embedInSwitch("something()\n ↓break", case: "case .foo, .foo2 where condition") + ], + corrections: [ + embedInSwitch("something()\n ↓break") + : embedInSwitch("something()"), + embedInSwitch("something()\n ↓break // line comment") + : embedInSwitch("something()\n // line comment"), + embedInSwitch(""" + something() + ↓break + /* + block comment + */ + """) + : embedInSwitch(""" + something() + /* + block comment + */ + """), + embedInSwitch("something()\n ↓break /// doc line comment") + : embedInSwitch("something()\n /// doc line comment"), + embedInSwitch(""" + something() + ↓break + /// + /// doc block comment + /// + """) + : embedInSwitch(""" + something() + /// + /// doc block comment + /// + """), + embedInSwitch("something()\n ↓break", case: "default") + : embedInSwitch("something()", case: "default"), + embedInSwitch("something()\n ↓break", case: "case .foo, .foo2 where condition") + : embedInSwitch("something()", case: "case .foo, .foo2 where condition") ] ) func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { UnneededBreakInSwitchRuleVisitor(viewMode: .sourceAccurate) } + + func makeRewriter(file: SwiftLintCore.SwiftLintFile) -> SwiftLintCore.ViolationsSyntaxRewriter? { + UnneededBreakInSwitchRewriter( + locationConverter: file.locationConverter, + disabledRegions: disabledRegions(file: file) + ) + } } private final class UnneededBreakInSwitchRuleVisitor: ViolationsSyntaxVisitor { override func visitPost(_ node: SwitchCaseSyntax) { - guard node.statements.count > 1, - let statement = node.statements.last, - let breakStatement = statement.item.as(BreakStmtSyntax.self), - breakStatement.label == nil else { + guard let statement = node.unneededBreak else { return } violations.append(statement.item.positionAfterSkippingLeadingTrivia) } } + +private final class UnneededBreakInSwitchRewriter: SyntaxRewriter, ViolationsSyntaxRewriter { + private(set) var correctionPositions: [SwiftSyntax.AbsolutePosition] = [] + let locationConverter: SourceLocationConverter + let disabledRegions: [SourceRange] + + init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) { + self.locationConverter = locationConverter + self.disabledRegions = disabledRegions + } + + override func visit(_ node: SwitchCaseSyntax) -> SwitchCaseSyntax { + let stmts = CodeBlockItemListSyntax(node.statements.dropLast()) + + guard let breakStatement = node.unneededBreak, let secondLast = stmts.last, + !node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else { + return super.visit(node) + } + + correctionPositions.append(breakStatement.item.positionAfterSkippingLeadingTrivia) + + let trivia = breakStatement.item.leadingTrivia + breakStatement.item.trailingTrivia + + let newNode = node + .with(\.statements, stmts) + .with(\.statements.trailingTrivia, secondLast.item.trailingTrivia + trivia) + .trimmed { !$0.isComment } + .formatted() + .as(SwitchCaseSyntax.self)! + + return super.visit(newNode) + } +} + +private extension SwitchCaseSyntax { + var unneededBreak: CodeBlockItemSyntax? { + guard statements.count > 1, + let breakStatement = statements.last?.item.as(BreakStmtSyntax.self), + breakStatement.label == nil else { + return nil + } + + return statements.last + } +} + +private extension TriviaPiece { + var isComment: Bool { + switch self { + case .lineComment, .blockComment, .docLineComment, .docBlockComment: + return true + default: + return false + } + } +}