Skip to content

Commit 54f73c1

Browse files
LaurenWhiteallevato
authored andcommitted
Implement use early exits (swiftlang#75)
1 parent 17bdbde commit 54f73c1

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed

Diff for: tools/swift-format/Sources/Rules/ReindentBlock.swift

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Foundation
2+
import SwiftSyntax
3+
4+
/// Rewriter that changes the indentation of a given code block with the
5+
/// provided indentation adjustment.
6+
private final class ReindentBlock: SyntaxRewriter {
7+
let adjustment: Int
8+
9+
init(adjustment: Int) {
10+
self.adjustment = adjustment
11+
}
12+
13+
override func visit(_ token: TokenSyntax) -> Syntax {
14+
guard token.leadingTrivia.containsNewlines else { return token }
15+
16+
var newTrivia: [TriviaPiece] = []
17+
var previousIsNewline = false
18+
19+
for piece in token.leadingTrivia {
20+
if case .newlines = piece {
21+
newTrivia.append(piece)
22+
previousIsNewline = true
23+
continue
24+
} else {
25+
guard previousIsNewline else { newTrivia.append(piece); continue }
26+
if case .spaces(let n) = piece {
27+
// Replace current indent
28+
let newIndent = n + adjustment
29+
let newPiece = (newIndent > 0) ? Trivia.spaces(newIndent) : .spaces(0)
30+
newTrivia.append(contentsOf: newPiece)
31+
} else {
32+
// Insert new indent at front
33+
let newPiece = (adjustment > 0) ? Trivia.spaces(adjustment) : .spaces(0)
34+
// TODO(laurenwhite): warning for indentation without enough spaces to be adjusted?
35+
newTrivia.append(contentsOf: newPiece)
36+
newTrivia.append(piece)
37+
}
38+
previousIsNewline = false
39+
}
40+
}
41+
return token.withLeadingTrivia(Trivia(pieces: newTrivia))
42+
}
43+
44+
}
45+
46+
/// Replaces the given block with new indentation from the provided adjustment.
47+
/// - Parameters:
48+
/// - block: The code block whose containing items will be reindented
49+
/// - adjustment: The number of spaces by which the current indentation will be moved. This
50+
/// integer may be negative (moves left) or positive (moves right).
51+
func reindentBlock(block: CodeBlockSyntax, adjustment: Int) -> Syntax {
52+
return ReindentBlock(adjustment: adjustment).visit(block)
53+
}

Diff for: tools/swift-format/Sources/Rules/UseEarlyExits.swift

+59
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,64 @@ import SwiftSyntax
1515
///
1616
/// - SeeAlso: https://google.github.io/swift#guards-for-early-exits
1717
public final class UseEarlyExits: SyntaxFormatRule {
18+
19+
public override func visit(_ node: CodeBlockSyntax) -> Syntax {
20+
21+
var newItems: [CodeBlockItemSyntax] = []
22+
for statement in node.statements {
23+
guard let ifStmt = statement.item as? IfStmtSyntax else { return node }
24+
guard let elseStmt = ifStmt.elseBody else { return node }
25+
guard let elseBody = elseStmt as? CodeBlockSyntax else { return node }
1826

27+
if elseContainsControlStmt(elseStmt: elseStmt) {
28+
diagnose(.useGuardStmt, on: ifStmt)
29+
guard let moveDeletedIfCode = visit(
30+
ifStmt.body.withLeftBrace(nil).withRightBrace(nil)) as? CodeBlockSyntax else { continue }
31+
guard let moveElseBody = visit(elseBody) as? CodeBlockSyntax else { continue }
32+
33+
let ifConditions = ifStmt.conditions
34+
let formattedGuardKeyword = SyntaxFactory.makeGuardKeyword(
35+
leadingTrivia: ifStmt.ifKeyword.leadingTrivia,
36+
trailingTrivia: .spaces(1))
37+
let newGuardStmt = SyntaxFactory.makeGuardStmt(
38+
guardKeyword: formattedGuardKeyword,
39+
conditions: ifConditions,
40+
elseKeyword: SyntaxFactory.makeElseKeyword(trailingTrivia: .spaces(1)),
41+
body: moveElseBody)
42+
newItems.append(
43+
SyntaxFactory.makeCodeBlockItem(item: newGuardStmt,
44+
semicolon: nil,
45+
errorTokens: nil)
46+
)
47+
newItems.append(
48+
SyntaxFactory.makeCodeBlockItem(item: moveDeletedIfCode,
49+
semicolon: nil,
50+
errorTokens: nil)
51+
)
52+
}
53+
}
54+
55+
let newNode = node.withStatements(SyntaxFactory.makeCodeBlockItemList(newItems))
56+
return super.visit(newNode)
57+
}
58+
59+
func elseContainsControlStmt(elseStmt: Syntax) -> Bool {
60+
for child in elseStmt.children {
61+
guard let codeBlockList = child as? CodeBlockItemListSyntax else { continue }
62+
guard let last = codeBlockList.child(at: codeBlockList.count - 1) as?
63+
CodeBlockItemSyntax else { continue }
64+
65+
switch last.item {
66+
case is ReturnStmtSyntax, is ThrowStmtSyntax, is BreakStmtSyntax, is ContinueStmtSyntax:
67+
return true
68+
default:
69+
continue
70+
}
71+
}
72+
return false
73+
}
74+
}
75+
76+
extension Diagnostic.Message {
77+
static let useGuardStmt = Diagnostic.Message(.warning, "replace with guard statement")
1978
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Foundation
2+
import XCTest
3+
import SwiftSyntax
4+
5+
@testable import Rules
6+
7+
public class UseEarlyExitsTests: DiagnosingTestCase {
8+
public func testIfToGuardStmtSwitch() {
9+
XCTAssertFormatting(
10+
UseEarlyExits.self,
11+
input: """
12+
func discombobulate(_ values: [Int]) throws -> Int {
13+
14+
// Comment 1
15+
16+
/*Comment 2*/ if let first = values.first {
17+
// Comment 3
18+
19+
/// Doc comment
20+
if first >= 0 {
21+
// Comment 4
22+
var result = 0
23+
for value in values {
24+
result += invertedCombobulatoryFactor(of: value)
25+
}
26+
return result
27+
} else {
28+
print("Can't have negative energy")
29+
throw DiscombobulationError.negativeEnergy
30+
}
31+
} else {
32+
print("The array was empty")
33+
throw DiscombobulationError.arrayWasEmpty
34+
}
35+
}
36+
""",
37+
expected: """
38+
func discombobulate(_ values: [Int]) throws -> Int {
39+
40+
// Comment 1
41+
42+
/*Comment 2*/ guard let first = values.first else {
43+
print("The array was empty")
44+
throw DiscombobulationError.arrayWasEmpty
45+
}
46+
// Comment 3
47+
48+
/// Doc comment
49+
guard first >= 0 else {
50+
print("Can't have negative energy")
51+
throw DiscombobulationError.negativeEnergy
52+
}
53+
// Comment 4
54+
var result = 0
55+
for value in values {
56+
result += invertedCombobulatoryFactor(of: value)
57+
}
58+
return result
59+
}
60+
""")
61+
62+
}
63+
64+
#if !os(macOS)
65+
static let allTests = [
66+
UseEarlyExitsTests.testSwitchStmts,
67+
]
68+
#endif
69+
70+
}

0 commit comments

Comments
 (0)