@@ -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