Skip to content

Commit 9323d08

Browse files
[SwiftSyntax] Add accessors for source locations and test diagnostic emission (#16141)
* [SwiftSyntax] Add accessors for source locations and test diagnostic emission * Add tests for endLocation * Pre-emptively copy AbsolutePosition to avoid mutating it twice
1 parent f7f9073 commit 9323d08

File tree

4 files changed

+118
-0
lines changed

4 files changed

+118
-0
lines changed

test/SwiftSyntax/DiagnosticTest.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ func loc(_ file: String = #file, line: Int = #line,
1212
return SourceLocation(line: line, column: column, offset: 0, file: file)
1313
}
1414

15+
func getInput(_ file: String) -> URL {
16+
var result = URL(fileURLWithPath: #file)
17+
result.deleteLastPathComponent()
18+
result.appendPathComponent("Inputs")
19+
result.appendPathComponent(file)
20+
return result
21+
}
22+
1523
/// Adds static constants to Diagnostic.Message.
1624
extension Diagnostic.Message {
1725
/// Error thrown when a conversion between two types is impossible.
@@ -24,6 +32,13 @@ extension Diagnostic.Message {
2432
/// Suggestion for the user to explicitly check a value does not equal zero.
2533
static let checkEqualToZero =
2634
Diagnostic.Message(.note, "check for explicit equality to '0'")
35+
36+
static func badFunction(_ name: TokenSyntax) -> Diagnostic.Message {
37+
return .init(.error, "bad function '\(name.text)'")
38+
}
39+
static func endOfFunction(_ name: TokenSyntax) -> Diagnostic.Message {
40+
return .init(.warning, "end of function '\(name.text)'")
41+
}
2742
}
2843

2944
var Diagnostics = TestSuite("Diagnostics")
@@ -58,4 +73,39 @@ Diagnostics.test("DiagnosticEmission") {
5873
expectEqual(fixIt.text, " != 0")
5974
}
6075

76+
Diagnostics.test("SourceLocations") {
77+
let engine = DiagnosticEngine()
78+
engine.addConsumer(PrintingDiagnosticConsumer())
79+
let url = getInput("diagnostics.swift")
80+
81+
class Visitor: SyntaxVisitor {
82+
let url: URL
83+
let engine: DiagnosticEngine
84+
init(url: URL, engine: DiagnosticEngine) {
85+
self.url = url
86+
self.engine = engine
87+
}
88+
override func visit(_ function: FunctionDeclSyntax) {
89+
let startLoc = function.identifier.startLocation(in: url)
90+
let endLoc = function.endLocation(in: url)
91+
print("\(function.identifier.text): startLoc: \(startLoc), endLoc: \(endLoc)")
92+
engine.diagnose(.badFunction(function.identifier), location: startLoc) {
93+
$0.highlight(function.identifier.sourceRange(in: self.url))
94+
}
95+
engine.diagnose(.endOfFunction(function.identifier), location: endLoc)
96+
}
97+
}
98+
99+
expectDoesNotThrow({
100+
let file = try SourceFileSyntax.parse(url)
101+
Visitor(url: url, engine: engine).visit(file)
102+
})
103+
104+
expectEqual(6, engine.diagnostics.count)
105+
let lines = Set(engine.diagnostics.compactMap { $0.location?.line })
106+
expectEqual([1, 3, 5, 7, 9, 11], lines)
107+
let columns = Set(engine.diagnostics.compactMap { $0.location?.column })
108+
expectEqual([6, 2], columns)
109+
}
110+
61111
runAllTests()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
func foo() {
2+
3+
}
4+
5+
func bar() {
6+
7+
}
8+
9+
func baz() {
10+
11+
}

tools/SwiftSyntax/RawSyntax.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@ extension RawSyntax {
251251
}
252252
}
253253

254+
func accumulateTrailingTrivia(_ pos: AbsolutePosition) {
255+
guard let trivia = trailingTrivia else { return }
256+
for piece in trivia {
257+
piece.accumulateAbsolutePosition(pos)
258+
}
259+
}
260+
254261
var isSourceFile: Bool {
255262
switch self {
256263
case .node(let kind, _, _):

tools/SwiftSyntax/Syntax.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,56 @@ extension Syntax {
202202
where Target: TextOutputStream {
203203
data.raw.write(to: &target)
204204
}
205+
206+
/// The starting location, in the provided file, of this Syntax node.
207+
/// - Parameters:
208+
/// - file: The file URL this node resides in.
209+
/// - afterLeadingTrivia: Whether to skip leading trivia when getting
210+
/// the node's location. Defaults to `true`.
211+
public func startLocation(
212+
in file: URL,
213+
afterLeadingTrivia: Bool = true
214+
) -> SourceLocation {
215+
let pos = afterLeadingTrivia ?
216+
data.position.copy() :
217+
data.positionAfterSkippingLeadingTrivia.copy()
218+
return SourceLocation(file: file.path, position: pos)
219+
}
220+
221+
222+
/// The ending location, in the provided file, of this Syntax node.
223+
/// - Parameters:
224+
/// - file: The file URL this node resides in.
225+
/// - afterTrailingTrivia: Whether to skip trailing trivia when getting
226+
/// the node's location. Defaults to `false`.
227+
public func endLocation(
228+
in file: URL,
229+
afterTrailingTrivia: Bool = false
230+
) -> SourceLocation {
231+
let pos = data.position.copy()
232+
raw.accumulateAbsolutePosition(pos)
233+
if afterTrailingTrivia {
234+
raw.accumulateTrailingTrivia(pos)
235+
}
236+
return SourceLocation(file: file.path, position: pos)
237+
}
238+
239+
/// The source range, in the provided file, of this Syntax node.
240+
/// - Parameters:
241+
/// - file: The file URL this node resides in.
242+
/// - afterLeadingTrivia: Whether to skip leading trivia when getting
243+
/// the node's start location. Defaults to `true`.
244+
/// - afterTrailingTrivia: Whether to skip trailing trivia when getting
245+
/// the node's end location. Defaults to `false`.
246+
public func sourceRange(
247+
in file: URL,
248+
afterLeadingTrivia: Bool = true,
249+
afterTrailingTrivia: Bool = false
250+
) -> SourceRange {
251+
let start = startLocation(in: file, afterLeadingTrivia: afterLeadingTrivia)
252+
let end = endLocation(in: file, afterTrailingTrivia: afterTrailingTrivia)
253+
return SourceRange(start: start, end: end)
254+
}
205255
}
206256

207257
/// Determines if two nodes are equal to each other.

0 commit comments

Comments
 (0)