Skip to content

Commit 6a81e49

Browse files
committed
[SourceLocaitonConverter] Collect '#sourceLocationDirective' in 1 pass
1 parent 34e7bb4 commit 6a81e49

File tree

2 files changed

+76
-38
lines changed

2 files changed

+76
-38
lines changed

Sources/SwiftSyntax/SourceLocation.swift

Lines changed: 48 additions & 38 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,15 +481,21 @@ extension SyntaxProtocol {
511481
/// the end-of-file position.
512482
fileprivate func computeLines(
513483
tree: Syntax
514-
) -> ([AbsolutePosition], AbsolutePosition) {
484+
) -> (
485+
lines: [AbsolutePosition],
486+
endOfFile: AbsolutePosition,
487+
sourceLocationDirectives: [(sourceLine: Int, arguments: SourceLocationDirectiveArguments?)]
488+
) {
515489
var lines: [AbsolutePosition] = [.startOfFile]
516490
var position: AbsolutePosition = .startOfFile
517-
491+
var sourceLocationDirectives: [(sourceLine: Int, arguments: SourceLocationDirectiveArguments?)] = []
518492
let lastLineLength = tree.raw.forEachLineLength { lineLength in
519493
position += lineLength
520494
lines.append(position)
495+
} handleSourceLocationDirective: { lineOffset, args in
496+
sourceLocationDirectives.append((sourceLine: lines.count + lineOffset, arguments: args))
521497
}
522-
return (lines, position + lastLineLength)
498+
return (lines, position + lastLineLength, sourceLocationDirectives)
523499
}
524500

525501
fileprivate func computeLines(_ source: SyntaxText) -> ([AbsolutePosition], AbsolutePosition) {
@@ -652,7 +628,8 @@ fileprivate extension RawSyntax {
652628
/// - Returns: The leftover ``SourceLength`` at the end of the walk.
653629
func forEachLineLength(
654630
prefix: SourceLength = .zero,
655-
body: (SourceLength) -> ()
631+
body: (SourceLength) -> (),
632+
handleSourceLocationDirective: (_ lineOffset: Int, _ arguments: SourceLocationDirectiveArguments?) -> ()
656633
) -> SourceLength {
657634
var curPrefix = prefix
658635
switch self.rawData.payload {
@@ -664,7 +641,40 @@ fileprivate extension RawSyntax {
664641
curPrefix = dat.trailingTrivia.forEachLineLength(prefix: curPrefix, body: body)
665642
case .layout(let dat):
666643
for case let node? in dat.layout where SyntaxTreeViewMode.sourceAccurate.shouldTraverse(node: node) {
667-
curPrefix = node.forEachLineLength(prefix: curPrefix, body: body)
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+
}
668678
}
669679
}
670680
return curPrefix

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)