import SwiftBasicFormat import SwiftLintCore import SwiftSyntax @SwiftSyntaxRule(explicitRewriter: true, optIn: true) struct SuperfluousElseRule: Rule { var configuration = SeverityConfiguration(.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 { 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 { 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 } }