Skip to content

Commit f3aaabf

Browse files
committed
attempt to parse decoded string into individual diagnostics
1 parent b81bfca commit f3aaabf

File tree

1 file changed

+125
-18
lines changed

1 file changed

+125
-18
lines changed

Sources/SwiftBuildSupport/SwiftBuildSystem.swift

Lines changed: 125 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,91 @@ final class SwiftBuildSystemMessageHandler {
308308
}
309309
}
310310

311+
/// Represents a parsed diagnostic segment from compiler output
312+
private struct ParsedDiagnostic {
313+
/// The file path if present
314+
let filePath: String?
315+
/// The line number if present
316+
let line: Int?
317+
/// The column number if present
318+
let column: Int?
319+
/// The severity (error, warning, note, remark)
320+
let severity: String
321+
/// The diagnostic message text
322+
let message: String
323+
/// The full text including any multi-line context (code snippets, carets, etc.)
324+
let fullText: String
325+
326+
/// Parse severity string to Diagnostic.Severity
327+
func toDiagnosticSeverity() -> Basics.Diagnostic.Severity {
328+
switch severity.lowercased() {
329+
case "error": return .error
330+
case "warning": return .warning
331+
case "note": return .info
332+
case "remark": return .debug
333+
default: return .info
334+
}
335+
}
336+
}
337+
338+
/// Split compiler output into individual diagnostic segments
339+
/// Format: /path/to/file.swift:line:column: severity: message
340+
private func splitIntoDiagnostics(_ output: String) -> [ParsedDiagnostic] {
341+
var diagnostics: [ParsedDiagnostic] = []
342+
343+
// Regex pattern to match diagnostic lines
344+
// Matches: path:line:column: severity: message (path is required)
345+
// The path must contain at least one character and line must be present
346+
let diagnosticPattern = #"^(.+?):(\d+):(?:(\d+):)?\s*(error|warning|note|remark):\s*(.*)$"#
347+
guard let regex = try? NSRegularExpression(pattern: diagnosticPattern, options: [.anchorsMatchLines]) else {
348+
return []
349+
}
350+
351+
let nsString = output as NSString
352+
let matches = regex.matches(in: output, options: [], range: NSRange(location: 0, length: nsString.length))
353+
354+
// Process each match and gather full text including subsequent lines
355+
for (index, match) in matches.enumerated() {
356+
let matchRange = match.range
357+
358+
// Extract components
359+
let filePathRange = match.range(at: 1)
360+
let lineRange = match.range(at: 2)
361+
let columnRange = match.range(at: 3)
362+
let severityRange = match.range(at: 4)
363+
let messageRange = match.range(at: 5)
364+
365+
let filePath = nsString.substring(with: filePathRange)
366+
let line = Int(nsString.substring(with: lineRange))
367+
let column = columnRange.location != NSNotFound ? Int(nsString.substring(with: columnRange)) : nil
368+
let severity = nsString.substring(with: severityRange)
369+
let message = nsString.substring(with: messageRange)
370+
371+
// Determine the full text range (from this diagnostic to the next one, or end)
372+
let startLocation = matchRange.location
373+
let endLocation: Int
374+
if index + 1 < matches.count {
375+
endLocation = matches[index + 1].range.location
376+
} else {
377+
endLocation = nsString.length
378+
}
379+
380+
let fullTextRange = NSRange(location: startLocation, length: endLocation - startLocation)
381+
let fullText = nsString.substring(with: fullTextRange).trimmingCharacters(in: .whitespacesAndNewlines)
382+
383+
diagnostics.append(ParsedDiagnostic(
384+
filePath: filePath,
385+
line: line,
386+
column: column,
387+
severity: severity,
388+
message: message,
389+
fullText: fullText
390+
))
391+
}
392+
393+
return diagnostics
394+
}
395+
311396
private func emitDiagnosticCompilerOutput(_ info: SwiftBuildMessage.TaskStartedInfo) {
312397
// Don't redundantly emit tasks.
313398
guard !self.tasksEmitted.contains(info.taskSignature) else {
@@ -318,27 +403,49 @@ final class SwiftBuildSystemMessageHandler {
318403
return
319404
}
320405

321-
// Fetch the task signature for a SwiftBuildMessage.DiagnosticInfo,
322-
// falling back to using the deprecated `locationContext` if we fail
323-
// to find it through the `locationContext2`.
324-
func getTaskSignature(from info: SwiftBuildMessage.DiagnosticInfo) -> String? {
325-
if let taskSignature = info.locationContext2.taskSignature {
326-
return taskSignature
327-
} else if let taskID = info.locationContext.taskID,
328-
let taskSignature = self.buildState.taskSignature(for: taskID)
329-
{
330-
return taskSignature
406+
// Decode the buffer to a string
407+
let decodedOutput = String(decoding: buffer, as: UTF8.self)
408+
409+
// Split the output into individual diagnostic segments
410+
let parsedDiagnostics = splitIntoDiagnostics(decodedOutput)
411+
412+
if parsedDiagnostics.isEmpty {
413+
// No structured diagnostics found - emit as-is based on task signature matching
414+
// Fetch the task signature for a SwiftBuildMessage.DiagnosticInfo
415+
func getTaskSignature(from info: SwiftBuildMessage.DiagnosticInfo) -> String? {
416+
if let taskSignature = info.locationContext2.taskSignature {
417+
return taskSignature
418+
} else if let taskID = info.locationContext.taskID,
419+
let taskSignature = self.buildState.taskSignature(for: taskID)
420+
{
421+
return taskSignature
422+
}
423+
return nil
331424
}
332-
return nil
333-
}
334425

335-
// Ensure that this info matches with the location context of the
336-
// DiagnosticInfo. Otherwise, it should be emitted with "info" severity.
337-
let decodedOutput = String(decoding: buffer, as: UTF8.self)
338-
if unprocessedDiagnostics.compactMap(getTaskSignature).contains(where: { $0 == info.taskSignature }) {
339-
self.observabilityScope.emit(error: decodedOutput)
426+
// Use existing logic as fallback
427+
if unprocessedDiagnostics.compactMap(getTaskSignature).contains(where: { $0 == info.taskSignature }) {
428+
self.observabilityScope.emit(error: decodedOutput)
429+
} else {
430+
self.observabilityScope.emit(info: decodedOutput)
431+
}
340432
} else {
341-
self.observabilityScope.emit(info: decodedOutput)
433+
// Process each parsed diagnostic derived from the decodedOutput
434+
for parsedDiag in parsedDiagnostics {
435+
let severity = parsedDiag.toDiagnosticSeverity()
436+
437+
// Use the appropriate emit method based on severity
438+
switch severity {
439+
case .error:
440+
self.observabilityScope.emit(error: parsedDiag.fullText)
441+
case .warning:
442+
self.observabilityScope.emit(warning: parsedDiag.fullText)
443+
case .info:
444+
self.observabilityScope.emit(info: parsedDiag.fullText)
445+
case .debug:
446+
self.observabilityScope.emit(severity: .debug, message: parsedDiag.fullText)
447+
}
448+
}
342449
}
343450

344451
// Record that we've emitted the output for a given task signature.

0 commit comments

Comments
 (0)