Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Style/SuperfluousElseRule.swift
Danny Mösch 47335d7f95 Remove tracking of correction positions (#5950)
Report number of corrections per file instead.
2025-04-05 06:04:37 -04:00

372 lines
12 KiB
Swift

import SwiftLintCore
import SwiftSyntax
@SwiftSyntaxRule(explicitRewriter: true, optIn: true)
struct SuperfluousElseRule: Rule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "superfluous_else",
name: "Superfluous Else",
description: "Else branches should be avoided when the previous if-block exits the current scope",
kind: .style,
nonTriggeringExamples: [
Example("""
if i > 0 {
// comment
} else if i < 12 {
return 2
} else {
return 3
}
"""),
Example("""
if i > 0 {
let a = 1
if a > 1 {
// comment
} else {
return 1
}
// comment
} else {
return 3
}
"""),
Example("""
if i > 0 {
if a > 1 {
return 1
}
} else {
return 3
}
"""),
Example("""
if i > 0 {
if a > 1 {
if a > 1 {
// comment
} else {
return 1
}
}
} else {
return 3
}
""", excludeFromDocumentation: true),
Example("""
for i in list {
if i > 12 {
// Do nothing
} else {
continue
}
if i > 14 {
// Do nothing
} else if i > 13 {
break
}
}
"""),
Example("""
if #available(iOS 13, *) {
return
} else {
deprecatedFunction()
}
"""),
],
triggeringExamples: [
Example("""
if i > 0 {
return 1
// comment
} ↓else {
return 2
}
"""),
Example("""
if i > 0 {
return 1
} ↓else if i < 12 {
return 2
} ↓else if i > 18 {
return 3
}
"""),
Example("""
if i > 0 {
if i < 12 {
return 5
} ↓else {
if i > 11 {
return 6
} ↓else {
return 7
}
}
} ↓else if i < 12 {
return 2
} ↓else if i < 24 {
return 8
} ↓else {
return 3
}
"""),
Example("""
for i in list {
if i > 13 {
return
} ↓else if i > 12 {
continue
} ↓else if i > 11 {
break
} ↓else {
throw error
}
}
"""),
],
corrections: [
Example("""
func f() -> Int {
if i > 0 {
return 1
// comment
} ↓else {
// another comment
return 2
// yet another comment
}
}
"""): Example("""
func f() -> Int {
if i > 0 {
return 1
// comment
}
// another comment
return 2
// yet another comment
}
"""),
Example("""
func f() -> Int {
if i > 0 {
return 1
// comment
} ↓else if i < 10 {
return 2
} ↓else {
return 3
}
}
"""): Example("""
func f() -> Int {
if i > 0 {
return 1
// comment
}
if i < 10 {
return 2
}
return 3
}
"""),
Example("""
func f() -> Int {
if i > 0 {
return 1
// comment
} ↓else if i < 10 {
// another comment
return 2
}
}
"""): Example("""
func f() -> Int {
if i > 0 {
return 1
// comment
}
if i < 10 {
// another comment
return 2
}
}
"""),
Example("""
{
if i > 0 {
return 1
} ↓else {
return 2
}
}()
"""): Example("""
{
if i > 0 {
return 1
}
return 2
}()
"""),
Example("""
for i in list {
if i > 13 {
return
} ↓else if i > 12 {
continue // continue with next index
} ↓else if i > 11 {
break
// end of loop
} ↓else if i > 10 {
// Some error
throw error
} ↓else {
}
}
"""): Example("""
for i in list {
if i > 13 {
return
}
if i > 12 {
continue // continue with next index
}
if i > 11 {
break
// end of loop
}
if i > 10 {
// Some error
throw error
}
}
"""),
]
)
}
private extension SuperfluousElseRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
override func visitPost(_ node: IfExprSyntax) {
if let elseKeyword = node.superfluousElse {
violations.append(elseKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> {
override init(configuration: ConfigurationType, file: SwiftLintFile) {
super.init(configuration: configuration, file: file)
numberOfCorrections += Visitor(configuration: configuration, file: file)
.walk(file: file) { $0.violations.map(\.position) }
.filter { !$0.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) }
.count
}
override func visitAny(_ node: Syntax) -> Syntax? {
numberOfCorrections == 0 ? node : nil // Avoid skipping all `if` expressions in a code block.
}
override func visit(_ list: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
var newStatements = CodeBlockItemListSyntax()
var ifExprRewritten = false
for item in list {
guard let ifExpr = item.item.as(ExpressionStmtSyntax.self)?.expression.as(IfExprSyntax.self),
let elseKeyword = ifExpr.superfluousElse,
!elseKeyword.isContainedIn(regions: disabledRegions, locationConverter: locationConverter) else {
newStatements.append(item)
continue
}
ifExprRewritten = true
let (newIfStm, removedItems) = modify(ifExpr: ifExpr)
newStatements.append(
CodeBlockItemSyntax(item: CodeBlockItemSyntax.Item(ExpressionStmtSyntax(expression: newIfStm)))
)
newStatements.append(contentsOf: removedItems)
}
return ifExprRewritten ? visit(newStatements) : super.visit(newStatements)
}
private func modify(ifExpr: IfExprSyntax) -> (newIfExpr: IfExprSyntax, removedItems: [CodeBlockItemSyntax]) {
let ifExprWithoutElse = removeElse(from: ifExpr)
if case let .codeBlock(block) = ifExpr.elseBody {
let indenter = CodeIndentingRewriter(style: .unindentSpaces(4))
let unindentedBlock = indenter.rewrite(block).cast(CodeBlockSyntax.self)
let items = unindentedBlock.statements.with(
\.trailingTrivia,
unindentedBlock.rightBrace.leadingTrivia.withTrailingEmptyLineRemoved
)
return (ifExprWithoutElse, Array(items))
}
if case let .ifExpr(nestedIfExpr) = ifExpr.elseBody {
let unindentedIfExpr = nestedIfExpr.with(
\.leadingTrivia,
Trivia(pieces: [.newlines(1)] + (ifExpr.leadingTrivia.indentation(isOnNewline: true) ?? Trivia()))
)
let item = CodeBlockItemSyntax(
item: CodeBlockItemSyntax.Item(ExpressionStmtSyntax(expression: unindentedIfExpr))
)
return (ifExprWithoutElse, [item])
}
return (ifExpr, [])
}
private func removeElse(from ifExpr: IfExprSyntax) -> IfExprSyntax {
ifExpr
.with(\.body, ifExpr.body.with(\.rightBrace, ifExpr.body.rightBrace.with(\.trailingTrivia, Trivia())))
.with(\.elseKeyword, nil)
.with(\.elseBody, nil)
}
}
}
private extension IfExprSyntax {
var superfluousElse: TokenSyntax? {
guard elseKeyword != nil,
conditions.onlyElement?.condition.is(AvailabilityConditionSyntax.self) != true,
lastStatementExitsScope(in: body) else {
return nil
}
if let parent = parent?.as(IfExprSyntax.self) {
return parent.superfluousElse != nil ? elseKeyword : nil
}
return elseKeyword
}
private var returnsInAllBranches: Bool {
guard lastStatementExitsScope(in: body) else {
return false
}
if case let .ifExpr(nestedIfExpr) = elseBody {
return nestedIfExpr.returnsInAllBranches
}
if case let .codeBlock(block) = elseBody {
return lastStatementExitsScope(in: block)
}
return false
}
private func lastStatementExitsScope(in block: CodeBlockSyntax) -> Bool {
guard let lastItem = block.statements.last?.item else {
return false
}
if [.returnStmt, .throwStmt, .continueStmt, .breakStmt].contains(lastItem.kind) {
return true
}
if let exprStmt = lastItem.as(ExpressionStmtSyntax.self),
let lastIfExpr = exprStmt.expression.as(IfExprSyntax.self) {
return lastIfExpr.returnsInAllBranches
}
return false
}
}