Skip to content

Commit 1fa1824

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 209bea7 commit 1fa1824

File tree

2 files changed

+248
-37
lines changed

2 files changed

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

Tests/SwiftDiagnosticsTest/GroupDiagnosticsFormatterTests.swift

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,6 @@ import SwiftSyntax
1717
import XCTest
1818
import _SwiftSyntaxTestSupport
1919

20-
struct SimpleDiagnosticMessage: DiagnosticMessage {
21-
let message: String
22-
let diagnosticID: MessageID
23-
let severity: DiagnosticSeverity
24-
}
25-
26-
extension SimpleDiagnosticMessage: FixItMessage {
27-
var fixItID: MessageID { diagnosticID }
28-
}
29-
3020
extension GroupedDiagnostics {
3121
/// Add a new test file to the group, starting with marked source and using
3222
/// the markers to add any suggested extra diagnostics at the marker
@@ -35,34 +25,22 @@ extension GroupedDiagnostics {
3525
_ markedSource: String,
3626
displayName: String,
3727
parent: (SourceFileID, AbsolutePosition)? = nil,
38-
extraDiagnostics: [String: (String, DiagnosticSeverity)] = [:]
28+
diagnosticDescriptors: [DiagnosticDescriptor]
3929
) -> (SourceFileID, [String: AbsolutePosition]) {
40-
// Parse the source file and produce parser diagnostics.
4130
let (markers, source) = extractMarkers(markedSource)
4231
let tree = Parser.parse(source: source)
43-
var diagnostics = ParseDiagnosticsGenerator.diagnostics(for: tree)
44-
45-
// Add on any extra diagnostics provided, at their marker locations.
46-
for (marker, (message, severity)) in extraDiagnostics {
47-
let pos = AbsolutePosition(utf8Offset: markers[marker]!)
48-
let node = tree.token(at: pos)!.parent!
49-
50-
let diag = Diagnostic(
51-
node: node,
52-
message: SimpleDiagnosticMessage(
53-
message: message,
54-
diagnosticID: MessageID(domain: "test", id: "conjured"),
55-
severity: severity
56-
)
57-
)
58-
diagnostics.append(diag)
32+
33+
let parserDiagnostics = ParseDiagnosticsGenerator.diagnostics(for: tree)
34+
35+
let additionalDiagnostics = diagnosticDescriptors.compactMap {
36+
$0.createDiagnostic(inSyntaxTree: tree, usingLocationMarkers: markers)
5937
}
6038

6139
let id = addSourceFile(
6240
tree: tree,
6341
displayName: displayName,
6442
parent: parent,
65-
diagnostics: diagnostics
43+
diagnostics: parserDiagnostics + additionalDiagnostics
6644
)
6745

6846
let markersWithAbsPositions = markers.map { (marker, pos) in
@@ -88,7 +66,13 @@ final class GroupedDiagnosticsFormatterTests: XCTestCase {
8866
print("hello"
8967
""",
9068
displayName: "main.swift",
91-
extraDiagnostics: ["1️⃣": ("in expansion of macro 'myAssert' here", .note)]
69+
diagnosticDescriptors: [
70+
DiagnosticDescriptor(
71+
locationMarker: "1️⃣",
72+
message: "in expansion of macro 'myAssert' here",
73+
severity: .note
74+
)
75+
]
9276
)
9377
let inExpansionNotePos = mainSourceMarkers["1️⃣"]!
9478

@@ -103,8 +87,12 @@ final class GroupedDiagnosticsFormatterTests: XCTestCase {
10387
""",
10488
displayName: "#myAssert",
10589
parent: (mainSourceID, inExpansionNotePos),
106-
extraDiagnostics: [
107-
"1️⃣": ("no matching operator '==' for types 'Double' and 'Int'", .error)
90+
diagnosticDescriptors: [
91+
DiagnosticDescriptor(
92+
locationMarker: "1️⃣",
93+
message: "no matching operator '==' for types 'Double' and 'Int'",
94+
severity: .error
95+
)
10896
]
10997
)
11098

@@ -143,7 +131,9 @@ final class GroupedDiagnosticsFormatterTests: XCTestCase {
143131
print("hello")
144132
""",
145133
displayName: "main.swift",
146-
extraDiagnostics: ["1️⃣": ("in expansion of macro 'myAssert' here", .note)]
134+
diagnosticDescriptors: [
135+
DiagnosticDescriptor(locationMarker: "1️⃣", message: "in expansion of macro 'myAssert' here", severity: .note)
136+
]
147137
)
148138
let inExpansionNotePos = mainSourceMarkers["1️⃣"]!
149139

@@ -158,8 +148,8 @@ final class GroupedDiagnosticsFormatterTests: XCTestCase {
158148
""",
159149
displayName: "#myAssert",
160150
parent: (mainSourceID, inExpansionNotePos),
161-
extraDiagnostics: [
162-
"1️⃣": ("in expansion of macro 'invertedEqualityCheck' here", .note)
151+
diagnosticDescriptors: [
152+
DiagnosticDescriptor(locationMarker: "1️⃣", message: "in expansion of macro 'invertedEqualityCheck' here", severity: .note)
163153
]
164154
)
165155
let inInnerExpansionNotePos = outerExpansionSourceMarkers["1️⃣"]!
@@ -171,8 +161,8 @@ final class GroupedDiagnosticsFormatterTests: XCTestCase {
171161
""",
172162
displayName: "#invertedEqualityCheck",
173163
parent: (outerExpansionSourceID, inInnerExpansionNotePos),
174-
extraDiagnostics: [
175-
"1️⃣": ("no matching operator '==' for types 'Double' and 'Int'", .error)
164+
diagnosticDescriptors: [
165+
DiagnosticDescriptor(locationMarker: "1️⃣", message: "no matching operator '==' for types 'Double' and 'Int'", severity: .error)
176166
]
177167
)
178168

0 commit comments

Comments
 (0)