Skip to content

Commit b0c4925

Browse files
authored
Merge pull request swiftlang#154 from google/fix-early-exits
Clean up UseEarlyExits and fix bug sometimes causing code loss.
2 parents a798c8e + 1f62540 commit b0c4925

File tree

3 files changed

+196
-100
lines changed

3 files changed

+196
-100
lines changed

Sources/SwiftFormatRules/UseEarlyExits.swift

Lines changed: 73 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,73 +16,95 @@ import SwiftSyntax
1616

1717
/// Early exits should be used whenever possible.
1818
///
19-
/// Practically, this means that `if ... else return/throw/break` constructs should be replaced by
20-
/// `guard ... else { return/throw/break }` constructs in order to keep indentation levels low.
19+
/// This means that `if ... else { return/throw/break/continue }` constructs should be replaced by
20+
/// `guard ... else { return/throw/break/continue }` constructs in order to keep indentation levels
21+
/// low. Specifically, code of the following form:
2122
///
22-
/// Lint: `if ... else return/throw/break` constructs will yield a lint error.
23+
/// ```swift
24+
/// if condition {
25+
/// trueBlock
26+
/// } else {
27+
/// falseBlock
28+
/// return/throw/break/continue
29+
/// }
30+
/// ```
2331
///
24-
/// Format: `if ... else return/throw/break` constructs will be replaced with equivalent
25-
/// `guard ... else { return/throw/break }` constructs.
26-
/// TODO(abl): replace implicit guards as well?
32+
/// will be transformed into:
33+
///
34+
/// ```swift
35+
/// guard condition else {
36+
/// falseBlock
37+
/// return/throw/break/continue
38+
/// }
39+
/// trueBlock
40+
/// ```
41+
///
42+
/// Lint: `if ... else { return/throw/break/continue }` constructs will yield a lint error.
43+
///
44+
/// Format: `if ... else { return/throw/break/continue }` constructs will be replaced with
45+
/// equivalent `guard ... else { return/throw/break/continue }` constructs.
2746
///
2847
/// - SeeAlso: https://google.github.io/swift#guards-for-early-exits
2948
public final class UseEarlyExits: SyntaxFormatRule {
3049

31-
public override func visit(_ node: CodeBlockSyntax) -> Syntax {
32-
33-
var newItems: [CodeBlockItemSyntax] = []
34-
for statement in node.statements {
35-
guard let ifStmt = statement.item as? IfStmtSyntax else { return node }
36-
guard let elseStmt = ifStmt.elseBody else { return node }
37-
guard let elseBody = elseStmt as? CodeBlockSyntax else { return node }
50+
public override func visit(_ node: CodeBlockItemListSyntax) -> Syntax {
51+
// Continue recursing down the tree first, so that any nested/child nodes get transformed first.
52+
let nodeAfterTransformingChildren = super.visit(node)
53+
guard let codeBlockItems = nodeAfterTransformingChildren as? CodeBlockItemListSyntax else {
54+
return nodeAfterTransformingChildren
55+
}
56+
57+
return SyntaxFactory.makeCodeBlockItemList(
58+
codeBlockItems.flatMap { (codeBlockItem: CodeBlockItemSyntax) -> [CodeBlockItemSyntax] in
59+
// The `elseBody` of an `IfStmtSyntax` will be a `CodeBlockSyntax` if it's an `else` block,
60+
// or another `IfStmtSyntax` if it's an `else if` block. We only want to handle the former.
61+
guard let ifStatement = codeBlockItem.item as? IfStmtSyntax,
62+
let elseBody = ifStatement.elseBody as? CodeBlockSyntax,
63+
codeBlockEndsWithEarlyExit(elseBody)
64+
else {
65+
return [codeBlockItem]
66+
}
3867

39-
if elseContainsControlStmt(elseStmt: elseStmt) {
40-
diagnose(.useGuardStmt, on: ifStmt)
41-
guard let moveDeletedIfCode = visit(
42-
ifStmt.body.withLeftBrace(nil).withRightBrace(nil)) as? CodeBlockSyntax else { continue }
43-
guard let moveElseBody = visit(elseBody) as? CodeBlockSyntax else { continue }
44-
45-
let ifConditions = ifStmt.conditions
46-
let formattedGuardKeyword = SyntaxFactory.makeGuardKeyword(
47-
leadingTrivia: ifStmt.ifKeyword.leadingTrivia,
68+
diagnose(.useGuardStatement, on: ifStatement.elseKeyword)
69+
70+
let trueBlock = ifStatement.body.withLeftBrace(nil).withRightBrace(nil)
71+
72+
let guardKeyword = SyntaxFactory.makeGuardKeyword(
73+
leadingTrivia: ifStatement.ifKeyword.leadingTrivia,
4874
trailingTrivia: .spaces(1))
49-
let newGuardStmt = SyntaxFactory.makeGuardStmt(
50-
guardKeyword: formattedGuardKeyword,
51-
conditions: ifConditions,
75+
let guardStatement = SyntaxFactory.makeGuardStmt(
76+
guardKeyword: guardKeyword,
77+
conditions: ifStatement.conditions,
5278
elseKeyword: SyntaxFactory.makeElseKeyword(trailingTrivia: .spaces(1)),
53-
body: moveElseBody)
54-
newItems.append(
55-
SyntaxFactory.makeCodeBlockItem(item: newGuardStmt,
56-
semicolon: nil)
57-
)
58-
newItems.append(
59-
SyntaxFactory.makeCodeBlockItem(item: moveDeletedIfCode,
60-
semicolon: nil)
61-
)
62-
}
63-
}
79+
body: elseBody)
6480

65-
let newNode = node.withStatements(SyntaxFactory.makeCodeBlockItemList(newItems))
66-
return super.visit(newNode)
81+
return [
82+
SyntaxFactory.makeCodeBlockItem(item: guardStatement, semicolon: nil),
83+
SyntaxFactory.makeCodeBlockItem(item: trueBlock, semicolon: nil),
84+
]
85+
})
6786
}
6887

69-
func elseContainsControlStmt(elseStmt: Syntax) -> Bool {
70-
for child in elseStmt.children {
71-
guard let codeBlockList = child as? CodeBlockItemListSyntax else { continue }
72-
guard let last = codeBlockList.child(at: codeBlockList.count - 1) as?
73-
CodeBlockItemSyntax else { continue }
88+
/// Returns true if the last statement in the given code block is one that will cause an early
89+
/// exit from the control flow construct or function.
90+
private func codeBlockEndsWithEarlyExit(_ codeBlock: CodeBlockSyntax) -> Bool {
91+
let statements = codeBlock.statements
92+
guard statements.count > 0 else { return false }
93+
let lastStatement = statements[statements.count - 1]
7494

75-
switch last.item {
76-
case is ReturnStmtSyntax, is ThrowStmtSyntax, is BreakStmtSyntax, is ContinueStmtSyntax:
77-
return true
78-
default:
79-
continue
80-
}
95+
switch lastStatement.item {
96+
case is ReturnStmtSyntax, is ThrowStmtSyntax, is BreakStmtSyntax, is ContinueStmtSyntax:
97+
return true
98+
default:
99+
return false
81100
}
82-
return false
83101
}
84102
}
85103

86104
extension Diagnostic.Message {
87-
static let useGuardStmt = Diagnostic.Message(.warning, "replace with guard statement")
105+
106+
static let useGuardStatement = Diagnostic.Message(
107+
.warning,
108+
"replace the `if/else` block with a `guard` statement containing the early exit"
109+
)
88110
}

Sources/swift-format/PopulatePipeline.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func populate(_ pipeline: Pipeline) {
201201
pipeline.addFormatter(
202202
UseEarlyExits.self,
203203
for:
204-
CodeBlockSyntax.self
204+
CodeBlockItemListSyntax.self
205205
)
206206

207207
pipeline.addFormatter(

Tests/SwiftFormatRulesTests/UseEarlyExitsTests.swift

Lines changed: 122 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,142 @@ import XCTest
55
@testable import SwiftFormatRules
66

77
public class UseEarlyExitsTests: DiagnosingTestCase {
8-
public func testIfToGuardStmtSwitch() {
9-
// The indentation in the expected output is explicitly incorrect because this formatting rule
10-
// does not fix it up with the assumption that the pretty-printer will handle it.
8+
9+
public func testBasicIfElse() {
10+
// In this and other tests, the indentation of the true block in the expected output is
11+
// explicitly incorrect because this formatting rule does not fix it up with the assumption that
12+
// the pretty-printer will handle it.
13+
XCTAssertFormatting(
14+
UseEarlyExits.self,
15+
input: """
16+
if condition {
17+
trueBlock()
18+
} else {
19+
falseBlock()
20+
return
21+
}
22+
""",
23+
expected: """
24+
guard condition else {
25+
falseBlock()
26+
return
27+
}
28+
trueBlock()
29+
""")
30+
}
31+
32+
public func testIfElseWithBothEarlyExiting() {
33+
XCTAssertFormatting(
34+
UseEarlyExits.self,
35+
input: """
36+
if condition {
37+
trueBlock()
38+
return
39+
} else {
40+
falseBlock()
41+
return
42+
}
43+
""",
44+
expected: """
45+
guard condition else {
46+
falseBlock()
47+
return
48+
}
49+
trueBlock()
50+
return
51+
""")
52+
}
53+
54+
public func testElseIfsDoNotChange() {
55+
let input = """
56+
if condition {
57+
trueBlock()
58+
} else if otherCondition {
59+
otherBlock()
60+
return
61+
}
62+
"""
63+
XCTAssertFormatting(UseEarlyExits.self, input: input, expected: input)
64+
}
65+
66+
public func testElsesAtEndOfElseIfsDoNotChange() {
67+
let input = """
68+
if condition {
69+
trueBlock()
70+
} else if otherCondition {
71+
otherBlock()
72+
return
73+
} else {
74+
falseBlock()
75+
return
76+
}
77+
"""
78+
XCTAssertFormatting(UseEarlyExits.self, input: input, expected: input)
79+
}
80+
81+
public func testComplex() {
1182
XCTAssertFormatting(
1283
UseEarlyExits.self,
1384
input: """
14-
func discombobulate(_ values: [Int]) throws -> Int {
85+
func discombobulate(_ values: [Int]) throws -> Int {
1586
16-
// Comment 1
87+
// Comment 1
1788
18-
/*Comment 2*/ if let first = values.first {
19-
// Comment 3
89+
/*Comment 2*/ if let first = values.first {
90+
// Comment 3
2091
21-
/// Doc comment
22-
if first >= 0 {
23-
// Comment 4
24-
var result = 0
25-
for value in values {
26-
result += invertedCombobulatoryFactor(of: value)
27-
}
28-
return result
29-
} else {
30-
print("Can't have negative energy")
31-
throw DiscombobulationError.negativeEnergy
32-
}
33-
} else {
34-
print("The array was empty")
35-
throw DiscombobulationError.arrayWasEmpty
36-
}
37-
}
38-
""",
92+
/// Doc comment
93+
if first >= 0 {
94+
// Comment 4
95+
var result = 0
96+
for value in values {
97+
result += invertedCombobulatoryFactor(of: value)
98+
}
99+
return result
100+
} else {
101+
print("Can't have negative energy")
102+
throw DiscombobulationError.negativeEnergy
103+
}
104+
} else {
105+
print("The array was empty")
106+
throw DiscombobulationError.arrayWasEmpty
107+
}
108+
}
109+
""",
39110
expected: """
40-
func discombobulate(_ values: [Int]) throws -> Int {
111+
func discombobulate(_ values: [Int]) throws -> Int {
41112
42-
// Comment 1
113+
// Comment 1
43114
44-
/*Comment 2*/ guard let first = values.first else {
45-
print("The array was empty")
46-
throw DiscombobulationError.arrayWasEmpty
47-
}
48-
// Comment 3
115+
/*Comment 2*/ guard let first = values.first else {
116+
print("The array was empty")
117+
throw DiscombobulationError.arrayWasEmpty
118+
}
119+
// Comment 3
49120
50-
/// Doc comment
51-
guard first >= 0 else {
52-
print("Can't have negative energy")
53-
throw DiscombobulationError.negativeEnergy
54-
}
55-
// Comment 4
56-
var result = 0
57-
for value in values {
58-
result += invertedCombobulatoryFactor(of: value)
59-
}
60-
return result
61-
}
62-
""")
63-
121+
/// Doc comment
122+
guard first >= 0 else {
123+
print("Can't have negative energy")
124+
throw DiscombobulationError.negativeEnergy
125+
}
126+
// Comment 4
127+
var result = 0
128+
for value in values {
129+
result += invertedCombobulatoryFactor(of: value)
130+
}
131+
return result
132+
}
133+
""")
64134
}
65135

66136
#if !os(macOS)
67137
static let allTests = [
68-
UseEarlyExitsTests.testSwitchStmts,
69-
]
138+
UseEarlyExitsTests.testBasicIfElse,
139+
UseEarlyExitsTests.testIfElseWithBothEarlyExiting,
140+
UseEarlyExitsTests.testElseIfsDoNotChange,
141+
UseEarlyExitsTests.testElsesAtEndOfElseIfsDoNotChange,
142+
UseEarlyExitsTests.testComplex,
143+
]
70144
#endif
71145

72146
}

0 commit comments

Comments
 (0)