Skip to content

Commit 1a555cf

Browse files
committed
Improve performance of BasicFormat
This mostly improves performance by calling `previousToken`, `nextToken`, and `firstToken` less often.
1 parent 4a3f0c0 commit 1a555cf

File tree

5 files changed

+48
-34
lines changed

5 files changed

+48
-34
lines changed

CodeGeneration/Sources/Utils/CodeGenerationFormat.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ import SwiftSyntax
1515

1616
/// A format style for files generated by CodeGeneration.
1717
public class CodeGenerationFormat: BasicFormat {
18-
public override var indentation: TriviaPiece { .spaces(indentationLevel * 2) }
18+
public init() {
19+
super.init(indentationWidth: .spaces(2))
20+
}
21+
22+
var indentedNewline: Trivia {
23+
.newline + currentIndentationLevel
24+
}
1925

2026
public override func visit(_ node: ArrayElementListSyntax) -> ArrayElementListSyntax {
2127
let children = node.children(viewMode: .all)
@@ -100,7 +106,7 @@ public class CodeGenerationFormat: BasicFormat {
100106
}
101107

102108
private func formatChildrenSeparatedByNewline<SyntaxType: SyntaxProtocol>(children: SyntaxChildren, elementType: SyntaxType.Type) -> [SyntaxType] {
103-
indentationLevel += 1
109+
pushIndentationLevel(increasingIndentationBy: indentationWidth)
104110
var formattedChildren = children.map {
105111
self.visit($0).as(SyntaxType.self)!
106112
}
@@ -111,7 +117,7 @@ public class CodeGenerationFormat: BasicFormat {
111117
return $0.with(\.leadingTrivia, indentedNewline + $0.leadingTrivia)
112118
}
113119
}
114-
indentationLevel -= 1
120+
popIndentationLevel()
115121
if !formattedChildren.isEmpty {
116122
formattedChildren[formattedChildren.count - 1] = formattedChildren[formattedChildren.count - 1].with(\.trailingTrivia, indentedNewline)
117123
}

Sources/SwiftBasicFormat/BasicFormat.swift

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,13 @@ open class BasicFormat: SyntaxRewriter {
3232
/// This is used as a reference-point to indent user-indented code.
3333
private var anchorPoints: [TokenSyntax: Trivia] = [:]
3434

35-
public init(indentationIncrement: Trivia = .spaces(4), initialIndentation: Trivia = []) {
36-
self.indentationWidth = indentationIncrement
35+
/// The previously visited token. This is faster than accessing
36+
/// `token.previousToken` inside `visit(_:TokenSyntax)`. `nil` if no token has
37+
/// been visited yet.
38+
private var previousToken: TokenSyntax? = nil
39+
40+
public init(indentationWidth: Trivia = .spaces(4), initialIndentation: Trivia = []) {
41+
self.indentationWidth = indentationWidth
3742
self.indentationStack = [initialIndentation]
3843
}
3944

@@ -132,7 +137,7 @@ open class BasicFormat: SyntaxRewriter {
132137
var ancestor: Syntax = Syntax(token)
133138
while let parent = ancestor.parent {
134139
ancestor = parent
135-
if ancestor.firstToken(viewMode: .sourceAccurate) != token {
140+
if ancestor.position != token.position {
136141
break
137142
}
138143
if let ancestorsParent = ancestor.parent, childrenSeparatedByNewline(ancestorsParent) {
@@ -143,22 +148,10 @@ open class BasicFormat: SyntaxRewriter {
143148
return false
144149
}
145150

146-
/// Whether a leading space on `token` should be added.
147-
open func requiresLeadingWhitespace(_ token: TokenSyntax) -> Bool {
148-
switch (token.previousToken(viewMode: .sourceAccurate)?.tokenKind, token.tokenKind) {
149-
case (.leftParen, .leftBrace): // Ensures there is not a space in `.map({ $0.foo })`
150-
return false
151-
default:
152-
break
153-
}
154-
155-
return token.requiresLeadingSpace
156-
}
157-
158-
/// Whether a trailing space on `token` should be added.
159-
open func requiresTrailingWhitespace(_ token: TokenSyntax) -> Bool {
160-
switch (token.tokenKind, token.nextToken(viewMode: .sourceAccurate)?.tokenKind) {
161-
case (.exclamationMark, .leftParen), // Ensures there is not a space in `myOptionalClosure!()`
151+
open func requiresWhitespace(between first: TokenSyntax?, and second: TokenSyntax?) -> Bool {
152+
switch (first?.tokenKind, second?.tokenKind) {
153+
case (.leftParen, .leftBrace), // Ensures there is not a space in `.map({ $0.foo })`
154+
(.exclamationMark, .leftParen), // Ensures there is not a space in `myOptionalClosure!()`
162155
(.exclamationMark, .period), // Ensures there is not a space in `myOptionalBar!.foo()`
163156
(.keyword(.as), .exclamationMark), // Ensures there is not a space in `as!`
164157
(.keyword(.as), .postfixQuestionMark), // Ensures there is not a space in `as?`
@@ -172,22 +165,34 @@ open class BasicFormat: SyntaxRewriter {
172165
break
173166
}
174167

175-
return token.requiresTrailingSpace
168+
if first?.requiresTrailingSpace ?? false {
169+
return true
170+
}
171+
if second?.requiresLeadingSpace ?? false {
172+
return true
173+
}
174+
return false
176175
}
177176

178177
// MARK: - Formatting a token
179178

180179
open override func visit(_ token: TokenSyntax) -> TokenSyntax {
180+
defer {
181+
self.previousToken = token
182+
}
183+
let previousToken = self.previousToken ?? token.previousToken(viewMode: .sourceAccurate)
184+
let nextToken = token.nextToken(viewMode: .sourceAccurate)
185+
181186
lazy var previousTokenWillEndWithWhitespace: Bool = {
182-
guard let previousToken = token.previousToken(viewMode: .sourceAccurate) else {
187+
guard let previousToken = previousToken else {
183188
return false
184189
}
185190
return previousToken.trailingTrivia.pieces.last?.isWhitespace ?? false
186-
|| requiresTrailingWhitespace(previousToken)
191+
|| requiresWhitespace(between: previousToken, and: token)
187192
}()
188193

189194
lazy var previousTokenWillEndWithNewline: Bool = {
190-
guard let previousToken = token.previousToken(viewMode: .sourceAccurate) else {
195+
guard let previousToken = previousToken else {
191196
// Assume that the start of the tree is equivalent to a newline so we
192197
// don't add a leading newline to the file.
193198
return true
@@ -196,7 +201,7 @@ open class BasicFormat: SyntaxRewriter {
196201
}()
197202

198203
lazy var nextTokenWillStartWithNewline: Bool = {
199-
guard let nextToken = token.nextToken(viewMode: .sourceAccurate) else {
204+
guard let nextToken = nextToken else {
200205
return false
201206
}
202207
return nextToken.leadingTrivia.startsWithNewline
@@ -206,7 +211,6 @@ open class BasicFormat: SyntaxRewriter {
206211
/// This token's trailing trivia + any spaces or tabs at the start of the
207212
/// next token's leading trivia.
208213
lazy var combinedTrailingTrivia: Trivia = {
209-
let nextToken = token.nextToken(viewMode: .sourceAccurate)
210214
let nextTokenLeadingWhitespace = nextToken?.leadingTrivia.prefix(while: { $0.isSpaceOrTab }) ?? []
211215
return trailingTrivia + Trivia(pieces: nextTokenLeadingWhitespace)
212216
}()
@@ -224,7 +228,7 @@ open class BasicFormat: SyntaxRewriter {
224228
// - the previous token didn't end with a newline
225229
leadingTrivia = .newline + leadingTrivia
226230
}
227-
} else if requiresLeadingWhitespace(token) {
231+
} else if requiresWhitespace(between: previousToken, and: token) {
228232
// Add a leading space if the token requires it unless
229233
// - it already starts with a whitespace or
230234
// - the previous token ends with a whitespace after the rewrite
@@ -243,7 +247,7 @@ open class BasicFormat: SyntaxRewriter {
243247
// - it already ends with a whitespace or
244248
// - the next token will start starts with a newline after the rewrite
245249
// because newlines should be preferred to spaces as a whitespace
246-
if requiresTrailingWhitespace(token)
250+
if requiresWhitespace(between: token, and: nextToken)
247251
&& !trailingTrivia.endsWithWhitespace
248252
&& !nextTokenWillStartWithNewline
249253
{
@@ -272,6 +276,10 @@ open class BasicFormat: SyntaxRewriter {
272276
leadingTrivia = leadingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: false)
273277
trailingTrivia = trailingTrivia.trimmingTrailingWhitespaceBeforeNewline(isBeforeNewline: nextTokenWillStartWithNewline)
274278

275-
return token.with(\.leadingTrivia, leadingTrivia).with(\.trailingTrivia, trailingTrivia)
279+
if leadingTrivia == token.leadingTrivia && trailingTrivia == token.trailingTrivia {
280+
return token
281+
}
282+
283+
return token.detach().with(\.leadingTrivia, leadingTrivia).with(\.trailingTrivia, trailingTrivia)
276284
}
277285
}

Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ extension FixIt.MultiNodeChange {
127127
let previousToken = node.previousToken(viewMode: .fixedUp),
128128
previousToken.presence == .present,
129129
previousToken.trailingTrivia.isEmpty,
130-
BasicFormat().requiresTrailingWhitespace(previousToken),
130+
BasicFormat().requiresWhitespace(between: previousToken, and: node.firstToken(viewMode: .fixedUp)),
131131
leadingTrivia == nil
132132
{
133133
/// If neither this nor the previous token are punctionation make sure they

Sources/_SwiftSyntaxTestSupport/SyntaxProtocol+Initializer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import SwiftSyntaxBuilder
1616

1717
private class InitializerExprFormat: BasicFormat {
1818
public init() {
19-
super.init(indentationIncrement: .spaces(2))
19+
super.init(indentationWidth: .spaces(2))
2020
}
2121

2222
private func formatChildrenSeparatedByNewline<SyntaxType: SyntaxProtocol>(children: SyntaxChildren, elementType: SyntaxType.Type) -> [SyntaxType] {

Tests/SwiftSyntaxBuilderTest/StringInterpolationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import XCTest
2020

2121
class TwoSpacesFormat: BasicFormat {
2222
public init() {
23-
super.init(indentationIncrement: .spaces(2))
23+
super.init(indentationWidth: .spaces(2))
2424
}
2525
}
2626

0 commit comments

Comments
 (0)