From 860f36719f25335d2b37e97c2c0f2f15fd45de27 Mon Sep 17 00:00:00 2001 From: Ihor Malovanyi Date: Sat, 13 Jun 2026 09:52:24 +0200 Subject: [PATCH] [SwiftDiagnostics] Render attached notes in GroupedDiagnostics GroupedDiagnostics passed only the top-level diagnostics to the formatter, so notes attached to a diagnostic (which carry their own location and message) were silently dropped. Expand each diagnostic's notes into note-severity diagnostics so the formatter renders them at their locations alongside the diagnostic. Fixes #2166. --- .../SwiftDiagnostics/GroupedDiagnostics.swift | 25 +++++++++++- .../GroupDiagnosticsFormatterTests.swift | 40 ++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftDiagnostics/GroupedDiagnostics.swift b/Sources/SwiftDiagnostics/GroupedDiagnostics.swift index a1e8f507b6c..fcb9fd69ed5 100644 --- a/Sources/SwiftDiagnostics/GroupedDiagnostics.swift +++ b/Sources/SwiftDiagnostics/GroupedDiagnostics.swift @@ -260,11 +260,16 @@ extension GroupedDiagnostics { ) + boxSuffix + "\n" } - // Render the buffer. + // Render the buffer, expanding each diagnostic's attached notes into + // note-severity diagnostics so the formatter lays them out too. + let diagnostics = sourceFile.diagnostics.flatMap { diagnostic in + [diagnostic] + diagnostic.notes.map(\.asDiagnostic) + } + return prefixString + formatter.annotatedSource( tree: sourceFile.tree, - diags: sourceFile.diagnostics, + diags: diagnostics, indentString: diagnosticDecorator.decorateBufferOutline(indentString), suffixTexts: childSources, sourceLocationConverter: slc @@ -289,3 +294,19 @@ extension DiagnosticsFormatter { return formatter.annotateSources(in: group) } } + +/// Renders a ``NoteMessage`` as a `.note`-severity ``DiagnosticMessage`` so that +/// notes can be laid out by ``DiagnosticsFormatter`` alongside their diagnostic. +private struct NoteAsDiagnosticMessage: DiagnosticMessage { + let noteMessage: NoteMessage + var message: String { noteMessage.message } + var diagnosticID: MessageID { noteMessage.noteID } + var severity: DiagnosticSeverity { .note } +} + +extension Note { + /// This note expressed as a `.note`-severity diagnostic anchored at its own location. + fileprivate var asDiagnostic: Diagnostic { + Diagnostic(node: node, position: position, message: NoteAsDiagnosticMessage(noteMessage: noteMessage)) + } +} diff --git a/Tests/SwiftDiagnosticsTest/GroupDiagnosticsFormatterTests.swift b/Tests/SwiftDiagnosticsTest/GroupDiagnosticsFormatterTests.swift index d4892a26020..47f56d115bc 100644 --- a/Tests/SwiftDiagnosticsTest/GroupDiagnosticsFormatterTests.swift +++ b/Tests/SwiftDiagnosticsTest/GroupDiagnosticsFormatterTests.swift @@ -85,6 +85,43 @@ final class GroupedDiagnosticsFormatterTests: XCTestCase { ) } + func testAttachedNotesAreRendered() { + var group = GroupedDiagnostics() + + _ = group.addTestFile( + """ + let 1️⃣value = 2️⃣undefined + """, + displayName: "test.swift", + diagnosticDescriptors: [ + DiagnosticDescriptor( + locationMarker: "2️⃣", + message: "cannot find 'undefined' in scope", + severity: .error, + noteDescriptors: [ + NoteDescriptor( + locationMarker: "1️⃣", + id: MessageID(domain: "test", id: "note"), + message: "'value' declared here" + ) + ] + ) + ] + ) + + let annotated = DiagnosticsFormatter.annotateSources(in: group) + assertStringsEqualWithDiff( + annotated, + """ + test.swift:1:13: error: cannot find 'undefined' in scope + 1 | let value = undefined + | | `- error: cannot find 'undefined' in scope + | `- note: 'value' declared here + + """ + ) + } + func testGroupingForMacroExpansion() { var group = GroupedDiagnostics() @@ -147,7 +184,8 @@ final class GroupedDiagnosticsFormatterTests: XCTestCase { |5 | } +--------------------------------------------------------------------- 6 | print("hello" - | `- error: expected ')' to end function call + | | `- error: expected ')' to end function call + | `- note: to match this opening '(' """ )