Skip to content

Commit 7b01db2

Browse files
authored
Merge pull request #2981 from DougGregor/diagnostic-category
2 parents 19458ae + d877261 commit 7b01db2

File tree

9 files changed

+108
-8
lines changed

9 files changed

+108
-8
lines changed

Release Notes/602.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## New APIs
44

5+
- `DiagnosticMessage` has a new optional property, `category`, that providesa category name and documentation URL for a diagnostic.
6+
- Description: Tools often have many different diagnostics. Diagnostic categories allow tools to group several diagnostics together with documentation that can help users understand what the diagnostics mean and how to address them. This API allows diagnostics to provide this category information. The diagnostic renderer will provide the category at the end of the diagnostic message in the form `[#CategoryName]`.
7+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2981
8+
- Migration steps: None required. The new `category` property has optional type, and there is a default implementation that returns `nil`. Types that conform to `DiagnosticMessage` can choose to implement this property and provide a category when appropriate.
9+
510
## API Behavior Changes
611

712
## Deprecations

Sources/SwiftDiagnostics/DiagnosticDecorators/ANSIDiagnosticDecorator.swift

+28-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ extension DiagnosticDecorator where Self == ANSIDiagnosticDecorator {
4848
/// ```
4949
@_spi(Testing) public func decorateMessage(
5050
_ message: String,
51-
basedOnSeverity severity: DiagnosticSeverity
51+
basedOnSeverity severity: DiagnosticSeverity,
52+
category: DiagnosticCategory? = nil
5253
) -> String {
5354
let severityText: String
5455
let severityAnnotation: ANSIAnnotation
@@ -77,7 +78,24 @@ extension DiagnosticDecorator where Self == ANSIDiagnosticDecorator {
7778
resetAfterApplication: false
7879
)
7980

80-
return prefix + colorizeIfNotEmpty(message, usingAnnotation: .diagnosticText)
81+
// Append the [#CategoryName] suffix when there is a category.
82+
let categorySuffix: String
83+
if let category {
84+
// Make the category name a link to the documentation, if there is
85+
// documentation.
86+
let categoryName: String
87+
if let documentationURL = category.documentationURL {
88+
categoryName = ANSIAnnotation.hyperlink(category.name, to: "\(documentationURL)")
89+
} else {
90+
categoryName = category.name
91+
}
92+
93+
categorySuffix = " [#\(categoryName)]"
94+
} else {
95+
categorySuffix = ""
96+
}
97+
98+
return prefix + colorizeIfNotEmpty(message, usingAnnotation: .diagnosticText) + categorySuffix
8199
}
82100

83101
/// Decorates a source code buffer outline using ANSI cyan color codes.
@@ -220,4 +238,12 @@ private struct ANSIAnnotation {
220238
static var remarkText: Self {
221239
Self(color: .blue, trait: .bold)
222240
}
241+
242+
/// Forms a hyperlink to the given URL with the given text.
243+
///
244+
/// This follows the OSC 8 standard for hyperlinks that is supported by
245+
/// a number of different terminals.
246+
static func hyperlink(_ text: String, to url: String) -> String {
247+
"\u{001B}]8;;\(url)\u{001B}\\\(text)\u{001B}]8;;\u{001B}\\"
248+
}
223249
}

Sources/SwiftDiagnostics/DiagnosticDecorators/BasicDiagnosticDecorator.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ extension DiagnosticDecorator where Self == BasicDiagnosticDecorator {
3434
/// - Returns: A string that combines the severity-specific prefix and the original diagnostic message.
3535
@_spi(Testing) public func decorateMessage(
3636
_ message: String,
37-
basedOnSeverity severity: DiagnosticSeverity
37+
basedOnSeverity severity: DiagnosticSeverity,
38+
category: DiagnosticCategory? = nil
3839
) -> String {
3940
let severityText: String
4041

@@ -49,7 +50,10 @@ extension DiagnosticDecorator where Self == BasicDiagnosticDecorator {
4950
severityText = "remark"
5051
}
5152

52-
return severityText + ": " + message
53+
// Append the [#CategoryName] suffix when there is a category.
54+
let categorySuffix: String = category.map { category in " [#\(category.name)]" } ?? ""
55+
56+
return severityText + ": " + message + categorySuffix
5357
}
5458

5559
/// Passes through the source code buffer outline without modification.

Sources/SwiftDiagnostics/DiagnosticDecorators/DiagnosticDecorator.swift

+10-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ protocol DiagnosticDecorator {
3939
///
4040
/// - Returns: A decorated version of the diagnostic message, enhanced by visual cues like color, text styles, or other markers,
4141
/// as well as a severity-specific prefix, based on its severity level.
42-
func decorateMessage(_ message: String, basedOnSeverity severity: DiagnosticSeverity) -> String
42+
func decorateMessage(
43+
_ message: String,
44+
basedOnSeverity severity: DiagnosticSeverity,
45+
category: DiagnosticCategory?
46+
) -> String
4347

4448
/// Decorates the outline of a source code buffer to visually enhance its structure.
4549
///
@@ -69,6 +73,10 @@ extension DiagnosticDecorator {
6973
///
7074
/// - Returns: A decorated version of the diagnostic message, determined by its severity level.
7175
func decorateDiagnosticMessage(_ diagnosticMessage: DiagnosticMessage) -> String {
72-
decorateMessage(diagnosticMessage.message, basedOnSeverity: diagnosticMessage.severity)
76+
decorateMessage(
77+
diagnosticMessage.message,
78+
basedOnSeverity: diagnosticMessage.severity,
79+
category: diagnosticMessage.category
80+
)
7381
}
7482
}

Sources/SwiftDiagnostics/GroupedDiagnostics.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@ extension GroupedDiagnostics {
227227
let bufferLoc = slc.location(for: rootPosition)
228228
let decoratedMessage = diagnosticDecorator.decorateMessage(
229229
"expanded code originates here",
230-
basedOnSeverity: .note
230+
basedOnSeverity: .note,
231+
category: nil
231232
)
232233
prefixString += "`- \(bufferLoc.file):\(bufferLoc.line):\(bufferLoc.column): \(decoratedMessage)\n"
233234
}

Sources/SwiftDiagnostics/Message.swift

+23
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,21 @@ public enum DiagnosticSeverity: Sendable, Hashable {
3333
case remark
3434
}
3535

36+
/// Describes a category of diagnostics, which covers a set of related
37+
/// diagnostics that can share documentation.
38+
public struct DiagnosticCategory: Sendable, Hashable {
39+
/// Name that identifies the category, e.g., StrictMemorySafety.
40+
public let name: String
41+
42+
/// URL providing documentation documentation for this category.
43+
public let documentationURL: String?
44+
45+
public init(name: String, documentationURL: String?) {
46+
self.name = name
47+
self.documentationURL = documentationURL
48+
}
49+
}
50+
3651
/// Types conforming to this protocol represent diagnostic messages that can be
3752
/// shown to the client.
3853
public protocol DiagnosticMessage: Sendable {
@@ -43,4 +58,12 @@ public protocol DiagnosticMessage: Sendable {
4358
var diagnosticID: MessageID { get }
4459

4560
var severity: DiagnosticSeverity { get }
61+
62+
/// The category that this diagnostic belongs in.
63+
var category: DiagnosticCategory? { get }
64+
}
65+
66+
extension DiagnosticMessage {
67+
/// Diagnostic messages default to having no category.
68+
public var category: DiagnosticCategory? { nil }
4669
}

Tests/SwiftDiagnosticsTest/DiagnosticDecorators/ANSIDiagnosticDecoratorTests.swift

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ final class ANSIDiagnosticDecoratorTests: XCTestCase {
3434

3535
let decoratedMessageForRemark = decorator.decorateMessage(message, basedOnSeverity: .remark)
3636
assertStringsEqualWithDiff(decoratedMessageForRemark, "\u{1B}[1;34mremark: \u{1B}[1;39mFile not found\u{1B}[0;0m")
37+
38+
let decoratedMessageWithCategory = decorator.decorateMessage(
39+
message,
40+
basedOnSeverity: .error,
41+
category: DiagnosticCategory(name: "Filesystem", documentationURL: "http://www.swift.org")
42+
)
43+
assertStringsEqualWithDiff(
44+
decoratedMessageWithCategory,
45+
"\u{1B}[1;31merror: \u{1B}[1;39mFile not found\u{1B}[0;0m [#\u{001B}]8;;http://www.swift.org\u{001B}\\Filesystem\u{001B}]8;;\u{001B}\\]"
46+
)
3747
}
3848

3949
func testDecorateMessageWithEmptyMessage() {

Tests/SwiftDiagnosticsTest/DiagnosticDecorators/BasicDiagnosticDecoratorTests.swift

+7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ final class BasicDiagnosticDecoratorTests: XCTestCase {
3434

3535
let decoratedMessageForRemark = decorator.decorateMessage(message, basedOnSeverity: .remark)
3636
assertStringsEqualWithDiff(decoratedMessageForRemark, "remark: File not found")
37+
38+
let decoratedMessageWithCategory = decorator.decorateMessage(
39+
message,
40+
basedOnSeverity: .error,
41+
category: DiagnosticCategory(name: "Filesystem", documentationURL: "http://www.swift.org")
42+
)
43+
assertStringsEqualWithDiff(decoratedMessageWithCategory, "error: File not found [#Filesystem]")
3744
}
3845

3946
// MARK: - Decorate Buffer Outline Tests

Tests/SwiftDiagnosticsTest/DiagnosticTestingUtils.swift

+17-1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ struct DiagnosticDescriptor {
6262
/// The severity level of the diagnostic message.
6363
let severity: DiagnosticSeverity
6464

65+
/// The diagnostic category.
66+
let category: DiagnosticCategory?
67+
6568
/// The syntax elements to be highlighted for this diagnostic message.
6669
let highlight: [Syntax] // TODO: How to create an abstract model for this?
6770

@@ -86,6 +89,7 @@ struct DiagnosticDescriptor {
8689
id: MessageID = MessageID(domain: "test", id: "conjured"),
8790
message: String,
8891
severity: DiagnosticSeverity = .error,
92+
category: DiagnosticCategory? = nil,
8993
highlight: [Syntax] = [],
9094
noteDescriptors: [NoteDescriptor] = [],
9195
fixIts: [FixIt] = []
@@ -94,6 +98,7 @@ struct DiagnosticDescriptor {
9498
self.id = id
9599
self.message = message
96100
self.severity = severity
101+
self.category = category
97102
self.highlight = highlight
98103
self.noteDescriptors = noteDescriptors
99104
self.fixIts = fixIts
@@ -139,7 +144,8 @@ struct DiagnosticDescriptor {
139144
message: SimpleDiagnosticMessage(
140145
message: self.message,
141146
diagnosticID: self.id,
142-
severity: self.severity
147+
severity: self.severity,
148+
category: category
143149
),
144150
highlights: self.highlight,
145151
notes: notes,
@@ -181,6 +187,16 @@ struct SimpleDiagnosticMessage: DiagnosticMessage {
181187

182188
/// The severity level of the diagnostic message.
183189
let severity: DiagnosticSeverity
190+
191+
/// The category for this diagnostic.
192+
let category: DiagnosticCategory?
193+
194+
init(message: String, diagnosticID: MessageID, severity: DiagnosticSeverity, category: DiagnosticCategory? = nil) {
195+
self.message = message
196+
self.diagnosticID = diagnosticID
197+
self.severity = severity
198+
self.category = category
199+
}
184200
}
185201

186202
/// Asserts that the annotated source generated from diagnostics matches an expected annotated source.

0 commit comments

Comments
 (0)