1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
|
/*
This source file is part of the Swift.org open source project
Copyright (c) 2020 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/
import Foundation
import TSCBasic
/// Represents diagnostics serialized in a .dia file by the Swift compiler or Clang.
public struct SerializedDiagnostics: Sendable {
public enum Error: Swift.Error {
case badMagic
case unexpectedTopLevelRecord
case unknownBlock
case malformedRecord
case noMetadataBlock
case unexpectedSubblock
case unexpectedRecord
case missingInformation
}
private enum BlockID: UInt64 {
case metadata = 8
case diagnostic = 9
}
private enum RecordID: UInt64 {
case version = 1
case diagnosticInfo = 2
case sourceRange = 3
case flag = 4
case category = 5
case filename = 6
case fixit = 7
}
/// The serialized diagnostics format version number.
public var versionNumber: Int
/// Serialized diagnostics.
public var diagnostics: [Diagnostic]
public init(bytes: ByteString) throws {
var reader = Reader()
try Bitcode.read(bytes: bytes, using: &reader)
guard let version = reader.versionNumber else { throw Error.noMetadataBlock }
self.versionNumber = version
self.diagnostics = reader.diagnostics
}
}
extension SerializedDiagnostics.Error: CustomNSError {
public var errorUserInfo: [String : Any] {
return [NSLocalizedDescriptionKey: "\(self)"]
}
}
extension SerializedDiagnostics {
public struct Diagnostic {
public enum Level: UInt64, Sendable {
case ignored, note, warning, error, fatal, remark
}
/// The diagnostic message text.
public var text: String
/// The level the diagnostic was emitted at.
public var level: Level
/// The location the diagnostic was emitted at in the source file.
public var location: SourceLocation?
/// The diagnostic category. Currently only Clang emits this.
public var category: String?
/// The corresponding diagnostic command-line flag. Currently only Clang emits this.
public var flag: String?
/// Ranges in the source file associated with the diagnostic.
public var ranges: [(SourceLocation, SourceLocation)]
/// Fix-its associated with the diagnostic.
public var fixIts: [FixIt]
fileprivate init(records: [SerializedDiagnostics.OwnedRecord],
filenameMap: [UInt64: String],
flagMap: [UInt64: String],
categoryMap: [UInt64: String]) throws {
var text: String? = nil
var level: Level? = nil
var location: SourceLocation? = nil
var category: String? = nil
var flag: String? = nil
var ranges: [(SourceLocation, SourceLocation)] = []
var fixIts: [FixIt] = []
for record in records {
switch SerializedDiagnostics.RecordID(rawValue: record.id) {
case .flag, .category, .filename: continue
case .diagnosticInfo:
guard record.fields.count == 8,
case .blob(let diagnosticBlob) = record.payload
else { throw Error.malformedRecord }
text = String(decoding: diagnosticBlob, as: UTF8.self)
level = Level(rawValue: record.fields[0])
location = SourceLocation(fields: record.fields[1...4],
filenameMap: filenameMap)
category = categoryMap[record.fields[5]]
flag = flagMap[record.fields[6]]
case .sourceRange:
guard record.fields.count == 8 else { throw Error.malformedRecord }
if let start = SourceLocation(fields: record.fields[0...3],
filenameMap: filenameMap),
let end = SourceLocation(fields: record.fields[4...7],
filenameMap: filenameMap) {
ranges.append((start, end))
}
case .fixit:
guard record.fields.count == 9,
case .blob(let fixItBlob) = record.payload
else { throw Error.malformedRecord }
let fixItText = String(decoding: fixItBlob, as: UTF8.self)
if let start = SourceLocation(fields: record.fields[0...3],
filenameMap: filenameMap),
let end = SourceLocation(fields: record.fields[4...7],
filenameMap: filenameMap) {
fixIts.append(FixIt(start: start, end: end, text: fixItText))
}
case .version, nil:
// Ignore unknown records, for future compatibility
continue
}
}
do {
guard let text = text, let level = level else {
throw Error.missingInformation
}
self.text = text
self.level = level
self.location = location
self.category = category
self.flag = flag
self.fixIts = fixIts
self.ranges = ranges
}
}
}
public struct SourceLocation: Equatable, Sendable {
/// The filename associated with the diagnostic.
public var filename: String
public var line: UInt64
public var column: UInt64
/// The byte offset in the source file of the diagnostic. Currently, only
/// Clang includes this, it is set to 0 by Swift.
public var offset: UInt64
fileprivate init?(fields: ArraySlice<UInt64>,
filenameMap: [UInt64: String]) {
guard let filename = filenameMap[fields[fields.startIndex]] else { return nil }
self.filename = filename
self.line = fields[fields.startIndex + 1]
self.column = fields[fields.startIndex + 2]
self.offset = fields[fields.startIndex + 3]
}
}
public struct FixIt: Sendable {
/// Start location.
public var start: SourceLocation
/// End location.
public var end: SourceLocation
/// Fix-it replacement text.
public var text: String
}
}
#if compiler(>=5.7)
extension SerializedDiagnostics.Diagnostic: Sendable {}
#else
extension SerializedDiagnostics.Diagnostic: UnsafeSendable {}
#endif
extension SerializedDiagnostics {
private struct Reader: BitstreamVisitor {
var diagnosticRecords: [[OwnedRecord]] = []
var activeBlocks: [BlockID] = []
var currentBlockID: BlockID? { activeBlocks.last }
var diagnostics: [Diagnostic] = []
var versionNumber: Int? = nil
var filenameMap = [UInt64: String]()
var flagMap = [UInt64: String]()
var categoryMap = [UInt64: String]()
func validate(signature: Bitcode.Signature) throws {
guard signature == .init(string: "DIAG") else { throw Error.badMagic }
}
mutating func shouldEnterBlock(id: UInt64) throws -> Bool {
guard let blockID = BlockID(rawValue: id) else { throw Error.unknownBlock }
activeBlocks.append(blockID)
if currentBlockID == .diagnostic {
diagnosticRecords.append([])
}
return true
}
mutating func didExitBlock() throws {
activeBlocks.removeLast()
if activeBlocks.isEmpty {
for records in diagnosticRecords where !records.isEmpty {
diagnostics.append(try Diagnostic(records: records,
filenameMap: filenameMap,
flagMap: flagMap,
categoryMap: categoryMap))
}
diagnosticRecords = []
}
}
mutating func visit(record: BitcodeElement.Record) throws {
switch currentBlockID {
case .metadata:
guard record.id == RecordID.version.rawValue,
record.fields.count == 1 else { throw Error.malformedRecord }
versionNumber = Int(record.fields[0])
case .diagnostic:
switch SerializedDiagnostics.RecordID(rawValue: record.id) {
case .filename:
guard record.fields.count == 4,
case .blob(let filenameBlob) = record.payload
else { throw Error.malformedRecord }
let filenameText = String(decoding: filenameBlob, as: UTF8.self)
let filenameID = record.fields[0]
// record.fields[1] and record.fields[2] are no longer used.
filenameMap[filenameID] = filenameText
case .category:
guard record.fields.count == 2,
case .blob(let categoryBlob) = record.payload
else { throw Error.malformedRecord }
let categoryText = String(decoding: categoryBlob, as: UTF8.self)
let categoryID = record.fields[0]
categoryMap[categoryID] = categoryText
case .flag:
guard record.fields.count == 2,
case .blob(let flagBlob) = record.payload
else { throw Error.malformedRecord }
let flagText = String(decoding: flagBlob, as: UTF8.self)
let diagnosticID = record.fields[0]
flagMap[diagnosticID] = flagText
default:
diagnosticRecords[diagnosticRecords.count - 1].append(SerializedDiagnostics.OwnedRecord(record))
}
case nil:
throw Error.unexpectedTopLevelRecord
}
}
}
}
extension SerializedDiagnostics {
struct OwnedRecord {
public enum Payload {
case none
case array([UInt64])
case char6String(String)
case blob([UInt8])
init(_ payload: BitcodeElement.Record.Payload) {
switch payload {
case .none:
self = .none
case .array(let a):
self = .array(Array(a))
case .char6String(let s):
self = .char6String(s)
case .blob(let b):
self = .blob(Array(b))
}
}
}
public var id: UInt64
public var fields: [UInt64]
public var payload: Payload
init(_ record: BitcodeElement.Record) {
self.id = record.id
self.fields = Array(record.fields)
self.payload = Payload(record.payload)
}
}
}
|