Skip to content

Commit 83ee689

Browse files
authored
Merge pull request #2663 from rintaro/perf-computelines
[SourceLocationConverter] Performance improvement
2 parents 76fb4cf + 6a81e49 commit 83ee689

File tree

2 files changed

+91
-50
lines changed

2 files changed

+91
-50
lines changed

Sources/SwiftSyntax/SourceLocation.swift

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -103,22 +103,6 @@ public struct SourceRange: Hashable, Codable, Sendable {
103103
}
104104
}
105105

106-
/// Collects all `PoundSourceLocationSyntax` directives in a file.
107-
fileprivate class SourceLocationCollector: SyntaxVisitor {
108-
private var sourceLocationDirectives: [PoundSourceLocationSyntax] = []
109-
110-
override func visit(_ node: PoundSourceLocationSyntax) -> SyntaxVisitorContinueKind {
111-
sourceLocationDirectives.append(node)
112-
return .skipChildren
113-
}
114-
115-
static func collectSourceLocations(in tree: some SyntaxProtocol) -> [PoundSourceLocationSyntax] {
116-
let collector = SourceLocationCollector(viewMode: .sourceAccurate)
117-
collector.walk(tree)
118-
return collector.sourceLocationDirectives
119-
}
120-
}
121-
122106
fileprivate struct SourceLocationDirectiveArguments {
123107
enum Error: Swift.Error, CustomStringConvertible {
124108
case nonDecimalLineNumber(TokenSyntax)
@@ -169,8 +153,8 @@ public final class SourceLocationConverter {
169153
/// The information from all `#sourceLocation` directives in the file
170154
/// necessary to compute presumed locations.
171155
///
172-
/// - `sourceLine` is the line at which the `#sourceLocation` statement occurs
173-
/// within the current file.
156+
/// - `sourceLine` is the physical line number of the end of the last token of
157+
/// `#sourceLocation(...)` directive within the current file.
174158
/// - `arguments` are the `file` and `line` arguments of the directive or `nil`
175159
/// if spelled as `#sourceLocation()` to reset the source location directive.
176160
private var sourceLocationDirectives: [(sourceLine: Int, arguments: SourceLocationDirectiveArguments?)] = []
@@ -189,21 +173,7 @@ public final class SourceLocationConverter {
189173
precondition(tree.parent == nil, "SourceLocationConverter must be passed the root of the syntax tree")
190174
self.fileName = fileName
191175
self.source = tree.syntaxTextBytes
192-
(self.lines, endOfFile) = computeLines(tree: Syntax(tree))
193-
precondition(tree.totalLength.utf8Length == endOfFile.utf8Offset)
194-
195-
for directive in SourceLocationCollector.collectSourceLocations(in: tree) {
196-
let location = self.physicalLocation(for: directive.positionAfterSkippingLeadingTrivia)
197-
if let args = directive.arguments {
198-
if let parsedArgs = try? SourceLocationDirectiveArguments(args) {
199-
// Ignore any malformed `#sourceLocation` directives.
200-
sourceLocationDirectives.append((sourceLine: location.line, arguments: parsedArgs))
201-
}
202-
} else {
203-
// `#sourceLocation()` without any arguments resets the `#sourceLocation` directive.
204-
sourceLocationDirectives.append((sourceLine: location.line, arguments: nil))
205-
}
206-
}
176+
(self.lines, self.endOfFile, self.sourceLocationDirectives) = computeLines(tree: Syntax(tree))
207177
}
208178

209179
/// Create a new ``SourceLocationConverter`` to convert between ``AbsolutePosition``
@@ -511,21 +481,21 @@ extension SyntaxProtocol {
511481
/// the end-of-file position.
512482
fileprivate func computeLines(
513483
tree: Syntax
514-
) -> ([AbsolutePosition], AbsolutePosition) {
515-
var lines: [AbsolutePosition] = []
516-
// First line starts from the beginning.
517-
lines.append(.startOfFile)
484+
) -> (
485+
lines: [AbsolutePosition],
486+
endOfFile: AbsolutePosition,
487+
sourceLocationDirectives: [(sourceLine: Int, arguments: SourceLocationDirectiveArguments?)]
488+
) {
489+
var lines: [AbsolutePosition] = [.startOfFile]
518490
var position: AbsolutePosition = .startOfFile
519-
let addLine = { (lineLength: SourceLength) in
491+
var sourceLocationDirectives: [(sourceLine: Int, arguments: SourceLocationDirectiveArguments?)] = []
492+
let lastLineLength = tree.raw.forEachLineLength { lineLength in
520493
position += lineLength
521494
lines.append(position)
495+
} handleSourceLocationDirective: { lineOffset, args in
496+
sourceLocationDirectives.append((sourceLine: lines.count + lineOffset, arguments: args))
522497
}
523-
var curPrefix: SourceLength = .zero
524-
for token in tree.tokens(viewMode: .sourceAccurate) {
525-
curPrefix = token.forEachLineLength(prefix: curPrefix, body: addLine)
526-
}
527-
position += curPrefix
528-
return (lines, position)
498+
return (lines, position + lastLineLength, sourceLocationDirectives)
529499
}
530500

531501
fileprivate func computeLines(_ source: SyntaxText) -> ([AbsolutePosition], AbsolutePosition) {
@@ -636,7 +606,7 @@ fileprivate extension RawTriviaPiece {
636606
}
637607
}
638608

639-
fileprivate extension Array where Element == RawTriviaPiece {
609+
fileprivate extension RawTriviaPieceBuffer {
640610
/// Walks and passes to `body` the ``SourceLength`` for every detected line,
641611
/// with the newline character included.
642612
/// - Returns: The leftover ``SourceLength`` at the end of the walk.
@@ -652,18 +622,61 @@ fileprivate extension Array where Element == RawTriviaPiece {
652622
}
653623
}
654624

655-
fileprivate extension TokenSyntax {
625+
fileprivate extension RawSyntax {
656626
/// Walks and passes to `body` the ``SourceLength`` for every detected line,
657627
/// with the newline character included.
658628
/// - Returns: The leftover ``SourceLength`` at the end of the walk.
659629
func forEachLineLength(
660630
prefix: SourceLength = .zero,
661-
body: (SourceLength) -> ()
631+
body: (SourceLength) -> (),
632+
handleSourceLocationDirective: (_ lineOffset: Int, _ arguments: SourceLocationDirectiveArguments?) -> ()
662633
) -> SourceLength {
663634
var curPrefix = prefix
664-
curPrefix = self.tokenView.leadingRawTriviaPieces.forEachLineLength(prefix: curPrefix, body: body)
665-
curPrefix = self.tokenView.rawText.forEachLineLength(prefix: curPrefix, body: body)
666-
curPrefix = self.tokenView.trailingRawTriviaPieces.forEachLineLength(prefix: curPrefix, body: body)
635+
switch self.rawData.payload {
636+
case .parsedToken(let dat):
637+
curPrefix = dat.wholeText.forEachLineLength(prefix: curPrefix, body: body)
638+
case .materializedToken(let dat):
639+
curPrefix = dat.leadingTrivia.forEachLineLength(prefix: curPrefix, body: body)
640+
curPrefix = dat.tokenText.forEachLineLength(prefix: curPrefix, body: body)
641+
curPrefix = dat.trailingTrivia.forEachLineLength(prefix: curPrefix, body: body)
642+
case .layout(let dat):
643+
for case let node? in dat.layout where SyntaxTreeViewMode.sourceAccurate.shouldTraverse(node: node) {
644+
curPrefix = node.forEachLineLength(
645+
prefix: curPrefix,
646+
body: body,
647+
handleSourceLocationDirective: handleSourceLocationDirective
648+
)
649+
}
650+
651+
// Handle '#sourceLocation' directive.
652+
if dat.kind == .poundSourceLocation {
653+
// Count newlines in the trailing trivia. The client want to get the
654+
// line of the _end_ of '#sourceLocation()' directive.
655+
var lineOffset = 0
656+
if let lastTok = self.lastToken(viewMode: .sourceAccurate) {
657+
switch lastTok.raw.rawData.payload {
658+
case .parsedToken(let dat):
659+
_ = dat.trailingTriviaText.forEachLineLength(body: { _ in lineOffset -= 1 })
660+
case .materializedToken(let dat):
661+
_ = dat.trailingTrivia.forEachLineLength(body: { _ in lineOffset -= 1 })
662+
case .layout(_):
663+
preconditionFailure("lastToken(viewMode:) returned non-token")
664+
}
665+
}
666+
667+
let directive = Syntax.forRoot(self, rawNodeArena: self.arenaReference.retained)
668+
.cast(PoundSourceLocationSyntax.self)
669+
if let args = directive.arguments {
670+
if let parsedArgs = try? SourceLocationDirectiveArguments(args) {
671+
// Ignore any malformed `#sourceLocation` directives.
672+
handleSourceLocationDirective(lineOffset, parsedArgs)
673+
}
674+
} else {
675+
// `#sourceLocation()` without any arguments resets the `#sourceLocation` directive.
676+
handleSourceLocationDirective(lineOffset, nil)
677+
}
678+
}
679+
}
667680
return curPrefix
668681
}
669682
}

Tests/SwiftSyntaxTest/SourceLocationConverterTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,34 @@ final class SourceLocationConverterTests: XCTestCase {
174174
)
175175
}
176176

177+
func testMultiLineDirective() {
178+
assertPresumedSourceLocation(
179+
#"""
180+
#sourceLocation(
181+
file: "input.swift",
182+
line: 10
183+
)
184+
185+
let a = 2
186+
"""#,
187+
presumedFile: "input.swift",
188+
presumedLine: 11
189+
)
190+
}
191+
192+
func testDirectiveWithTrailingBlockComment() {
193+
assertPresumedSourceLocation(
194+
#"""
195+
#sourceLocation(file: "input.swift", line: 10) /*
196+
comment
197+
*/
198+
199+
let a = 2
200+
"""#,
201+
presumedFile: "input.swift",
202+
presumedLine: 13
203+
)
204+
}
177205
func testMultiLineStringLiteralAsFilename() {
178206
// FIXME: The current parser handles this fine but it’s a really bogus filename.
179207
// We ignore the directive because the multi-line string literal contains multiple segments.

0 commit comments

Comments
 (0)