Skip to content

Commit fab4998

Browse files
committed
Diagnostics: teach the parser API to listen to diagnostics emitted from the compiler(parser).
This patch adds a parameter of DiagnosticConsumer to the parser entry point to capture any diagnostics emitted from the compiler(parser). We also convert the c structures collected from the compiler side to an existing Diagnostic type in SwiftSyntax to unify the API. To avoid unnecessarily calculating line/column from a utf8 offset, a property is added to DiagnosticConsumer to indicate whether the users want line/column; by default they are calculated. rdar://48439271
1 parent 5dc1f31 commit fab4998

File tree

6 files changed

+216
-11
lines changed

6 files changed

+216
-11
lines changed

Sources/SwiftSyntax/Diagnostic.swift

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
/// A FixIt represents a change to source code in order to "correct" a
1616
/// diagnostic.
17-
public enum FixIt: Codable {
17+
public enum FixIt: Codable, CustomDebugStringConvertible {
18+
1819
/// Remove the characters from the source file over the provided source range.
1920
case remove(SourceRange)
2021

@@ -32,6 +33,10 @@ public enum FixIt: Codable {
3233
case string
3334
}
3435

36+
public var debugDescription: String {
37+
return "Fixit: \(range.debugDescription) Text: \"\(text)\""
38+
}
39+
3540
public init(from decoder: Decoder) throws {
3641
let container = try decoder.container(keyedBy: CodingKeys.self)
3742
let type = try container.decode(String.self, forKey: .type)
@@ -119,8 +124,14 @@ public struct Note: Codable {
119124
}
120125

121126
/// A Diagnostic message that can be emitted regarding some piece of code.
122-
public struct Diagnostic: Codable {
123-
public struct Message: Codable {
127+
public struct Diagnostic: Codable, CustomDebugStringConvertible {
128+
129+
public struct Message: Codable, CustomDebugStringConvertible {
130+
131+
public var debugDescription: String {
132+
return "\(severity): \(text)"
133+
}
134+
124135
/// The severity of diagnostic. This can be note, error, or warning.
125136
public let severity: Severity
126137

@@ -157,6 +168,18 @@ public struct Diagnostic: Codable {
157168
/// An array of possible FixIts to apply to this diagnostic.
158169
public let fixIts: [FixIt]
159170

171+
public var debugDescription: String {
172+
var lines: [String] = []
173+
if let location = location {
174+
lines.append("\(location) \(message.debugDescription)")
175+
} else {
176+
lines.append("\(message.debugDescription)")
177+
}
178+
fixIts.forEach { lines.append("\($0.debugDescription)") }
179+
highlights.forEach { lines.append("Hightlight: \($0.debugDescription)") }
180+
return lines.joined(separator: "\n")
181+
}
182+
160183
/// A diagnostic builder that exposes mutating operations for notes,
161184
/// highlights, and FixIts. When a Diagnostic is created, a builder
162185
/// will be provided in a closure where the user can conditionally

Sources/SwiftSyntax/DiagnosticConsumer.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,19 @@
1515
/// An object that intends to receive notifications when diagnostics are
1616
/// emitted.
1717
public protocol DiagnosticConsumer {
18+
19+
/// Whether the collected diagnostics should calculate line:column pair; true
20+
/// by default.
21+
var calculateLineColumn: Bool { get }
22+
1823
/// Handle the provided diagnostic which has just been registered with the
1924
/// DiagnosticEngine.
2025
func handle(_ diagnostic: Diagnostic)
2126

2227
/// Finalize the consumption of diagnostics, flushing to disk if necessary.
2328
func finalize()
2429
}
30+
31+
public extension DiagnosticConsumer {
32+
var calculateLineColumn: Bool { return true }
33+
}

Sources/SwiftSyntax/SourceLocation.swift

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
/// Represent the user-facing part of SourceLocation that can be calculated
1414
/// on demand.
15-
struct ComputedLocation: Codable {
15+
struct ComputedLocation: Codable, CustomDebugStringConvertible {
1616
/// The line in the file where this location resides. 1-based.
1717
let line: Int
1818

@@ -23,6 +23,11 @@ struct ComputedLocation: Codable {
2323
/// The file in which this location resides.
2424
let file: String
2525

26+
var debugDescription: String {
27+
// Print file name?
28+
return "\(line):\(column)"
29+
}
30+
2631
init(line: Int, column: Int, file: String) {
2732
self.line = line
2833
self.column = column
@@ -38,7 +43,7 @@ struct ComputedLocation: Codable {
3843
}
3944

4045
/// Represents a source location in a Swift file.
41-
public struct SourceLocation: Codable {
46+
public struct SourceLocation: Codable, CustomDebugStringConvertible {
4247

4348
/// Line and column that can be computed on demand.
4449
private var compLoc: ComputedLocation?
@@ -62,6 +67,13 @@ public struct SourceLocation: Codable {
6267
return compLoc?.file
6368
}
6469

70+
public var debugDescription: String {
71+
guard let compLoc = compLoc else {
72+
return "\(offset)"
73+
}
74+
return compLoc.debugDescription
75+
}
76+
6577
public init(line: Int, column: Int, offset: Int, file: String) {
6678
self.offset = offset
6779
self.compLoc = ComputedLocation(line: line, column: column, file: file)
@@ -79,13 +91,18 @@ public struct SourceLocation: Codable {
7991
}
8092

8193
/// Represents a start and end location in a Swift file.
82-
public struct SourceRange: Codable {
94+
public struct SourceRange: Codable, CustomDebugStringConvertible {
95+
8396
/// The beginning location in the source range.
8497
public let start: SourceLocation
8598

8699
/// The beginning location in the source range.
87100
public let end: SourceLocation
88101

102+
public var debugDescription: String {
103+
return "(\(start.debugDescription),\(end.debugDescription))"
104+
}
105+
89106
public init(start: SourceLocation, end: SourceLocation) {
90107
self.start = start
91108
self.end = end
@@ -111,6 +128,15 @@ public final class SourceLocationConverter {
111128
assert(tree.byteSize == endOfFile.utf8Offset)
112129
}
113130

131+
/// - Parameters:
132+
/// - file: The file path associated with the syntax tree.
133+
/// - source: The source code to convert positions to line/columns for.
134+
public init(file: String, source: String) {
135+
self.file = file
136+
(self.lines, endOfFile) = computeLines(source)
137+
assert(source.lengthOfBytes(using: .utf8) == endOfFile.utf8Offset)
138+
}
139+
114140
/// Convert a `AbsolutePosition` to a `SourceLocation`. If the position is
115141
/// exceeding the file length then the `SourceLocation` for the end of file
116142
/// is returned. If position is negative the location for start of file is
@@ -305,6 +331,22 @@ fileprivate func computeLines(
305331
return (lines, position)
306332
}
307333

334+
fileprivate func computeLines(_ source: String) ->
335+
([AbsolutePosition], AbsolutePosition) {
336+
var lines: [AbsolutePosition] = []
337+
// First line starts from the beginning.
338+
lines.append(.startOfFile)
339+
var position: AbsolutePosition = .startOfFile
340+
let addLine = { (lineLength: SourceLength) in
341+
position += lineLength
342+
lines.append(position)
343+
}
344+
var curPrefix: SourceLength = .zero
345+
curPrefix = source.forEachLineLength(prefix: curPrefix, body: addLine)
346+
position += curPrefix
347+
return (lines, position)
348+
}
349+
308350
fileprivate extension String {
309351
/// Walks and passes to `body` the `SourceLength` for every detected line,
310352
/// with the newline character included.

Sources/SwiftSyntax/SyntaxParser.swift

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ typealias CTokenData = swiftparse_token_data_t
2828
typealias CLayoutData = swiftparse_layout_data_t
2929
typealias CParseLookupResult = swiftparse_lookup_result_t
3030
typealias CClientNode = swiftparse_client_node_t
31+
typealias CDiagnostic = swiftparser_diagnostic_t
32+
typealias CFixit = swiftparse_diagnostic_fixit_t
33+
typealias CRange = swiftparse_range_t
3134

3235
/// A list of possible errors that could be encountered while parsing a
3336
/// Syntax tree.
@@ -66,7 +69,9 @@ public enum SyntaxParser {
6669
/// - Throws: `ParserError`
6770
public static func parse(
6871
source: String,
69-
parseTransition: IncrementalParseTransition? = nil
72+
file: String = "",
73+
parseTransition: IncrementalParseTransition? = nil,
74+
diagConsumer: DiagnosticConsumer? = nil
7075
) throws -> SourceFileSyntax {
7176
guard nodeHashVerifyResult else {
7277
throw ParserError.parserCompatibilityCheckFailed
@@ -77,7 +82,7 @@ public enum SyntaxParser {
7782
var utf8Source = source
7883
utf8Source.makeNativeUTF8IfNeeded()
7984

80-
let rawSyntax = parseRaw(utf8Source, parseTransition)
85+
let rawSyntax = parseRaw(file, utf8Source, parseTransition, diagConsumer)
8186

8287
guard let file = makeSyntax(.forRoot(rawSyntax)) as? SourceFileSyntax else {
8388
throw ParserError.invalidSyntaxData
@@ -92,19 +97,23 @@ public enum SyntaxParser {
9297
/// - Returns: A top-level Syntax node representing the contents of the tree,
9398
/// if the parse was successful.
9499
/// - Throws: `ParserError`
95-
public static func parse(_ url: URL) throws -> SourceFileSyntax {
100+
public static func parse(_ url: URL,
101+
diagConsumer: DiagnosticConsumer? = nil) throws -> SourceFileSyntax {
96102
// Avoid using `String(contentsOf:)` because it creates a wrapped NSString.
97103
var fileData = try Data(contentsOf: url)
98104
fileData.append(0) // null terminate.
99105
let source = fileData.withUnsafeBytes { (ptr: UnsafePointer<CChar>) in
100106
return String(cString: ptr)
101107
}
102-
return try parse(source: source)
108+
return try parse(source: source, file: url.absoluteString,
109+
diagConsumer: diagConsumer)
103110
}
104111

105112
private static func parseRaw(
113+
_ file: String,
106114
_ source: String,
107-
_ parseTransition: IncrementalParseTransition?
115+
_ parseTransition: IncrementalParseTransition?,
116+
_ diagConsumer: DiagnosticConsumer?
108117
) -> RawSyntax {
109118
assert(source.isNativeUTF8)
110119
let c_parser = swiftparse_parser_create()
@@ -135,9 +144,81 @@ public enum SyntaxParser {
135144
swiftparse_parser_set_node_lookup(c_parser, nodeLookup);
136145
}
137146

147+
// Set up diagnostics consumer if requested by the caller.
148+
if let diagConsumer = diagConsumer {
149+
// If requested, we should set up a source location converter to calculate
150+
// line and column.
151+
let converter = diagConsumer.calculateLineColumn ?
152+
SourceLocationConverter(file: file, source: source) : nil
153+
let diagHandler = { (diag: CDiagnostic!) in
154+
diagConsumer.handle(Diagnostic(diag: diag, using: converter))
155+
}
156+
swiftparse_parser_set_diagnostic_handler(c_parser, diagHandler)
157+
}
158+
138159
let c_top = swiftparse_parse_string(c_parser, source)
139160

140161
// Get ownership back from the C parser.
141162
return RawSyntax.moveFromOpaque(c_top)!
142163
}
143164
}
165+
166+
extension SourceRange {
167+
init(_ range: CRange, _ converter: SourceLocationConverter?) {
168+
let start = SourceLocation(offset: Int(range.offset), converter: converter)
169+
let end = SourceLocation(offset: Int(range.offset + range.length),
170+
converter: converter)
171+
self.init(start: start, end: end)
172+
}
173+
}
174+
175+
extension Diagnostic.Message {
176+
init(_ diag: CDiagnostic) {
177+
let message = String(cString: swiftparse_diagnostic_get_message(diag))
178+
switch swiftparse_diagnostic_get_severity(diag) {
179+
case SWIFTPARSER_DIAGNOSTIC_SEVERITY_ERROR:
180+
self.init(.error, message)
181+
case SWIFTPARSER_DIAGNOSTIC_SEVERITY_WARNING:
182+
self.init(.warning, message)
183+
case SWIFTPARSER_DIAGNOSTIC_SEVERITY_NOTE:
184+
self.init(.note, message)
185+
default:
186+
fatalError("unrecognized diagnostic level")
187+
}
188+
}
189+
}
190+
191+
extension FixIt {
192+
init(_ cfixit: CFixit, _ converter: SourceLocationConverter?) {
193+
let replacement = String(cString: cfixit.text)
194+
let range = SourceRange(cfixit.range, converter)
195+
if cfixit.range.length == 0 {
196+
// Insert
197+
self = .insert(SourceLocation(offset: Int(cfixit.range.offset),
198+
converter: converter), replacement)
199+
} else if replacement.isEmpty {
200+
// Remove
201+
self = .remove(range)
202+
} else {
203+
// Replace
204+
self = .replace(range, replacement)
205+
}
206+
}
207+
}
208+
209+
extension Diagnostic {
210+
init(diag: CDiagnostic, using converter: SourceLocationConverter?) {
211+
// Collect highlighted ranges
212+
let hightlights = (0..<swiftparse_diagnostic_get_range_count(diag)).map {
213+
return SourceRange(swiftparse_diagnostic_get_range(diag, $0), converter)
214+
}
215+
// Collect fixits
216+
let fixits = (0..<swiftparse_diagnostic_get_fixit_count(diag)).map {
217+
return FixIt(swiftparse_diagnostic_get_fixit(diag, $0), converter)
218+
}
219+
self.init(message: Diagnostic.Message(diag),
220+
location: SourceLocation(offset: Int(swiftparse_diagnostic_get_source_loc(diag)),
221+
converter: converter),
222+
notes: [], highlights: hightlights, fixIts: fixits)
223+
}
224+
}

Sources/lit-test-helper/main.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,27 @@ func printSyntaxTree(args: CommandLineArguments) throws {
392392
tree.walk(&printer)
393393
}
394394

395+
func printParserDiags(args: CommandLineArguments) throws {
396+
let treeURL = URL(fileURLWithPath: try args.getRequired("-source-file"))
397+
class ParserDiagPrinter: DiagnosticConsumer {
398+
var counter = [0, 0, 0]
399+
var calculateLineColumn: Bool { return true }
400+
func finalize() {}
401+
func handle(_ diag: Diagnostic) {
402+
switch diag.message.severity {
403+
case .error: counter[0] += 1
404+
case .warning: counter[1] += 1
405+
case .note: counter[2] += 1
406+
}
407+
print(diag.debugDescription)
408+
}
409+
deinit {
410+
print("\(counter[0]) error(s) \(counter[1]) warnings(s) \(counter[2]) note(s)")
411+
}
412+
}
413+
_ = try SyntaxParser.parse(treeURL, diagConsumer: ParserDiagPrinter())
414+
}
415+
395416
do {
396417
let args = try CommandLineArguments.parse(CommandLine.arguments.dropFirst())
397418

@@ -403,6 +424,8 @@ do {
403424
try performRoundtrip(args: args)
404425
} else if args.has("-print-tree") {
405426
try printSyntaxTree(args: args)
427+
} else if args.has("-dump-diags") {
428+
try printParserDiags(args: args)
406429
} else if args.has("-help") {
407430
printHelp()
408431
} else {

lit_tests/parser-diags.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// RUN: %lit-test-helper -source-file %s -dump-diags 2>&1 | %FileCheck %s
2+
3+
// CHECK: [[@LINE+2]]:11 error: consecutive statements on a line must be separated by ';'
4+
// CHECK-NEXT: Fixit: ([[@LINE+1]]:11,[[@LINE+1]]:11) Text: ";"
5+
let number Int
6+
// CHECK-NEXT: [[@LINE-1]]:11 error: operator with postfix spacing cannot start a subexpression
7+
8+
// CHECK-NEXT: [[@LINE+2]]:3 error: invalid character in source file
9+
// CHECK-NEXT: Fixit: ([[@LINE+1]]:3,[[@LINE+1]]:6) Text: " "
10+
55
11+
// CHECK-NEXT: [[@LINE-1]]:3 note: unicode character '‒' looks similar to '-'; did you mean to use '-'?
12+
// CHECK-NEXT: Fixit: ([[@LINE-2]]:3,[[@LINE-2]]:6) Text: "-"
13+
// CHECK-NEXT: [[@LINE-3]]:2 error: consecutive statements on a line must be separated by ';'
14+
// CHECK-NEXT: Fixit: ([[@LINE-4]]:2,[[@LINE-4]]:2) Text: ";"
15+
16+
// CHECK-NEXT: [[@LINE+2]]:10 error: expected ',' separator
17+
// CHECK-NEXT: Fixit: ([[@LINE+1]]:9,[[@LINE+1]]:9) Text: ","
18+
if (true ꝸꝸꝸ false) {}
19+
20+
if (55) == 0 {}
21+
// CHECK-NEXT: [[@LINE-1]]:7 error: invalid character in source file
22+
// CHECK-NEXT: Fixit: ([[@LINE-2]]:7,[[@LINE-2]]:10) Text: " "
23+
// CHECK-NEXT: [[@LINE-3]]:7 note: unicode character '‒' looks similar to '-'; did you mean to use '-'?
24+
// CHECK-NEXT: Fixit: ([[@LINE-4]]:7,[[@LINE-4]]:10) Text: "-"
25+
// CHECK-NEXT: [[@LINE-5]]:11 error: expected ',' separator
26+
// CHECK-NEXT: Fixit: ([[@LINE-6]]:6,[[@LINE-6]]:6) Text: ","
27+
// CHECK-NEXT: 7 error(s) 0 warnings(s) 2 note(s)

0 commit comments

Comments
 (0)