Skip to content

Commit de16b8a

Browse files
committed
Improve assertMacroExpansion highlight verification (#2213)
Adds a new initializer to DiagnosticSpec which takes an optional array of highlights instead of a single optional highlight string. Updates existing initializer to map the string into an array of one. Updates assertMacroExpansion to check each highlight in the diagnostic spec against the actual produced diagnostic individually. Additionally changes the behavior to drop leading and trailing trivial when performing the highlight comparison to better match the Swift compiler's behavior.
1 parent e0c65b2 commit de16b8a

File tree

4 files changed

+174
-22
lines changed

4 files changed

+174
-22
lines changed

Package.swift

+5
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ let package = Package(
213213
dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftParser", "SwiftSyntaxMacros", "SwiftSyntaxMacroExpansion"]
214214
),
215215

216+
.testTarget(
217+
name: "SwiftSyntaxMacrosTestSupportTests",
218+
dependencies: ["SwiftDiagnostics", "SwiftSyntax", "SwiftSyntaxMacros", "SwiftSyntaxMacrosTestSupport"]
219+
),
220+
216221
// MARK: SwiftParser
217222

218223
.target(

Release Notes/510.md

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
- Issue: https://github.com/apple/swift-syntax/issues/2261
4848
- Pull Request: https://github.com/apple/swift-syntax/pull/2264
4949

50+
- `DiagnosticSpec.highlight` replaced by `highlights`
51+
- Description: The use of a single string `highlight` prevented users from asserting that a macro highlighted exactly the expected set of syntax nodes. Use of `DiagnosticSpec.init(...highlight:...)` is deprecated and forwards to `DiagnosticSpec.init(...highlights:...)`. Migrating from `highlight` to `highlights` is straightforward; any uses of `DiagnosticSpec.init` which do not specify a `highlight` do not need to change, otherwise:
52+
- If the diagnostic highlights a single node, the `highlight` string should be replaced with a single element array containing the same string without any trailing trivia, e.g., `highlight: "let "` -> `highlights: ["let"]`.
53+
- If the diagnostic highlights multiple nodes, the `highlight` string should be replaced with an array containing an element for each highlighted node, e.g., `highlight: "struct {}"` -> `highlights: ["struct", "{}"]`.
54+
- Pull Request: https://github.com/apple/swift-syntax/pull/2213
55+
5056
## Template
5157

5258
- *Affected API or two word description*

Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift

+62-22
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ public struct DiagnosticSpec {
128128
/// The expected severity of the diagnostic
129129
public let severity: DiagnosticSeverity
130130

131-
/// If not `nil`, the text the diagnostic is expected to highlight
132-
public let highlight: String?
131+
/// If not `nil`, the text fragments the diagnostic is expected to highlight
132+
public let highlights: [String]?
133133

134134
/// The notes that are expected to be attached to the diagnostic
135135
public let notes: [NoteSpec]
@@ -141,15 +141,15 @@ public struct DiagnosticSpec {
141141
internal let originatorFile: StaticString
142142
internal let originatorLine: UInt
143143

144-
/// Creates a new ``DiagnosticSpec`` that describes a diagnsotic tests are expecting to be generated by a macro expansion.
144+
/// Creates a new ``DiagnosticSpec`` that describes a diagnostic tests are expecting to be generated by a macro expansion.
145145
///
146146
/// - Parameters:
147147
/// - id: If not `nil`, the ID, which the diagnostic is expected to have.
148148
/// - message: The expected message of the diagnostic
149149
/// - line: The line to which the diagnostic is expected to point
150150
/// - column: The column to which the diagnostic is expected to point
151151
/// - severity: The expected severity of the diagnostic
152-
/// - highlight: If not `nil`, the text the diagnostic is expected to highlight
152+
/// - highlights: If not empty, the text fragments the diagnostic is expected to highlight
153153
/// - notes: The notes that are expected to be attached to the diagnostic
154154
/// - fixIts: The messages of the Fix-Its the diagnostic is expected to produce
155155
/// - originatorFile: The file at which this ``NoteSpec`` was created, so that assertion failures can be reported at its location.
@@ -160,7 +160,7 @@ public struct DiagnosticSpec {
160160
line: Int,
161161
column: Int,
162162
severity: DiagnosticSeverity = .error,
163-
highlight: String? = nil,
163+
highlights: [String]? = nil,
164164
notes: [NoteSpec] = [],
165165
fixIts: [FixItSpec] = [],
166166
originatorFile: StaticString = #file,
@@ -171,14 +171,50 @@ public struct DiagnosticSpec {
171171
self.line = line
172172
self.column = column
173173
self.severity = severity
174-
self.highlight = highlight
174+
self.highlights = highlights
175175
self.notes = notes
176176
self.fixIts = fixIts
177177
self.originatorFile = originatorFile
178178
self.originatorLine = originatorLine
179179
}
180180
}
181181

182+
extension DiagnosticSpec {
183+
@available(*, deprecated, message: "Use highlights instead")
184+
public var highlight: String? {
185+
guard let highlights else {
186+
return nil
187+
}
188+
return highlights.joined(separator: " ")
189+
}
190+
191+
@_disfavoredOverload
192+
@available(*, deprecated, message: "Use init(id:message:line:column:severity:highlights:notes:fixIts:originatorFile:originatorLine:) instead")
193+
public init(
194+
id: MessageID? = nil,
195+
message: String,
196+
line: Int,
197+
column: Int,
198+
severity: DiagnosticSeverity = .error,
199+
highlight: String? = nil,
200+
notes: [NoteSpec] = [],
201+
fixIts: [FixItSpec] = [],
202+
originatorFile: StaticString = #file,
203+
originatorLine: UInt = #line
204+
) {
205+
self.init(
206+
id: id,
207+
message: message,
208+
line: line,
209+
column: column,
210+
severity: severity,
211+
highlights: highlight.map { [$0] },
212+
notes: notes,
213+
fixIts: fixIts
214+
)
215+
}
216+
}
217+
182218
func assertDiagnostic(
183219
_ diag: Diagnostic,
184220
in expansionContext: BasicMacroExpansionContext,
@@ -194,23 +230,27 @@ func assertDiagnostic(
194230

195231
XCTAssertEqual(spec.severity, diag.diagMessage.severity, "severity does not match", file: spec.originatorFile, line: spec.originatorLine)
196232

197-
if let highlight = spec.highlight {
198-
var highlightedCode = ""
199-
highlightedCode.append(diag.highlights.first?.with(\.leadingTrivia, []).description ?? "")
200-
for highlight in diag.highlights.dropFirst().dropLast() {
201-
highlightedCode.append(highlight.description)
202-
}
203-
if diag.highlights.count > 1 {
204-
highlightedCode.append(diag.highlights.last?.with(\.trailingTrivia, []).description ?? "")
233+
if let highlights = spec.highlights {
234+
if diag.highlights.count != highlights.count {
235+
XCTFail(
236+
"""
237+
Expected \(highlights.count) highlights but received \(diag.highlights.count):
238+
\(diag.highlights.map(\.trimmedDescription).joined(separator: "\n"))
239+
""",
240+
file: spec.originatorFile,
241+
line: spec.originatorLine
242+
)
243+
} else {
244+
for (actual, expected) in zip(diag.highlights, highlights) {
245+
assertStringsEqualWithDiff(
246+
actual.trimmedDescription,
247+
expected,
248+
"highlight does not match",
249+
file: spec.originatorFile,
250+
line: spec.originatorLine
251+
)
252+
}
205253
}
206-
207-
assertStringsEqualWithDiff(
208-
highlightedCode,
209-
highlight,
210-
"highlight does not match",
211-
file: spec.originatorFile,
212-
line: spec.originatorLine
213-
)
214254
}
215255
if diag.notes.count != spec.notes.count {
216256
XCTFail(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 SwiftSyntax
15+
import SwiftSyntaxMacros
16+
import SwiftSyntaxMacrosTestSupport
17+
import XCTest
18+
19+
final class AssertionsTests: XCTestCase {
20+
struct OnlyStruct: DiagnosticMessage {
21+
var message = "'@NoStruct' cannot be applied to struct types"
22+
var diagnosticID = MessageID(domain: "\(AssertionsTests.self)", id: "\(OnlyStruct.self)")
23+
var severity = DiagnosticSeverity.error
24+
}
25+
26+
struct NoStruct: MemberMacro {
27+
static func expansion(
28+
of node: AttributeSyntax,
29+
providingMembersOf declaration: some DeclGroupSyntax,
30+
conformingTo protocols: [TypeSyntax],
31+
in context: some MacroExpansionContext
32+
) throws -> [DeclSyntax] {
33+
if let structDecl = declaration.as(StructDeclSyntax.self) {
34+
context.diagnose(
35+
.init(
36+
node: structDecl.structKeyword,
37+
message: OnlyStruct()
38+
)
39+
)
40+
}
41+
return []
42+
}
43+
}
44+
45+
struct NoStructMultiHighlight: MemberMacro {
46+
static func expansion(
47+
of node: AttributeSyntax,
48+
providingMembersOf declaration: some DeclGroupSyntax,
49+
conformingTo protocols: [TypeSyntax],
50+
in context: some MacroExpansionContext
51+
) throws -> [DeclSyntax] {
52+
if let structDecl = declaration.as(StructDeclSyntax.self) {
53+
context.diagnose(
54+
.init(
55+
node: structDecl.structKeyword,
56+
message: OnlyStruct(),
57+
highlights: [
58+
Syntax(structDecl.structKeyword),
59+
Syntax(structDecl.name),
60+
]
61+
)
62+
)
63+
}
64+
return []
65+
}
66+
}
67+
68+
func testAssertMacroExpansionIgnoresHighlightMatchingIfNil() {
69+
assertMacroExpansion(
70+
"@NoStruct struct S { }",
71+
expandedSource: "struct S { }",
72+
diagnostics: [
73+
.init(message: OnlyStruct().message, line: 1, column: 11)
74+
],
75+
macros: ["NoStruct": NoStruct.self]
76+
)
77+
}
78+
79+
@available(*, deprecated)
80+
func testAssertMacroExpansionMatchesSingleStringHighlight() {
81+
assertMacroExpansion(
82+
"@NoStruct struct S { }",
83+
expandedSource: "struct S { }",
84+
diagnostics: [
85+
.init(message: OnlyStruct().message, line: 1, column: 11, highlight: "struct")
86+
],
87+
macros: ["NoStruct": NoStruct.self]
88+
)
89+
}
90+
91+
func testAssertMacroExpansionMatchesArrayHighlight() {
92+
assertMacroExpansion(
93+
"@NoStruct struct S { }",
94+
expandedSource: "struct S { }",
95+
diagnostics: [
96+
.init(message: OnlyStruct().message, line: 1, column: 11, highlights: ["struct", "S"])
97+
],
98+
macros: ["NoStruct": NoStructMultiHighlight.self]
99+
)
100+
}
101+
}

0 commit comments

Comments
 (0)