Skip to content

Commit 52f76cd

Browse files
committed
Introduce diagnostic decorator
This commit introduces three significant components: 1. `DiagnosticDecorator` Protocol: - Defines a standard interface for decorating diagnostic output in source code. - Design to be used by entities like `DiagnosticsFormatter` and `GroupedDiagnostics`. 2. `ANSIDiagnosticDecorator` Struct: - Implements the `DiagnosticDecorator` protocol. - Adds severity-based prefixes to diagnostic messages. - Supports ANSI colorization to enhance visibility and interpretation. 3. `BasicDiagnosticDecorator` Struct: - Implements the `DiagnosticDecorator` protocol. - Adds severity-based prefixes to diagnostic messages. Also includes: - Unit tests for `ANSIDiagnosticDecorator` in `ANSIDiagnosticDecoratorTests` and `BasicDiagnosticDecorator` in `BasicDiagnosticDecoratorTests`. This feature enriches the diagnostic capabilities, offering customizable and visually informative feedback.
1 parent f6a7999 commit 52f76cd

8 files changed

+521
-156
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
extension DiagnosticDecorator where Self == ANSIDiagnosticDecorator {
14+
/// - SeeAlso: ``ANSIDiagnosticDecorator``
15+
static var ANSI: Self {
16+
Self()
17+
}
18+
}
19+
20+
/// An implementation of the `DiagnosticDecorator` protocol that enhances various diagnostic elements—including messages,
21+
/// buffer outlines, and code highlights—by applying severity-based prefixes and ANSI color codes.
22+
///
23+
/// This decorator uses ANSI codes—control characters specialized for text formatting in terminals—to provide visual cues.
24+
@_spi(Testing) public struct ANSIDiagnosticDecorator: DiagnosticDecorator {
25+
26+
@_spi(Testing) public init() {}
27+
28+
/// Decorates a diagnostic message by appending a severity-based prefix and applying ANSI color codes.
29+
///
30+
/// - Parameters:
31+
/// - message: The diagnostic message that needs to be decorated.
32+
/// - severity: The severity level associated with the diagnostic message.
33+
///
34+
/// - Returns: A string that combines the severity-specific prefix and the original diagnostic message, with ANSI colorization.
35+
///
36+
/// ## Example
37+
///
38+
/// ```swift
39+
/// let decorator = ANSIDiagnosticDecorator()
40+
/// let decoratedMessage = decorator.decorateMessage("File not found", basedOnSeverity: .error)
41+
/// // Output would be: "error: File not found"
42+
/// ```
43+
/// In this example, the "error: " prefix is colorized, likely appearing in red, while the message retains its default text color.
44+
///
45+
/// For a similar colorized output in the console, you can use `printf` in Bash:
46+
/// ```bash
47+
/// printf "\e[1;31merror: \e[1;39mFile not found\e[0;0m\n"
48+
/// ```
49+
@_spi(Testing) public func decorateMessage(_ message: String, basedOnSeverity severity: DiagnosticSeverity) -> String {
50+
let severityText: String
51+
let severityAnnotation: ANSIAnnotation
52+
53+
switch severity {
54+
case .error:
55+
severityText = "error"
56+
severityAnnotation = .errorText
57+
58+
case .warning:
59+
severityText = "warning"
60+
severityAnnotation = .warningText
61+
62+
case .note:
63+
severityText = "note"
64+
severityAnnotation = .noteText
65+
66+
case .remark:
67+
severityText = "remark"
68+
severityAnnotation = .remarkText
69+
}
70+
71+
let prefix = colorizeIfNotEmpty("\(severityText): ", usingAnnotation: severityAnnotation, resetAfterApplication: false)
72+
73+
return prefix + colorizeIfNotEmpty(message, usingAnnotation: .diagnosticText)
74+
}
75+
76+
/// Decorates a source code buffer outline using ANSI cyan color codes.
77+
///
78+
/// - Parameter bufferOutline: The string representation of the source code buffer outline.
79+
///
80+
/// - Returns: A string featuring ANSI cyan color codes applied to the source code buffer outline.
81+
@_spi(Testing) public func decorateBufferOutline(_ bufferOutline: String) -> String {
82+
colorizeIfNotEmpty(bufferOutline, usingAnnotation: .bufferOutline)
83+
}
84+
85+
/// Emphasizes a specific text segment within a source code snippet using ANSI color codes.
86+
///
87+
/// - Parameter highlight: The text segment within the source code snippet that should be emphasized.
88+
///
89+
/// - Returns: A tuple containing:
90+
/// - `highlightedSourceCode`: The underlined version of the original source code snippet.
91+
/// - `additionalHighlightedLine`: Always nil.
92+
///
93+
/// ## Example
94+
///
95+
/// ```swift
96+
/// let decorator = ANSIDiagnosticDecorator()
97+
/// let decoratedHighlight = decorator.decorateHighlight("let x = 10")
98+
/// // Output would be: ["\u{1B}[4;39mlet x = 10\u{1B}[0;0m"]
99+
/// ```
100+
///
101+
/// To reproduce a similar colorized output manually in the console, you can use `printf` in Bash:
102+
/// ```bash
103+
/// printf "\e[4;39mlet x = 10\e[0;0m\n"
104+
/// ```
105+
@_spi(Testing) public func decorateHighlight(_ highlight: String) -> (highlightedSourceCode: String, additionalHighlightedLine: String?) {
106+
(highlightedSourceCode: colorizeIfNotEmpty(highlight, usingAnnotation: .sourceHighlight), additionalHighlightedLine: nil)
107+
}
108+
109+
/// Applies ANSI annotation to a given text segment, if the text is not empty.
110+
///
111+
/// - Parameters:
112+
/// - text: The text segment to which the annotation should be applied.
113+
/// - annotation: The ANSI annotation to apply.
114+
/// - resetAfter: A flag indicating whether to reset ANSI settings after applying them. Defaults to true.
115+
///
116+
/// - Returns: A potentially colorized version of the input text.
117+
private func colorizeIfNotEmpty(
118+
_ text: String,
119+
usingAnnotation annotation: ANSIAnnotation,
120+
resetAfterApplication resetAfter: Bool = true
121+
) -> String {
122+
if text.isEmpty {
123+
return text
124+
} else {
125+
return annotation.applied(to: text, resetAfter: resetAfter)
126+
}
127+
}
128+
}
129+
130+
/// Defines text attributes to be applied to console output.
131+
private struct ANSIAnnotation {
132+
/// Represents ANSI color codes.
133+
enum Color: UInt8 {
134+
case normal = 0
135+
case black = 30
136+
case red = 31
137+
case green = 32
138+
case yellow = 33
139+
case blue = 34
140+
case magenta = 35
141+
case cyan = 36
142+
case white = 37
143+
case `default` = 39
144+
}
145+
146+
/// Represents ANSI text traits.
147+
enum Trait: UInt8 {
148+
case normal = 0
149+
case bold = 1
150+
case underline = 4
151+
}
152+
153+
/// The ANSI color to be used.
154+
let color: Color
155+
156+
/// The ANSI text trait to be used.
157+
let trait: Trait
158+
159+
/// Returns ANSI code as a string, including both trait and color.
160+
var code: String {
161+
"\u{001B}[\(trait.rawValue);\(color.rawValue)m"
162+
}
163+
164+
/// Applies the ANSI code to a message string. Optionally resets the code after the message.
165+
func applied(to message: String, resetAfter: Bool = true) -> String {
166+
guard resetAfter else {
167+
return "\(code)\(message)"
168+
}
169+
return "\(code)\(message)\(ANSIAnnotation.normal.code)"
170+
}
171+
172+
/// The default 'normal' ANSIAnnotation used to reset styles.
173+
static var normal: Self {
174+
Self(color: .normal, trait: .normal)
175+
}
176+
177+
/// Annotation used for the outline and line numbers of a buffer.
178+
static var bufferOutline: Self {
179+
Self(color: .cyan, trait: .normal)
180+
}
181+
182+
/// Annotation used for highlighting source text.
183+
static var sourceHighlight: Self {
184+
Self(color: .default, trait: .underline)
185+
}
186+
187+
/// Annotation used for making text bold, commonly used in diagnostic messages.
188+
static var diagnosticText: Self {
189+
Self(color: .default, trait: .bold)
190+
}
191+
192+
/// Annotation used for error text.
193+
static var errorText: Self {
194+
Self(color: .red, trait: .bold)
195+
}
196+
197+
/// Annotation used for warning text.
198+
static var warningText: Self {
199+
Self(color: .yellow, trait: .bold)
200+
}
201+
202+
/// Annotation used for note text.
203+
static var noteText: Self {
204+
Self(color: .default, trait: .bold)
205+
}
206+
207+
/// Annotation used for remarks or less critical text.
208+
static var remarkText: Self {
209+
Self(color: .blue, trait: .bold)
210+
}
211+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
extension DiagnosticDecorator where Self == BasicDiagnosticDecorator {
14+
/// - Seealso: ``BasicDiagnosticDecorator``
15+
static var basic: Self {
16+
Self()
17+
}
18+
}
19+
20+
/// An implementation of the `DiagnosticDecorator` protocol that enhances diagnostic elements—such as messages,
21+
/// buffer outlines, and code highlights—by appending severity-based prefixes.
22+
///
23+
/// Unlike `ANSIDiagnosticDecorator`, this decorator does not use ANSI color codes and solely relies on textual cues.
24+
@_spi(Testing) public struct BasicDiagnosticDecorator: DiagnosticDecorator {
25+
26+
@_spi(Testing) public init() {}
27+
28+
/// Decorates a diagnostic message by appending a severity-based prefix.
29+
///
30+
/// - Parameters:
31+
/// - message: The diagnostic message that needs to be decorated.
32+
/// - severity: The severity level associated with the diagnostic message.
33+
///
34+
/// - Returns: A string that combines the severity-specific prefix and the original diagnostic message.
35+
@_spi(Testing) public func decorateMessage(_ message: String, basedOnSeverity severity: DiagnosticSeverity) -> String {
36+
let severityText: String
37+
38+
switch severity {
39+
case .error:
40+
severityText = "error"
41+
case .warning:
42+
severityText = "warning"
43+
case .note:
44+
severityText = "note"
45+
case .remark:
46+
severityText = "remark"
47+
}
48+
49+
return severityText + ": " + message
50+
}
51+
52+
/// Passes through the source code buffer outline without modification.
53+
///
54+
/// - Parameter bufferOutline: The string representation of the source code buffer outline.
55+
///
56+
/// - Returns: The original source code buffer outline.
57+
@_spi(Testing) public func decorateBufferOutline(_ bufferOutline: String) -> String {
58+
return bufferOutline
59+
}
60+
61+
/// Passes through the text segment within a source code snippet without modification.
62+
///
63+
/// - Parameter highlight: The text segment within the source code snippet that should be emphasized.
64+
///
65+
/// - Returns: A tuple containing:
66+
/// - `highlightedSourceCode`: The original text segment.
67+
/// - `additionalHighlightedLine`: Always nil.
68+
@_spi(Testing) public func decorateHighlight(_ highlight: String) -> (highlightedSourceCode: String, additionalHighlightedLine: String?) {
69+
return (highlightedSourceCode: highlight, additionalHighlightedLine: nil)
70+
}
71+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Protocol that defines a standard interface for decorating diagnostic output in source code.
14+
///
15+
/// This protocol is intended to be used by entities such as ``DiagnosticsFormatter`` and ``GroupedDiagnostics``
16+
/// to apply custom decorations to diagnostic messages, buffer outlines, and code highlights.
17+
///
18+
/// ## Conforming to `DiagnosticDecorator`:
19+
///
20+
/// To conform to the `DiagnosticDecorator` protocol, you must implement three required methods:
21+
///
22+
/// 1. `decorateMessage(_:basedOnSeverity:)`: For decorating diagnostic messages.
23+
/// 2. `decorateBufferOutline(_:)`: For decorating the outlines of source code buffers.
24+
/// 3. `decorateHighlight(_:)`: For decorating individual highlights within a source code snippet.
25+
///
26+
/// ## Customization:
27+
///
28+
/// The protocol is designed to be easily customizable. Developers can create their own entities that conform
29+
/// to `DiagnosticDecorator` to implement custom decorating logic. This allows for different visual representations,
30+
/// such as using ANSI colors, underscores, emoji-based or other markers, for diagnostics in source code.
31+
protocol DiagnosticDecorator {
32+
/// Decorates a diagnostic message based on its severity level.
33+
///
34+
/// Implementations are expected to prepend a severity-specific prefix (e.g., "error: ", "warning: ") to the diagnostic message.
35+
///
36+
/// - Parameters:
37+
/// - message: The diagnostic message that needs to be decorated.
38+
/// - severity: The severity level associated with the diagnostic message.
39+
///
40+
/// - Returns: A decorated version of the diagnostic message, enhanced by visual cues like color, text styles, or other markers,
41+
/// as well as a severity-specific prefix, based on its severity level.
42+
func decorateMessage(_ message: String, basedOnSeverity severity: DiagnosticSeverity) -> String
43+
44+
/// Decorates the outline of a source code buffer to visually enhance its structure.
45+
///
46+
/// - Parameter bufferOutline: The string representation of the source code buffer outline.
47+
///
48+
/// - Returns: A decorated version of the buffer outline, improved with visual cues like color, text styles, or other markers.
49+
func decorateBufferOutline(_ bufferOutline: String) -> String
50+
51+
/// Decorates a highlight within a source code snippet to emphasize it.
52+
///
53+
/// - Parameter highlight: The text segment within the source code snippet that should be emphasized.
54+
///
55+
/// - Returns: A tuple containing:
56+
/// - `highlightedSourceCode`: A string that represents the decorated version of the original source code snippet.
57+
/// - `additionalHighlightedLine`: An optional string containing additional lines of highlighting, if applicable.
58+
///
59+
/// - Note: The method returns a tuple to offer more flexibility in decorating highlights.
60+
/// This allows for a variety of techniques to be used, such as ANSI codes for color
61+
/// and additional lines for contextual emphasis, which will be combined during the rendering process.
62+
func decorateHighlight(_ highlight: String) -> (highlightedSourceCode: String, additionalHighlightedLine: String?)
63+
}
64+
65+
extension DiagnosticDecorator {
66+
/// Decorates a ``DiagnosticMessage`` instance by delegating to the `decorateMessage(_:basedOnSeverity:)` method.
67+
///
68+
/// - Parameter diagnosticMessage: The ``DiagnosticMessage`` instance that encapsulates both the message and its severity level.
69+
///
70+
/// - Returns: A decorated version of the diagnostic message, determined by its severity level.
71+
func decorateDiagnosticMessage(_ diagnosticMessage: DiagnosticMessage) -> String {
72+
decorateMessage(diagnosticMessage.message, basedOnSeverity: diagnosticMessage.severity)
73+
}
74+
}

0 commit comments

Comments
 (0)