Skip to content

Commit 5176169

Browse files
authored
Fix TSC diagnostic file parsing (handle parsing diagnostics with multiple blocks)
This patch fixes parts of the .dia file reading. As originally implemented, the reader would fail and emit an error if a diagnostic had sub-diagnostics, i.e, a warning with notes. Part of the issue was that the parser could only handle one block at a time and couldn't handle the nesting of blocks. After fixing this, it became apparent that there were some other issues. The parser was consuming the records in a post-order traversal of the diagnostic graph, when it really needs to be handling them in a pre-order ordering. This wasn't an issue as a post-order and pre-ordering are the same if you can only handle trees with a root node. Emitted in the wrong order, the filemap, flagmap, and categorymap wouldn't be updated correctly, resulting on dropping diagnostic information on the floor, when really the information would come later. In the end, I recorded the diagnostic records in the pre-order traversal order into the `diagnosticRecords` property. When exiting from the last active blockID in a diagnostic tree, I would run over the lists of records, emitting a diagnostic for each. This way we emit diagnostics in a pre-order ordering once we have collected all of the information for the diagnostics in the tree. Now, there's even more fun to be had. When constructing the diagnostic, the code made an assumption that filename records, category records, and flag records would come strictly before the diagnosticInfo, source range, and fixIt records. This isn't the case. To work around that, I process filenames, categories, and flags, while visiting the record tree to fully populate the respective maps. I'm doing it this way because it means that we don't have to iterate over the records twice in the `Diagnostic` initializer, and don't need to mutate the maps either. So now we emit full diagnostics, even when the tagged records come in different orders.
1 parent 710b623 commit 5176169

File tree

3 files changed

+132
-47
lines changed

3 files changed

+132
-47
lines changed

Sources/TSCUtility/SerializedDiagnostics.swift

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ extension SerializedDiagnostics {
8080
public var fixIts: [FixIt]
8181

8282
fileprivate init(records: [SerializedDiagnostics.OwnedRecord],
83-
filenameMap: inout [UInt64: String],
84-
flagMap: inout [UInt64: String],
85-
categoryMap: inout [UInt64: String]) throws {
83+
filenameMap: [UInt64: String],
84+
flagMap: [UInt64: String],
85+
categoryMap: [UInt64: String]) throws {
8686
var text: String? = nil
8787
var level: Level? = nil
8888
var location: SourceLocation? = nil
@@ -93,6 +93,7 @@ extension SerializedDiagnostics {
9393

9494
for record in records {
9595
switch SerializedDiagnostics.RecordID(rawValue: record.id) {
96+
case .flag, .category, .filename: continue
9697
case .diagnosticInfo:
9798
guard record.fields.count == 8,
9899
case .blob(let diagnosticBlob) = record.payload
@@ -114,34 +115,6 @@ extension SerializedDiagnostics {
114115
filenameMap: filenameMap) {
115116
ranges.append((start, end))
116117
}
117-
case .flag:
118-
guard record.fields.count == 2,
119-
case .blob(let flagBlob) = record.payload
120-
else { throw Error.malformedRecord }
121-
122-
let flagText = String(decoding: flagBlob, as: UTF8.self)
123-
let diagnosticID = record.fields[0]
124-
flagMap[diagnosticID] = flagText
125-
126-
case .category:
127-
guard record.fields.count == 2,
128-
case .blob(let categoryBlob) = record.payload
129-
else { throw Error.malformedRecord }
130-
131-
let categoryText = String(decoding: categoryBlob, as: UTF8.self)
132-
let categoryID = record.fields[0]
133-
categoryMap[categoryID] = categoryText
134-
135-
case .filename:
136-
guard record.fields.count == 4,
137-
case .blob(let filenameBlob) = record.payload
138-
else { throw Error.malformedRecord }
139-
140-
let filenameText = String(decoding: filenameBlob, as: UTF8.self)
141-
let filenameID = record.fields[0]
142-
// record.fields[1] and record.fields[2] are no longer used.
143-
filenameMap[filenameID] = filenameText
144-
145118
case .fixit:
146119
guard record.fields.count == 9,
147120
case .blob(let fixItBlob) = record.payload
@@ -206,48 +179,78 @@ extension SerializedDiagnostics {
206179

207180
extension SerializedDiagnostics {
208181
private struct Reader: BitstreamVisitor {
209-
var currentBlockID: BlockID? = nil
182+
var diagnosticRecords: [[OwnedRecord]] = []
183+
var activeBlocks: [BlockID] = []
184+
var currentBlockID: BlockID? { activeBlocks.last }
210185

211186
var diagnostics: [Diagnostic] = []
212187
var versionNumber: Int? = nil
213188
var filenameMap = [UInt64: String]()
214189
var flagMap = [UInt64: String]()
215190
var categoryMap = [UInt64: String]()
216191

217-
var currentDiagnosticRecords: [OwnedRecord] = []
218-
219192
func validate(signature: Bitcode.Signature) throws {
220193
guard signature == .init(string: "DIAG") else { throw Error.badMagic }
221194
}
222195

223196
mutating func shouldEnterBlock(id: UInt64) throws -> Bool {
224197
guard let blockID = BlockID(rawValue: id) else { throw Error.unknownBlock }
225-
guard currentBlockID == nil else { throw Error.unexpectedSubblock }
226-
currentBlockID = blockID
198+
activeBlocks.append(blockID)
199+
if currentBlockID == .diagnostic {
200+
diagnosticRecords.append([])
201+
}
227202
return true
228203
}
229204

230205
mutating func didExitBlock() throws {
231-
if currentBlockID == .diagnostic {
232-
diagnostics.append(try Diagnostic(records: currentDiagnosticRecords,
233-
filenameMap: &filenameMap,
234-
flagMap: &flagMap,
235-
categoryMap: &categoryMap))
236-
currentDiagnosticRecords = []
206+
activeBlocks.removeLast()
207+
if activeBlocks.isEmpty {
208+
for records in diagnosticRecords where !records.isEmpty {
209+
diagnostics.append(try Diagnostic(records: records,
210+
filenameMap: filenameMap,
211+
flagMap: flagMap,
212+
categoryMap: categoryMap))
213+
}
214+
diagnosticRecords = []
237215
}
238-
currentBlockID = nil
239216
}
240217

241218
mutating func visit(record: BitcodeElement.Record) throws {
242219
switch currentBlockID {
243220
case .metadata:
244221
guard record.id == RecordID.version.rawValue,
245-
record.fields.count == 1 else {
246-
throw Error.malformedRecord
247-
}
222+
record.fields.count == 1 else { throw Error.malformedRecord }
248223
versionNumber = Int(record.fields[0])
249224
case .diagnostic:
250-
currentDiagnosticRecords.append(SerializedDiagnostics.OwnedRecord(record))
225+
switch SerializedDiagnostics.RecordID(rawValue: record.id) {
226+
case .filename:
227+
guard record.fields.count == 4,
228+
case .blob(let filenameBlob) = record.payload
229+
else { throw Error.malformedRecord }
230+
231+
let filenameText = String(decoding: filenameBlob, as: UTF8.self)
232+
let filenameID = record.fields[0]
233+
// record.fields[1] and record.fields[2] are no longer used.
234+
filenameMap[filenameID] = filenameText
235+
case .category:
236+
guard record.fields.count == 2,
237+
case .blob(let categoryBlob) = record.payload
238+
else { throw Error.malformedRecord }
239+
240+
let categoryText = String(decoding: categoryBlob, as: UTF8.self)
241+
let categoryID = record.fields[0]
242+
categoryMap[categoryID] = categoryText
243+
case .flag:
244+
guard record.fields.count == 2,
245+
case .blob(let flagBlob) = record.payload
246+
else { throw Error.malformedRecord }
247+
248+
let flagText = String(decoding: flagBlob, as: UTF8.self)
249+
let diagnosticID = record.fields[0]
250+
flagMap[diagnosticID] = flagText
251+
default:
252+
diagnosticRecords[diagnosticRecords.count - 1].append(SerializedDiagnostics.OwnedRecord(record))
253+
}
251254
case nil:
252255
throw Error.unexpectedTopLevelRecord
253256
}
988 Bytes
Binary file not shown.

Tests/TSCUtilityTests/SerializedDiagnosticsTests.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,88 @@ import TSCBasic
1414
import TSCUtility
1515

1616
final class SerializedDiagnosticsTests: XCTestCase {
17+
func testReadSwiftDiagnosticWithNote() throws {
18+
let serializedDiagnosticsPath = AbsolutePath(#file).parentDirectory
19+
.appending(components: "Inputs", "multiblock.dia")
20+
let contents = try localFileSystem.readFileContents(serializedDiagnosticsPath)
21+
let serializedDiags = try SerializedDiagnostics(bytes: contents)
22+
23+
XCTAssertEqual(serializedDiags.versionNumber, 2)
24+
XCTAssertEqual(serializedDiags.diagnostics.count, 7)
25+
26+
struct TestSrcLoc: Equatable {
27+
var filename: String
28+
var line: UInt64
29+
var column: UInt64
30+
var offset: UInt64
31+
32+
init(filename: String, line: UInt64, column: UInt64, offset: UInt64) {
33+
self.filename = filename
34+
self.line = line
35+
self.column = column
36+
self.offset = offset
37+
}
38+
39+
init(_ original: SerializedDiagnostics.SourceLocation) {
40+
self.filename = original.filename
41+
self.line = original.line
42+
self.column = original.column
43+
self.offset = original.offset
44+
}
45+
}
46+
47+
struct TestDiag {
48+
var text: String
49+
var level: SerializedDiagnostics.Diagnostic.Level
50+
var location: TestSrcLoc?
51+
var category: String?
52+
var flag: String?
53+
54+
}
55+
56+
let expectedResults = [
57+
TestDiag(text: "type 'A' does not conform to protocol 'P'",
58+
level: .error,
59+
location: TestSrcLoc(filename: "test.swift",
60+
line: 5, column: 7, offset: 35)),
61+
TestDiag(text: "candidate is 'async', but protocol requirement is not",
62+
level: .note,
63+
location: TestSrcLoc(filename: "test.swift",
64+
line: 6, column: 10, offset: 51)),
65+
TestDiag(text: "do you want to add protocol stubs?",
66+
level: .note,
67+
location: TestSrcLoc(filename: "test.swift",
68+
line: 5, column: 7, offset: 35)),
69+
TestDiag(text: "initialization of immutable value 'a' was never used",
70+
level: .warning,
71+
location: TestSrcLoc(filename: "test.swift",
72+
line: 7, column: 13, offset: 75)),
73+
TestDiag(text: "consider replacing with '_' or removing it",
74+
level: .note,
75+
location: TestSrcLoc(filename: "test.swift",
76+
line: 7, column: 13, offset: 75)),
77+
TestDiag(text: "initialization of immutable value 'b' was never used",
78+
level: .warning,
79+
location: TestSrcLoc(filename: "test.swift",
80+
line: 8, column: 13, offset: 94)),
81+
TestDiag(text: "consider replacing with '_' or removing it",
82+
level: .note,
83+
location: TestSrcLoc(filename: "test.swift",
84+
line: 8, column: 13, offset: 94))
85+
]
86+
87+
for case let (diag, expected) in zip(serializedDiags.diagnostics,
88+
expectedResults) {
89+
XCTAssertEqual(diag.text, expected.text, "Mismatched Diagnostic Text")
90+
XCTAssertEqual(diag.level, expected.level, "Mismatched Diagnostic Level")
91+
92+
XCTAssertEqual((diag.location == nil), (expected.location == nil), "Unexpected Diagnostic Location")
93+
if let diagLoc = diag.location, let expectedLoc = expected.location {
94+
XCTAssertEqual(TestSrcLoc(diagLoc), expectedLoc, "Mismatched Diagnostic Location")
95+
}
96+
}
97+
}
98+
1799
func testReadSwiftSerializedDiags() throws {
18100
let serializedDiagnosticsPath = AbsolutePath(#file).parentDirectory
19101
.appending(components: "Inputs", "serialized.dia")

0 commit comments

Comments
 (0)