mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
47335d7f95
Report number of corrections per file instead.
372 lines
12 KiB
Swift
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
|
|
}
|
|
}
|