Skip to content

Commit ca31078

Browse files
committed
Respect #sourceLocation directives in SourceLocationConverter
Add a `presumedFile` and `presumedLine` property to `SourceLocation` that contains the file and line of the location while taking `#sourceLocation` directives into account. The terms “presumed file” and “presumed line” have been taken from LLVM and the Swift compiler. rdar://99187174
1 parent 1076504 commit ca31078

File tree

5 files changed

+223
-13
lines changed

5 files changed

+223
-13
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ let package = Package(
144144

145145
.testTarget(
146146
name: "SwiftSyntaxTest",
147-
dependencies: ["_SwiftSyntaxTestSupport", "SwiftSyntax"]
147+
dependencies: ["_SwiftSyntaxTestSupport", "SwiftSyntax", "SwiftSyntaxBuilder"]
148148
),
149149

150150
// MARK: SwiftSyntaxBuilder

Sources/SwiftSyntax/SourceLocation.swift

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,39 @@
1313
/// Represents a source location in a Swift file.
1414
public struct SourceLocation: Hashable, Codable, CustomDebugStringConvertible {
1515

16-
/// The UTF-8 byte offset into the file where this location resides.
17-
public let offset: Int
18-
1916
/// The line in the file where this location resides. 1-based.
17+
///
18+
/// ### See also
19+
/// ``SourceLocation/presumedLine``
2020
public var line: Int
2121

2222
/// The UTF-8 byte offset from the beginning of the line where this location
2323
/// resides. 1-based.
2424
public let column: Int
2525

26+
/// The UTF-8 byte offset into the file where this location resides.
27+
public let offset: Int
28+
2629
/// The file in which this location resides.
30+
///
31+
/// ### See also
32+
/// ``SourceLocation/presumedFile``
2733
public let file: String
2834

35+
/// The line of this location when respecting `#sourceLocation` directives.
36+
///
37+
/// If the location hasn’t been adjusted using `#sourceLocation` directives,
38+
/// this is the same as `file`.
39+
public let presumedLine: Int
40+
41+
/// The file in which the the location resides when respecting `#sourceLocation`
42+
/// directives.
43+
///
44+
/// If the location has been adjusted using `#sourceLocation` directives, this
45+
/// is the file mentioned in the last `#sourceLocation` directive before this
46+
/// location, otherwise this is the same as `file`.
47+
public let presumedFile: String
48+
2949
/// Returns the location as `<line>:<column>` for debugging purposes.
3050
/// Do not rely on this output being stable.
3151
public var debugDescription: String {
@@ -47,11 +67,26 @@ public struct SourceLocation: Hashable, Codable, CustomDebugStringConvertible {
4767
/// location in the source file has `offset` 0.
4868
/// - file: A string describing the name of the file in which this location
4969
/// is contained.
50-
public init(line: Int, column: Int, offset: Int, file: String) {
70+
/// - presumedLine: If the location has been adjusted using `#sourceLocation`
71+
/// directives, the adjusted line. If `nil`, this defaults to
72+
/// `line`.
73+
/// - presumedFile: If the location has been adjusted using `#sourceLocation`
74+
/// directives, the adjusted file. If `nil`, this defaults to
75+
/// `file`.
76+
public init(
77+
line: Int,
78+
column: Int,
79+
offset: Int,
80+
file: String,
81+
presumedLine: Int? = nil,
82+
presumedFile: String? = nil
83+
) {
5184
self.line = line
5285
self.offset = offset
5386
self.column = column
5487
self.file = file
88+
self.presumedLine = presumedLine ?? line
89+
self.presumedFile = presumedFile ?? file
5590
}
5691
}
5792

@@ -83,6 +118,22 @@ public struct SourceRange: Hashable, Codable, CustomDebugStringConvertible {
83118
}
84119
}
85120

121+
/// Collects all `PoundSourceLocationSyntax` directives in a file.
122+
fileprivate class SourceLocationCollector: SyntaxVisitor {
123+
private var sourceLocationDirectives: [PoundSourceLocationSyntax] = []
124+
125+
override func visit(_ node: PoundSourceLocationSyntax) -> SyntaxVisitorContinueKind {
126+
sourceLocationDirectives.append(node)
127+
return .skipChildren
128+
}
129+
130+
static func collectSourceLocations(in tree: some SyntaxProtocol) -> [PoundSourceLocationSyntax] {
131+
let collector = SourceLocationCollector(viewMode: .sourceAccurate)
132+
collector.walk(tree)
133+
return collector.sourceLocationDirectives
134+
}
135+
}
136+
86137
/// Converts ``AbsolutePosition``s of syntax nodes to ``SourceLocation``s, and
87138
/// vice-versa. The ``AbsolutePosition``s must be originating from nodes that are
88139
/// part of the same tree that was used to initialize this class.
@@ -95,6 +146,15 @@ public final class SourceLocationConverter {
95146
/// Position at end of file.
96147
let endOfFile: AbsolutePosition
97148

149+
/// The information from all `#sourceLocation` directives in the file
150+
/// necessary to compute presumed locations.
151+
///
152+
/// - `sourceLine` is the line at which the `#sourceLocation` statement occurs
153+
/// within the current file.
154+
/// - `fileArgument` is the `file` argument of the `#sourceLocation` directive.
155+
/// - `lineArgument` is the `line` argument of the `#sourceLocation` directive.
156+
var sourceLocationDirectives: [(sourceLine: Int, arguments: (file: String, line: Int)?)] = []
157+
98158
/// - Parameters:
99159
/// - file: The file path associated with the syntax tree.
100160
/// - tree: The root of the syntax tree to convert positions to line/columns for.
@@ -104,11 +164,31 @@ public final class SourceLocationConverter {
104164
self.source = tree.syntaxTextBytes
105165
(self.lines, endOfFile) = computeLines(tree: Syntax(tree))
106166
precondition(tree.byteSize == endOfFile.utf8Offset)
167+
168+
for directive in SourceLocationCollector.collectSourceLocations(in: tree) {
169+
let location = self.physicalLocation(for: directive.positionAfterSkippingLeadingTrivia)
170+
if let args = directive.args {
171+
let logicalFile = args.fileName.segments.description
172+
guard let logicalLine = Int(args.lineNumber.description) else {
173+
// FIXME: Handle hex line numbers
174+
continue
175+
}
176+
sourceLocationDirectives.append((sourceLine: location.line, arguments: (file: logicalFile, line: logicalLine)))
177+
} else {
178+
// `#sourceLocation()` without any arguments resets the `#sourceLocation` directive.
179+
sourceLocationDirectives.append((sourceLine: location.line, arguments: nil))
180+
}
181+
}
107182
}
108183

184+
/// - Important: This initializer does not take `#sourceLocation` directives
185+
/// into account and doesn’t produce `presumedFile` and
186+
/// `presumedLine`.
187+
///
109188
/// - Parameters:
110189
/// - file: The file path associated with the syntax tree.
111190
/// - source: The source code to convert positions to line/columns for.
191+
@available(*, deprecated, message: "Use init(file:tree:) instead")
112192
public init(file: String, source: String) {
113193
self.file = file
114194
self.source = Array(source.utf8)
@@ -145,13 +225,40 @@ public final class SourceLocationConverter {
145225
}
146226
}
147227

148-
/// Convert a ``AbsolutePosition`` to a ``SourceLocation``. If the position is
228+
/// Convert a ``AbsolutePosition`` to a ``SourceLocation``.
229+
///
230+
/// If the position is exceeding the file length then the ``SourceLocation``
231+
/// for the end of file is returned. If position is negative the location for
232+
/// start of file is returned.
233+
public func location(for position: AbsolutePosition) -> SourceLocation {
234+
let physicalLocation = physicalLocation(for: position)
235+
if let lastSourceLocationDirective = sourceLocationDirectives.last(where: { $0.sourceLine < physicalLocation.line }),
236+
let arguments = lastSourceLocationDirective.arguments
237+
{
238+
let presumedLine = arguments.line + physicalLocation.line - lastSourceLocationDirective.sourceLine - 1
239+
return SourceLocation(
240+
line: physicalLocation.line,
241+
column: physicalLocation.column,
242+
offset: physicalLocation.offset,
243+
file: physicalLocation.file,
244+
presumedLine: presumedLine,
245+
presumedFile: arguments.file
246+
)
247+
}
248+
249+
return physicalLocation
250+
}
251+
252+
/// Compute the location of `position` without taking `#sourceLocation`
253+
/// directives into account.
254+
///
255+
/// If the position is
149256
/// exceeding the file length then the ``SourceLocation`` for the end of file
150257
/// is returned. If position is negative the location for start of file is
151258
/// returned.
152-
public func location(for origpos: AbsolutePosition) -> SourceLocation {
259+
private func physicalLocation(for position: AbsolutePosition) -> SourceLocation {
153260
// Clamp the given position to the end of file if needed.
154-
let pos = min(origpos, endOfFile)
261+
let pos = min(position, endOfFile)
155262
if pos.utf8Offset < 0 {
156263
return SourceLocation(line: 1, column: 1, offset: 0, file: self.file)
157264
}

Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func assertNote(
6565
expected spec: NoteSpec
6666
) {
6767
assertStringsEqualWithDiff(note.message, spec.message, "message of note does not match", file: spec.originatorFile, line: spec.originatorLine)
68-
let location = note.location(converter: SourceLocationConverter(file: "", source: tree.description))
68+
let location = note.location(converter: SourceLocationConverter(file: "", tree: tree))
6969
XCTAssertEqual(location.line, spec.line, "line of note does not match", file: spec.originatorFile, line: spec.originatorLine)
7070
XCTAssertEqual(location.column, spec.column, "column of note does not match", file: spec.originatorFile, line: spec.originatorLine)
7171
}
@@ -187,7 +187,7 @@ func assertDiagnostic(
187187
XCTAssertEqual(diag.diagnosticID, id, "diagnostic ID does not match", file: spec.originatorFile, line: spec.originatorLine)
188188
}
189189
assertStringsEqualWithDiff(diag.message, spec.message, "message does not match", file: spec.originatorFile, line: spec.originatorLine)
190-
let location = diag.location(converter: SourceLocationConverter(file: "", source: tree.description))
190+
let location = diag.location(converter: SourceLocationConverter(file: "", tree: tree))
191191
XCTAssertEqual(location.line, spec.line, "line does not match", file: spec.originatorFile, line: spec.originatorLine)
192192
XCTAssertEqual(location.column, spec.column, "column does not match", file: spec.originatorFile, line: spec.originatorLine)
193193

Tests/SwiftParserTest/Assertions.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ func assertLocation<T: SyntaxProtocol>(
331331
line: UInt = #line
332332
) {
333333
if let markerLoc = markerLocations[locationMarker] {
334-
let locationConverter = SourceLocationConverter(file: "", source: tree.description)
334+
let locationConverter = SourceLocationConverter(file: "", tree: tree)
335335
let actualLocation = location
336336
let expectedLocation = locationConverter.location(for: AbsolutePosition(utf8Offset: markerLoc))
337337
if actualLocation.line != expectedLocation.line || actualLocation.column != expectedLocation.column {
@@ -355,7 +355,7 @@ func assertNote<T: SyntaxProtocol>(
355355
expected spec: NoteSpec
356356
) {
357357
XCTAssertEqual(note.message, spec.message, file: spec.file, line: spec.line)
358-
let locationConverter = SourceLocationConverter(file: "", source: tree.description)
358+
let locationConverter = SourceLocationConverter(file: "", tree: tree)
359359
assertLocation(
360360
note.location(converter: locationConverter),
361361
in: tree,
@@ -374,7 +374,7 @@ func assertDiagnostic<T: SyntaxProtocol>(
374374
markerLocations: [String: Int],
375375
expected spec: DiagnosticSpec
376376
) {
377-
let locationConverter = SourceLocationConverter(file: "", source: tree.description)
377+
let locationConverter = SourceLocationConverter(file: "", tree: tree)
378378
assertLocation(
379379
diag.location(converter: locationConverter),
380380
in: tree,

Tests/SwiftSyntaxTest/SourceLocationConverterTests.swift

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@
1212

1313
import XCTest
1414
@_spi(RawSyntax) import SwiftSyntax
15+
import SwiftSyntaxBuilder
16+
17+
fileprivate func assertLogicalSourceLocation(
18+
_ source: SourceFileSyntax,
19+
inspectionItemFilter: (CodeBlockItemSyntax.Item) -> (some SyntaxProtocol)? = { $0.as(VariableDeclSyntax.self) },
20+
presumedFile: String,
21+
presumedLine: Int,
22+
file: StaticString = #file,
23+
line: UInt = #line
24+
) {
25+
let converter = SourceLocationConverter(file: "input.swift", tree: source)
26+
27+
guard let variableDecl = source.statements.compactMap({ inspectionItemFilter($0.item) }).first else {
28+
XCTFail("Could not find a node that matches the `inspectionItemFilter` in `source`", file: file, line: line)
29+
return
30+
}
31+
let location = converter.location(for: variableDecl.positionAfterSkippingLeadingTrivia)
32+
XCTAssertEqual(presumedFile, location.presumedFile, "presumed file did not match", file: file, line: line)
33+
XCTAssertEqual(presumedLine, location.presumedLine, "presumed line did not match", file: file, line: line)
34+
}
1535

1636
final class SourceLocationConverterTests: XCTestCase {
1737
func testInvalidUtf8() {
@@ -43,4 +63,87 @@ final class SourceLocationConverterTests: XCTestCase {
4363
// ```
4464
_ = SourceLocationConverter(file: "", tree: tree)
4565
}
66+
67+
func testSingleSourceLocationDirective() {
68+
assertLogicalSourceLocation(
69+
"""
70+
#sourceLocation(file: "other.swift", line: 1)
71+
let a = 2
72+
""",
73+
presumedFile: "other.swift",
74+
presumedLine: 1
75+
)
76+
77+
assertLogicalSourceLocation(
78+
"""
79+
#sourceLocation(file: "other.swift", line: 3)
80+
let a = 2
81+
""",
82+
presumedFile: "other.swift",
83+
presumedLine: 3
84+
)
85+
86+
assertLogicalSourceLocation(
87+
"""
88+
#sourceLocation(file: "other.swift", line: 4)
89+
func foo() {
90+
}
91+
let a = 2
92+
""",
93+
presumedFile: "other.swift",
94+
presumedLine: 6
95+
)
96+
97+
assertLogicalSourceLocation(
98+
"""
99+
func foo() {
100+
print(1)
101+
}
102+
#sourceLocation(file: "other.swift", line: 1)
103+
let a = 2
104+
""",
105+
presumedFile: "other.swift",
106+
presumedLine: 1
107+
)
108+
}
109+
110+
func testMultipleSourceLocationDirectives() {
111+
assertLogicalSourceLocation(
112+
"""
113+
#sourceLocation(file: "other.swift", line: 10)
114+
115+
let a = 2
116+
117+
#sourceLocation(file: "andAnother.swift", line: 20)
118+
""",
119+
presumedFile: "other.swift",
120+
presumedLine: 11
121+
)
122+
123+
assertLogicalSourceLocation(
124+
"""
125+
#sourceLocation(file: "other.swift", line: 10)
126+
127+
#sourceLocation(file: "andAnother.swift", line: 20)
128+
129+
let a = 2
130+
""",
131+
presumedFile: "andAnother.swift",
132+
presumedLine: 21
133+
)
134+
}
135+
136+
func testResetSourceLocationDirective() {
137+
assertLogicalSourceLocation(
138+
"""
139+
#sourceLocation(file: "other.swift", line: 10)
140+
141+
#sourceLocation()
142+
143+
let a = 2
144+
""",
145+
presumedFile: "input.swift",
146+
presumedLine: 5
147+
)
148+
}
46149
}

0 commit comments

Comments
 (0)