Skip to content

Commit 08bd94f

Browse files
committed
Add deserialization for the Swift/Clang serialized diagnostics (.dia) format
1 parent 2434831 commit 08bd94f

File tree

3 files changed

+316
-12
lines changed

3 files changed

+316
-12
lines changed

Sources/TSCUtility/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ add_library(TSCUtility
1010
Archiver.swift
1111
ArgumentParser.swift
1212
ArgumentParserShellCompletion.swift
13+
Bits.swift
14+
Bitstream.swift
1315
BuildFlags.swift
1416
CollectionExtensions.swift
1517
Diagnostics.swift
@@ -28,6 +30,7 @@ add_library(TSCUtility
2830
Platform.swift
2931
PolymorphicCodable.swift
3032
ProgressAnimation.swift
33+
SerializedDiagnostics.swift
3134
SQLite.swift
3235
SimplePersistence.swift
3336
StringExtensions.swift
Lines changed: 230 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,232 @@
1-
//
2-
// SerializedDiagnostics.swift
3-
// swift-tools-support-core
4-
//
5-
// Created by Owen Voorhees on 9/1/20.
6-
//
1+
/*
2+
This source file is part of the Swift.org open source project
73

4+
Copyright (c) 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
810
import Foundation
11+
12+
/// Represents diagnostics serialized in a .dia file by the Swift compiler or Clang.
13+
public struct SerializedDiagnostics {
14+
public enum Error: Swift.Error {
15+
case badMagic
16+
case unexpectedTopLevelRecord
17+
case unknownBlock
18+
case malformedRecord
19+
case noMetadataBlock
20+
case unexpectedSubblock
21+
case unexpectedRecord
22+
case missingInformation
23+
}
24+
25+
private enum BlockID: UInt64 {
26+
case metadata = 8
27+
case diagnostic = 9
28+
}
29+
30+
private enum RecordID: UInt64 {
31+
case version = 1
32+
case diagnosticInfo = 2
33+
case sourceRange = 3
34+
case flag = 4
35+
case category = 5
36+
case filename = 6
37+
case fixit = 7
38+
}
39+
40+
/// The serialized diagnostics format version number.
41+
public var versionNumber: Int
42+
/// Serialized diagnostics.
43+
public var diagnostics: [Diagnostic]
44+
45+
public init(data: Data) throws {
46+
let bitcode = try Bitcode(data: data)
47+
48+
guard bitcode.signature == .init(string: "DIAG") else { throw Error.badMagic }
49+
50+
var diagnostics: [Diagnostic] = []
51+
var versionNumber: Int? = nil
52+
var filenameMap = [UInt64: String]()
53+
var flagMap = [UInt64: String]()
54+
var categoryMap = [UInt64: String]()
55+
56+
for element in bitcode.elements {
57+
guard case let .block(block) = element else { throw Error.unexpectedTopLevelRecord }
58+
switch BlockID(rawValue: block.id) {
59+
case .metadata:
60+
guard block.elements.count == 1,
61+
case let .record(versionRecord) = block.elements[0],
62+
versionRecord.id == RecordID.version.rawValue,
63+
versionRecord.fields.count == 1 else {
64+
throw Error.malformedRecord
65+
}
66+
versionNumber = Int(versionRecord.fields[0])
67+
case .diagnostic:
68+
diagnostics.append(try Diagnostic(block: block,
69+
filenameMap: &filenameMap,
70+
flagMap: &flagMap,
71+
categoryMap: &categoryMap))
72+
case nil:
73+
throw Error.unknownBlock
74+
}
75+
}
76+
77+
guard let version = versionNumber else { throw Error.noMetadataBlock }
78+
self.versionNumber = version
79+
self.diagnostics = diagnostics
80+
}
81+
}
82+
83+
extension SerializedDiagnostics {
84+
public struct Diagnostic {
85+
86+
public enum Level: UInt64 {
87+
case ignored, note, warning, error, fatal, remark
88+
}
89+
/// The diagnostic message text.
90+
public var text: String
91+
/// The level the diagnostic was emitted at.
92+
public var level: Level
93+
/// The location the diagnostic was emitted at in the source file.
94+
public var location: SourceLocation
95+
/// The diagnostic category. Currently only Clang emits this.
96+
public var category: String?
97+
/// The corresponding diagnostic command-line flag. Currently only Clang emits this.
98+
public var flag: String?
99+
/// Ranges in the source file associated with the diagnostic.
100+
public var ranges: [(SourceLocation, SourceLocation)]
101+
/// Fix-its associated with the diagnostic.
102+
public var fixIts: [FixIt]
103+
104+
fileprivate init(block: BitcodeElement.Block,
105+
filenameMap: inout [UInt64: String],
106+
flagMap: inout [UInt64: String],
107+
categoryMap: inout [UInt64: String]) throws {
108+
var text: String? = nil
109+
var level: Level? = nil
110+
var location: SourceLocation? = nil
111+
var category: String? = nil
112+
var flag: String? = nil
113+
var ranges: [(SourceLocation, SourceLocation)] = []
114+
var fixIts: [FixIt] = []
115+
116+
for element in block.elements {
117+
guard case let .record(record) = element else {
118+
throw Error.unexpectedSubblock
119+
}
120+
121+
switch SerializedDiagnostics.RecordID(rawValue: record.id) {
122+
case .diagnosticInfo:
123+
guard record.fields.count == 8,
124+
case .blob(let diagnosticBlob) = record.payload
125+
else { throw Error.malformedRecord }
126+
127+
text = String(data: diagnosticBlob, encoding: .utf8)
128+
level = Level(rawValue: record.fields[0])
129+
location = try SourceLocation(fields: record.fields[1...4],
130+
filenameMap: filenameMap)
131+
category = categoryMap[record.fields[5]]
132+
flag = flagMap[record.fields[6]]
133+
134+
case .sourceRange:
135+
guard record.fields.count == 8 else { throw Error.malformedRecord }
136+
137+
let start = try SourceLocation(fields: record.fields[0...3],
138+
filenameMap: filenameMap)
139+
let end = try SourceLocation(fields: record.fields[4...7],
140+
filenameMap: filenameMap)
141+
ranges.append((start, end))
142+
143+
case .flag:
144+
guard record.fields.count == 2,
145+
case .blob(let flagBlob) = record.payload,
146+
let flagText = String(data: flagBlob, encoding: .utf8)
147+
else { throw Error.malformedRecord }
148+
149+
let diagnosticID = record.fields[0]
150+
flagMap[diagnosticID] = flagText
151+
152+
case .category:
153+
guard record.fields.count == 2,
154+
case .blob(let categoryBlob) = record.payload,
155+
let categoryText = String(data: categoryBlob, encoding: .utf8)
156+
else { throw Error.malformedRecord }
157+
158+
let categoryID = record.fields[0]
159+
categoryMap[categoryID] = categoryText
160+
161+
case .filename:
162+
guard record.fields.count == 4,
163+
case .blob(let filenameBlob) = record.payload,
164+
let filenameText = String(data: filenameBlob, encoding: .utf8)
165+
else { throw Error.malformedRecord }
166+
167+
let filenameID = record.fields[0]
168+
// record.fields[1] and record.fields[2] are no longer used.
169+
filenameMap[filenameID] = filenameText
170+
171+
case .fixit:
172+
guard record.fields.count == 9,
173+
case .blob(let fixItBlob) = record.payload,
174+
let fixItText = String(data: fixItBlob, encoding: .utf8)
175+
else { throw Error.malformedRecord }
176+
177+
let start = try SourceLocation(fields: record.fields[0...3],
178+
filenameMap: filenameMap)
179+
let end = try SourceLocation(fields: record.fields[4...7],
180+
filenameMap: filenameMap)
181+
fixIts.append(FixIt(start: start, end: end, text: fixItText))
182+
183+
case .version, nil:
184+
throw Error.unexpectedRecord
185+
}
186+
}
187+
188+
do {
189+
guard let text = text, let level = level, let location = location else {
190+
throw Error.missingInformation
191+
}
192+
self.text = text
193+
self.level = level
194+
self.location = location
195+
self.category = category
196+
self.flag = flag
197+
self.fixIts = fixIts
198+
self.ranges = ranges
199+
}
200+
}
201+
}
202+
203+
public struct SourceLocation: Equatable {
204+
/// The filename associated with the diagnostic.
205+
public var filename: String
206+
public var line: UInt64
207+
public var column: UInt64
208+
/// The byte offset in the source file of the diagnostic. Currently, only
209+
/// Clang includes this, it is set to 0 by Swift.
210+
public var offset: UInt64
211+
212+
fileprivate init(fields: ArraySlice<UInt64>,
213+
filenameMap: [UInt64: String]) throws {
214+
guard let name = filenameMap[fields[fields.startIndex]] else {
215+
throw Error.missingInformation
216+
}
217+
self.filename = name
218+
self.line = fields[fields.startIndex + 1]
219+
self.column = fields[fields.startIndex + 2]
220+
self.offset = fields[fields.startIndex + 3]
221+
}
222+
}
223+
224+
public struct FixIt {
225+
/// Start location.
226+
public var start: SourceLocation
227+
/// End location.
228+
public var end: SourceLocation
229+
/// Fix-it replacement text.
230+
public var text: String
231+
}
232+
}
Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,85 @@
1-
//
2-
// File.swift
3-
//
4-
//
5-
// Created by Owen Voorhees on 9/1/20.
6-
//
1+
/*
2+
This source file is part of the Swift.org open source project
73

4+
Copyright (c) 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import XCTest
812
import Foundation
13+
import TSCBasic
14+
import TSCUtility
15+
16+
final class SerializedDiagnosticsTests: XCTestCase {
17+
func testReadSwiftSerializedDiags() throws {
18+
let serializedDiagnosticsPath = AbsolutePath(#file).parentDirectory
19+
.appending(components: "Inputs", "serialized.dia")
20+
let contents = try localFileSystem.readFileContents(serializedDiagnosticsPath)
21+
let serializedDiags = try SerializedDiagnostics(data: Data(contents.contents))
22+
23+
XCTAssertEqual(serializedDiags.versionNumber, 1)
24+
XCTAssertEqual(serializedDiags.diagnostics.count, 17)
25+
26+
let one = serializedDiags.diagnostics[5]
27+
XCTAssertEqual(one.text, "expected ',' separator")
28+
XCTAssertEqual(one.level, .error)
29+
XCTAssertTrue(one.location.filename.hasSuffix("/StoreSearchCoordinator.swift"))
30+
XCTAssertEqual(one.location.line, 21)
31+
XCTAssertEqual(one.location.column, 69)
32+
XCTAssertEqual(one.location.offset, 0)
33+
XCTAssertNil(one.category)
34+
XCTAssertNil(one.flag)
35+
XCTAssertEqual(one.ranges.count, 0)
36+
XCTAssertEqual(one.fixIts.count, 1)
37+
XCTAssertEqual(one.fixIts[0].text, ",")
38+
XCTAssertEqual(one.fixIts[0].start, one.fixIts[0].end)
39+
XCTAssertTrue(one.fixIts[0].start.filename.hasSuffix("/StoreSearchCoordinator.swift"))
40+
XCTAssertEqual(one.fixIts[0].start.line, 21)
41+
XCTAssertEqual(one.fixIts[0].start.column, 69)
42+
XCTAssertEqual(one.fixIts[0].start.offset, 0)
43+
44+
let two = serializedDiags.diagnostics[16]
45+
XCTAssertEqual(two.text, "use of unresolved identifier 'DispatchQueue'")
46+
XCTAssertEqual(two.level, .error)
47+
XCTAssertTrue(two.location.filename.hasSuffix("/Observable.swift"))
48+
XCTAssertEqual(two.location.line, 34)
49+
XCTAssertEqual(two.location.column, 13)
50+
XCTAssertEqual(two.location.offset, 0)
51+
XCTAssertNil(two.category)
52+
XCTAssertNil(two.flag)
53+
XCTAssertEqual(two.ranges.count, 1)
54+
XCTAssertTrue(two.ranges[0].0.filename.hasSuffix("/Observable.swift"))
55+
XCTAssertEqual(two.ranges[0].0.line, 34)
56+
XCTAssertEqual(two.ranges[0].0.column, 13)
57+
XCTAssertEqual(two.ranges[0].0.offset, 0)
58+
XCTAssertTrue(two.ranges[0].1.filename.hasSuffix("/Observable.swift"))
59+
XCTAssertEqual(two.ranges[0].1.line, 34)
60+
XCTAssertEqual(two.ranges[0].1.column, 26)
61+
XCTAssertEqual(two.ranges[0].1.offset, 0)
62+
XCTAssertEqual(two.fixIts.count, 0)
63+
}
64+
65+
func testReadClangSerializedDiags() throws {
66+
let serializedDiagnosticsPath = AbsolutePath(#file).parentDirectory
67+
.appending(components: "Inputs", "clang.dia")
68+
let contents = try localFileSystem.readFileContents(serializedDiagnosticsPath)
69+
let serializedDiags = try SerializedDiagnostics(data: Data(contents.contents))
70+
71+
XCTAssertEqual(serializedDiags.versionNumber, 1)
72+
XCTAssertEqual(serializedDiags.diagnostics.count, 4)
73+
74+
let one = serializedDiags.diagnostics[1]
75+
XCTAssertEqual(one.text, "values of type 'NSInteger' should not be used as format arguments; add an explicit cast to 'long' instead")
76+
XCTAssertEqual(one.level, .warning)
77+
XCTAssertEqual(one.location.line, 252)
78+
XCTAssertEqual(one.location.column, 137)
79+
XCTAssertEqual(one.location.offset, 10046)
80+
XCTAssertEqual(one.category, "Format String Issue")
81+
XCTAssertEqual(one.flag, "format")
82+
XCTAssertEqual(one.ranges.count, 4)
83+
XCTAssertEqual(one.fixIts.count, 2)
84+
}
85+
}

0 commit comments

Comments
 (0)