Skip to content

Commit 09e5675

Browse files
committed
Add diagnostic testing utils for enhanced diagnostics testing
Introduces `DiagnosticTestingUtils.swift`, a utility suite designed to aid in writing unit tests for `DiagnosticsFormatter` and `GroupedDiagnostics`. Highlights include: 1. `LocationMarker` Typealias: - Enhances readability and precision in location identification within AST. 2. `DiagnosticDescriptor` and `NoteDescriptor` Structs: - Offers a robust mechanism to construct and describe diagnostics and notes for testing. 3. Simple Implementations for Protocols: - `SimpleNoteMessage` and `SimpleDiagnosticMessage` for streamlined testing. 4. `assertAnnotated` Function: - Asserts that annotated source generated from diagnostics aligns with the expected output. This addition significantly bolsters the testing utilities, providing a comprehensive framework for ensuring accurate and effective diagnostics.
1 parent 924dd7e commit 09e5675

File tree

1 file changed

+216
-0
lines changed

1 file changed

+216
-0
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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+
import SwiftDiagnostics
14+
import SwiftParser
15+
import SwiftSyntax
16+
import XCTest
17+
import _SwiftSyntaxTestSupport
18+
19+
/// A typealias representing a location marker.
20+
///
21+
/// This string serves to pinpoint the exact location of a particular token in the Abstract Syntax Tree (AST).
22+
/// Once the token location is identified, it can be leveraged for various test-specific operations such as inserting diagnostics, notes, or fix-its,
23+
/// or for closer examination of the syntax tree.
24+
///
25+
/// Markers are instrumental in writing unit tests that require precise location data. They are commonly represented using emojis like 1️⃣, 2️⃣, 3️⃣, etc., to improve readability.
26+
///
27+
/// ### Example
28+
///
29+
/// In the following test code snippet, the emojis 1️⃣ and 2️⃣ are used as location markers:
30+
///
31+
/// ```swift
32+
/// func foo() -> Int {
33+
/// if 1️⃣1 != 0 2️⃣{
34+
/// return 0
35+
/// }
36+
/// return 1
37+
/// }
38+
/// ```
39+
typealias LocationMarker = String
40+
41+
/// Represents a descriptor for constructing a diagnostic in testing.
42+
struct DiagnosticDescriptor {
43+
/// The marker pointing to location in source code.
44+
let locationMarker: LocationMarker
45+
46+
/// The ID associated with the message, used for categorizing or referencing it.
47+
let id: MessageID
48+
49+
/// The textual content of the message to be displayed.
50+
let message: String
51+
52+
/// The severity level of the diagnostic message.
53+
let severity: DiagnosticSeverity
54+
55+
/// The syntax elements to be highlighted for this diagnostic message.
56+
let highlight: [Syntax] // TODO: How to create an abstract model for this?
57+
58+
/// Descriptors for any accompanying notes for this diagnostic message.
59+
let noteDescriptors: [NoteDescriptor]
60+
61+
/// Descriptors for any Fix-Its that can be applied for this diagnostic message.
62+
let fixIts: [FixIt] // TODO: How to create an abstract model for this?
63+
64+
/// Initializes a new `DiagnosticDescriptor`.
65+
///
66+
/// - Parameters:
67+
/// - locationMarker: The marker pointing to location in source code.
68+
/// - id: The message ID of the diagnostic.
69+
/// - message: The textual message to display for the diagnostic.
70+
/// - severity: The severity level of the diagnostic. Default is `.error`.
71+
/// - highlight: The syntax elements to be highlighted. Default is an empty array.
72+
/// - noteDescriptors: An array of note descriptors for additional context. Default is an empty array.
73+
/// - fixIts: An array of Fix-It descriptors for quick fixes. Default is an empty array.
74+
init(
75+
locationMarker: LocationMarker,
76+
id: MessageID,
77+
message: String,
78+
severity: DiagnosticSeverity = .error,
79+
highlight: [Syntax] = [],
80+
noteDescriptors: [NoteDescriptor] = [],
81+
fixIts: [FixIt] = []
82+
) {
83+
self.locationMarker = locationMarker
84+
self.id = id
85+
self.message = message
86+
self.severity = severity
87+
self.highlight = highlight
88+
self.noteDescriptors = noteDescriptors
89+
self.fixIts = fixIts
90+
}
91+
92+
/// Creates a ``Diagnostic`` instance from a given ``DiagnosticDescriptor``, syntax tree, and location markers.
93+
///
94+
/// - Parameters:
95+
/// - tree: The syntax tree where the diagnostic is rooted.
96+
/// - markers: A dictionary mapping location markers to their respective offsets in the source code.
97+
/// - file: The file where the test is located, used for reporting failures.
98+
/// - line: The line where the test is located, used for reporting failures.
99+
///
100+
/// - Returns: A ``Diagnostic`` instance populated with details from the ``DiagnosticDescriptor``, or `nil` if it fails.
101+
func createDiagnostic(
102+
inSyntaxTree tree: some SyntaxProtocol,
103+
usingLocationMarkers markers: [LocationMarker: Int],
104+
file: StaticString = #file,
105+
line: UInt = #line
106+
) -> Diagnostic? {
107+
func node(at marker: LocationMarker) -> Syntax? {
108+
guard let markedOffset = markers[marker] else {
109+
XCTFail("Marker \(marker) not found in the marked source", file: file, line: line)
110+
return nil
111+
}
112+
let markedPosition = AbsolutePosition(utf8Offset: markedOffset)
113+
guard let token = tree.token(at: markedPosition) else {
114+
XCTFail("Node not found at marker \(marker)", file: file, line: line)
115+
return nil
116+
}
117+
return Syntax(token)
118+
}
119+
120+
guard let diagnosticNode = node(at: self.locationMarker) else { return nil }
121+
122+
var notes = [Note]()
123+
for noteDescriptor in self.noteDescriptors {
124+
guard let noteNode = node(at: noteDescriptor.locationMarker) else { continue }
125+
126+
let note = Note(
127+
node: noteNode,
128+
message: SimpleNoteMessage(message: noteDescriptor.message, fixItID: noteDescriptor.id)
129+
)
130+
notes.append(note)
131+
}
132+
133+
return Diagnostic(
134+
node: diagnosticNode,
135+
message: SimpleDiagnosticMessage(
136+
message: self.message,
137+
diagnosticID: self.id,
138+
severity: self.severity
139+
),
140+
highlights: self.highlight,
141+
notes: notes,
142+
fixIts: self.fixIts
143+
)
144+
}
145+
}
146+
147+
/// Represents a descriptor for constructing a note message in testing.
148+
struct NoteDescriptor {
149+
/// The marker pointing to location in source code.
150+
let locationMarker: LocationMarker
151+
152+
/// The ID associated with the note message.
153+
let id: MessageID
154+
155+
/// The textual content of the note to be displayed.
156+
let message: String
157+
}
158+
159+
/// A simple implementation of the `NoteMessage` protocol for testing.
160+
/// This struct holds the message text and a fix-it ID for a note.
161+
struct SimpleNoteMessage: NoteMessage {
162+
/// The textual content of the note to be displayed.
163+
let message: String
164+
165+
/// The ID associated with the fix-it, if applicable.
166+
let fixItID: MessageID
167+
}
168+
169+
/// A simple implementation of the `DiagnosticMessage` protocol for testing.
170+
/// This struct holds the message text, diagnostic ID, and severity for a diagnostic.
171+
struct SimpleDiagnosticMessage: DiagnosticMessage {
172+
/// The textual content of the diagnostic message to be displayed.
173+
let message: String
174+
175+
/// The ID associated with the diagnostic message for categorization or referencing.
176+
let diagnosticID: MessageID
177+
178+
/// The severity level of the diagnostic message.
179+
let severity: DiagnosticSeverity
180+
}
181+
182+
/// Asserts that the annotated source generated from diagnostics matches an expected annotated source.
183+
///
184+
/// - Parameters:
185+
/// - markedSource: The source code with location markers `LocationMarker` for diagnostics.
186+
/// - withDiagnostics: An array of diagnostic descriptors to generate diagnostics.
187+
/// - matches: The expected annotated source after applying the diagnostics.
188+
/// - file: The file in which failure occurred.
189+
/// - line: The line number on which failure occurred.
190+
func assertAnnotated(
191+
markedSource: String,
192+
withDiagnostics diagnosticDescriptors: [DiagnosticDescriptor],
193+
matches expectedAnnotatedSource: String,
194+
file: StaticString = #file,
195+
line: UInt = #line
196+
) {
197+
198+
let (markers, source) = extractMarkers(markedSource)
199+
let tree = Parser.parse(source: source)
200+
201+
// if ParseDiagnosticsGenerator.diagnostics(for: tree) contains any diagnostics
202+
// run XCTFail with information.
203+
204+
let diagnostics = diagnosticDescriptors.compactMap {
205+
$0.createDiagnostic(inSyntaxTree: tree, usingLocationMarkers: markers)
206+
}
207+
208+
let annotatedSource = DiagnosticsFormatter.annotatedSource(inSyntaxTree: tree, withDiagnostics: diagnostics)
209+
210+
assertStringsEqualWithDiff(
211+
annotatedSource,
212+
expectedAnnotatedSource,
213+
file: file,
214+
line: line
215+
)
216+
}

0 commit comments

Comments
 (0)