Skip to content

Commit 948b59d

Browse files
authored
Apply suite diagnostics to the entire lexical context. (#336)
1 parent bde5d35 commit 948b59d

File tree

6 files changed

+107
-66
lines changed

6 files changed

+107
-66
lines changed

Sources/TestingMacros/SuiteDeclarationMacro.swift

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
5050
/// - Returns: Whether or not macro expansion should continue (i.e. stopping
5151
/// if a fatal error was diagnosed.)
5252
private static func _diagnoseIssues(
53-
with declaration: some SyntaxProtocol,
53+
with declaration: some DeclSyntaxProtocol,
5454
suiteAttribute: AttributeSyntax,
5555
in context: some MacroExpansionContext
5656
) -> Bool {
@@ -59,43 +59,18 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
5959
context.diagnose(diagnostics)
6060
}
6161

62-
// The @Suite attribute is only supported on type declarations, all of which
63-
// are DeclGroupSyntax types.
64-
guard let declaration = declaration.asProtocol((any DeclGroupSyntax).self) else {
65-
diagnostics.append(.attributeNotSupported(suiteAttribute, on: declaration))
66-
return false
67-
}
68-
6962
#if canImport(SwiftSyntax600)
7063
// Check if the lexical context is appropriate for a suite or test.
71-
diagnostics += diagnoseIssuesWithLexicalContext(containing: declaration, attribute: suiteAttribute, in: context)
64+
diagnostics += diagnoseIssuesWithLexicalContext(context.lexicalContext, containing: declaration, attribute: suiteAttribute)
7265
#endif
73-
74-
// Generic suites are not supported.
75-
if let genericClause = declaration.asProtocol((any WithGenericParametersSyntax).self)?.genericParameterClause {
76-
diagnostics.append(.genericDeclarationNotSupported(declaration, whenUsing: suiteAttribute, becauseOf: genericClause))
77-
} else if let whereClause = declaration.genericWhereClause {
78-
diagnostics.append(.genericDeclarationNotSupported(declaration, whenUsing: suiteAttribute, becauseOf: whereClause))
79-
}
66+
diagnostics += diagnoseIssuesWithLexicalContext(declaration, containing: declaration, attribute: suiteAttribute)
8067

8168
// Suites inheriting from XCTestCase are not supported.
82-
if declaration.inherits(fromTypeNamed: "XCTestCase", inModuleNamed: "XCTest") {
69+
if let declaration = declaration.asProtocol((any DeclGroupSyntax).self),
70+
declaration.inherits(fromTypeNamed: "XCTestCase", inModuleNamed: "XCTest") {
8371
diagnostics.append(.xcTestCaseNotSupported(declaration, whenUsing: suiteAttribute))
8472
}
8573

86-
// Suites that are classes must be final.
87-
if let classDecl = declaration.as(ClassDeclSyntax.self) {
88-
if !classDecl.modifiers.lazy.map(\.name.tokenKind).contains(.keyword(.final)) {
89-
diagnostics.append(.nonFinalClassNotSupported(classDecl, whenUsing: suiteAttribute))
90-
}
91-
}
92-
93-
// Suites cannot be protocols (there's nowhere to put most of the
94-
// declarations we generate.)
95-
if let protocolDecl = declaration.as(ProtocolDeclSyntax.self) {
96-
diagnostics.append(.attributeNotSupported(suiteAttribute, on: protocolDecl))
97-
}
98-
9974
// @Suite cannot be applied to a type extension (although a type extension
10075
// can still contain test functions and test suites.)
10176
if let extensionDecl = declaration.as(ExtensionDeclSyntax.self) {
@@ -106,24 +81,10 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
10681
// impossible to reach this point if the declaration can't have attributes.
10782
if let attributedDecl = declaration.asProtocol((any WithAttributesSyntax).self) {
10883
// Only one @Suite attribute is supported.
109-
let suiteAttributes = attributedDecl.attributes(named: "Suite", in: context)
84+
let suiteAttributes = attributedDecl.attributes(named: "Suite")
11085
if suiteAttributes.count > 1 {
11186
diagnostics.append(.multipleAttributesNotSupported(suiteAttributes, on: declaration))
11287
}
113-
114-
// Availability is not supported on suites (we need semantic availability
115-
// to correctly understand the availability of a suite.)
116-
let availabilityAttributes = attributedDecl.availabilityAttributes
117-
if !availabilityAttributes.isEmpty {
118-
// Diagnose all @available attributes.
119-
for availabilityAttribute in availabilityAttributes {
120-
diagnostics.append(.availabilityAttributeNotSupported(availabilityAttribute, on: declaration, whenUsing: suiteAttribute))
121-
}
122-
} else if let noasyncAttribute = attributedDecl.noasyncAttribute {
123-
// No @available attributes, but we do have an @_unavailableFromAsync
124-
// attribute and we still need to diagnose that.
125-
diagnostics.append(.availabilityAttributeNotSupported(noasyncAttribute, on: declaration, whenUsing: suiteAttribute))
126-
}
12788
}
12889

12990
return !diagnostics.lazy.map(\.severity).contains(.error)

Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,10 @@ extension WithAttributesSyntax {
121121
/// - name: The name of the attribute to look for.
122122
/// - moduleName: The name of the module that declares the attribute named
123123
/// `name`.
124-
/// - context: The macro context in which the expression is being parsed.
125124
///
126125
/// - Returns: An array of `AttributeSyntax` corresponding to the attached
127126
/// `@Test` attributes, or the empty array if none is attached.
128-
func attributes(named name: String, inModuleNamed moduleName: String = "Testing", in context: some MacroExpansionContext) -> [AttributeSyntax] {
127+
func attributes(named name: String, inModuleNamed moduleName: String = "Testing") -> [AttributeSyntax] {
129128
attributes.lazy.compactMap { attribute in
130129
if case let .attribute(attribute) = attribute {
131130
return attribute

Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,37 +88,104 @@ func diagnoseIssuesWithTags(in traitExprs: [ExprSyntax], addedTo attribute: Attr
8888
}
8989
}
9090

91-
#if canImport(SwiftSyntax600)
92-
/// Diagnose issues with the lexical context containing a declaration.
91+
/// Diagnose issues with a synthesized suite (one without an `@Suite` attribute)
92+
/// containing a declaration.
9393
///
9494
/// - Parameters:
95+
/// - lexicalContext: The single lexical context to inspect.
9596
/// - decl: The declaration to inspect.
96-
/// - testAttribute: The `@Test` attribute applied to `decl`.
97-
/// - context: The macro context in which the expression is being parsed.
97+
/// - attribute: The `@Test` or `@Suite` attribute applied to `decl`.
9898
///
9999
/// - Returns: An array of zero or more diagnostic messages related to the
100100
/// lexical context containing `decl`.
101+
///
102+
/// This function is also used by ``SuiteDeclarationMacro`` for a number of its
103+
/// own diagnostics. The implementation substitutes different diagnostic
104+
/// messages when `suiteDecl` and `decl` are the same syntax node on the
105+
/// assumption that a suite is self-diagnosing.
101106
func diagnoseIssuesWithLexicalContext(
107+
_ lexicalContext: some SyntaxProtocol,
102108
containing decl: some DeclSyntaxProtocol,
103-
attribute: AttributeSyntax,
104-
in context: some MacroExpansionContext
109+
attribute: AttributeSyntax
105110
) -> [DiagnosticMessage] {
106111
var diagnostics = [DiagnosticMessage]()
107112

108-
for lexicalContext in context.lexicalContext {
109-
if !lexicalContext.isProtocol((any DeclGroupSyntax).self) {
113+
// Functions, closures, etc. are not supported as enclosing lexical contexts.
114+
guard let lexicalContext = lexicalContext.asProtocol((any DeclGroupSyntax).self) else {
115+
if Syntax(lexicalContext) == Syntax(decl) {
116+
diagnostics.append(.attributeNotSupported(attribute, on: lexicalContext))
117+
} else {
110118
diagnostics.append(.containingNodeUnsupported(lexicalContext, whenUsing: attribute, on: decl))
111119
}
120+
return diagnostics
121+
}
122+
123+
// Generic suites are not supported.
124+
if let genericClause = lexicalContext.asProtocol((any WithGenericParametersSyntax).self)?.genericParameterClause {
125+
diagnostics.append(.genericDeclarationNotSupported(decl, whenUsing: attribute, becauseOf: genericClause))
126+
} else if let whereClause = lexicalContext.genericWhereClause {
127+
diagnostics.append(.genericDeclarationNotSupported(decl, whenUsing: attribute, becauseOf: whereClause))
128+
}
112129

113-
if let classDecl = lexicalContext.as(ClassDeclSyntax.self) {
114-
if !classDecl.modifiers.lazy.map(\.name.tokenKind).contains(.keyword(.final)) {
130+
// Suites that are classes must be final.
131+
if let classDecl = lexicalContext.as(ClassDeclSyntax.self) {
132+
if !classDecl.modifiers.lazy.map(\.name.tokenKind).contains(.keyword(.final)) {
133+
if Syntax(classDecl) == Syntax(decl) {
134+
diagnostics.append(.nonFinalClassNotSupported(classDecl, whenUsing: attribute))
135+
} else {
115136
diagnostics.append(.containingNodeUnsupported(classDecl, whenUsing: attribute, on: decl))
116137
}
117-
} else if let protocolDecl = lexicalContext.as(ProtocolDeclSyntax.self) {
138+
}
139+
}
140+
141+
// Suites cannot be protocols (there's nowhere to put most of the
142+
// declarations we generate.)
143+
if let protocolDecl = lexicalContext.as(ProtocolDeclSyntax.self) {
144+
if Syntax(protocolDecl) == Syntax(decl) {
145+
diagnostics.append(.attributeNotSupported(attribute, on: protocolDecl))
146+
} else {
118147
diagnostics.append(.containingNodeUnsupported(protocolDecl, whenUsing: attribute, on: decl))
119148
}
120149
}
121150

151+
// Check other attributes on the declaration. Note that it should be
152+
// impossible to reach this point if the declaration can't have attributes.
153+
if let attributedDecl = lexicalContext.asProtocol((any WithAttributesSyntax).self) {
154+
// Availability is not supported on suites (we need semantic availability
155+
// to correctly understand the availability of a suite.)
156+
let availabilityAttributes = attributedDecl.availabilityAttributes
157+
if !availabilityAttributes.isEmpty {
158+
// Diagnose all @available attributes.
159+
for availabilityAttribute in availabilityAttributes {
160+
diagnostics.append(.availabilityAttributeNotSupported(availabilityAttribute, on: decl, whenUsing: attribute))
161+
}
162+
} else if let noasyncAttribute = attributedDecl.noasyncAttribute {
163+
// No @available attributes, but we do have an @_unavailableFromAsync
164+
// attribute and we still need to diagnose that.
165+
diagnostics.append(.availabilityAttributeNotSupported(noasyncAttribute, on: decl, whenUsing: attribute))
166+
}
167+
}
168+
122169
return diagnostics
123170
}
171+
172+
#if canImport(SwiftSyntax600)
173+
/// Diagnose issues with the lexical context containing a declaration.
174+
///
175+
/// - Parameters:
176+
/// - decl: The declaration to inspect.
177+
/// - attribute: The `@Test` or `@Suite` attribute applied to `decl`.
178+
/// - context: The macro context in which the expression is being parsed.
179+
///
180+
/// - Returns: An array of zero or more diagnostic messages related to the
181+
/// lexical context containing `decl`.
182+
func diagnoseIssuesWithLexicalContext(
183+
_ lexicalContext: [Syntax],
184+
containing decl: some DeclSyntaxProtocol,
185+
attribute: AttributeSyntax
186+
) -> [DiagnosticMessage] {
187+
lexicalContext.lazy
188+
.map { diagnoseIssuesWithLexicalContext($0, containing: decl, attribute: attribute) }
189+
.reduce(into: [], +=)
190+
}
124191
#endif

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,11 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
188188
///
189189
/// - Returns: A diagnostic message.
190190
static func genericDeclarationNotSupported(_ decl: some SyntaxProtocol, whenUsing attribute: AttributeSyntax, becauseOf genericClause: some SyntaxProtocol) -> Self {
191-
Self(
192-
syntax: Syntax(genericClause),
191+
// Avoid using a syntax node from a lexical context (it won't have source
192+
// location information.)
193+
let syntax = (genericClause.root != decl.root) ? Syntax(decl) : Syntax(genericClause)
194+
return Self(
195+
syntax: syntax,
193196
message: "Attribute \(_macroName(attribute)) cannot be applied to a generic \(_kindString(for: decl))",
194197
severity: .error
195198
)
@@ -209,8 +212,11 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
209212
/// semantic availability and fully-qualified names for types at macro
210213
/// expansion time. ([104081994](rdar://104081994))
211214
static func availabilityAttributeNotSupported(_ availabilityAttribute: AttributeSyntax, on decl: some SyntaxProtocol, whenUsing attribute: AttributeSyntax) -> Self {
212-
Self(
213-
syntax: Syntax(availabilityAttribute),
215+
// Avoid using a syntax node from a lexical context (it won't have source
216+
// location information.)
217+
let syntax = (availabilityAttribute.root != decl.root) ? Syntax(decl) : Syntax(availabilityAttribute)
218+
return Self(
219+
syntax: syntax,
214220
message: "Attribute \(_macroName(attribute)) cannot be applied to this \(_kindString(for: decl)) because it has been marked '\(availabilityAttribute.trimmed)'",
215221
severity: .error
216222
)
@@ -331,7 +337,6 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
331337
)
332338
}
333339

334-
#if canImport(SwiftSyntax600)
335340
/// Create a diagnostic message stating that the given attribute cannot be
336341
/// used within a lexical context.
337342
///
@@ -375,6 +380,7 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
375380
}
376381
}
377382

383+
#if canImport(SwiftSyntax600)
378384
/// Create a diagnostic message stating that the given attribute cannot be
379385
/// applied to the given declaration outside the scope of an extension to
380386
/// `Tag`.

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
5858

5959
#if canImport(SwiftSyntax600)
6060
// Check if the lexical context is appropriate for a suite or test.
61-
diagnostics += diagnoseIssuesWithLexicalContext(containing: declaration, attribute: testAttribute, in: context)
61+
diagnostics += diagnoseIssuesWithLexicalContext(context.lexicalContext, containing: declaration, attribute: testAttribute)
6262
#endif
6363

6464
// Only one @Test attribute is supported.
65-
let suiteAttributes = function.attributes(named: "Test", in: context)
65+
let suiteAttributes = function.attributes(named: "Test")
6666
if suiteAttributes.count > 1 {
6767
diagnostics.append(.multipleAttributesNotSupported(suiteAttributes, on: declaration))
6868
}
@@ -290,7 +290,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
290290
// If the function is noasync *and* main-actor-isolated, we'll call through
291291
// MainActor.run to invoke it. We do not have a general mechanism for
292292
// detecting isolation to other global actors.
293-
lazy var isMainActorIsolated = !functionDecl.attributes(named: "MainActor", inModuleNamed: "Swift", in: context).isEmpty
293+
lazy var isMainActorIsolated = !functionDecl.attributes(named: "MainActor", inModuleNamed: "Swift").isEmpty
294294
var forwardCall: (ExprSyntax) -> ExprSyntax = {
295295
"try await (\($0), Testing.__requiringTry, Testing.__requiringAwait).0"
296296
}

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ struct TestDeclarationMacroTests {
123123
"Attribute 'Test' cannot be applied to a function within function 'f()'",
124124
"struct S { func f(x: Int) { @Suite struct S { } } }":
125125
"Attribute 'Suite' cannot be applied to a structure within function 'f(x:)'",
126+
"struct S<T> { @Test func f() {} }":
127+
"Attribute 'Test' cannot be applied to a generic function",
128+
"struct S<T> { @Suite struct S {} }":
129+
"Attribute 'Suite' cannot be applied to a generic structure",
126130
"class C { @Test func f() {} }":
127131
"Attribute 'Test' cannot be applied to a function within non-final class 'C'",
128132
"class C { @Suite struct S {} }":
@@ -135,6 +139,10 @@ struct TestDeclarationMacroTests {
135139
"Attribute 'Test' cannot be applied to a function within a closure",
136140
"{ _ in @Suite struct S {} }":
137141
"Attribute 'Suite' cannot be applied to a structure within a closure",
142+
"@available(*, noasync) struct S { @Test func f() {} }":
143+
"Attribute 'Test' cannot be applied to this function because it has been marked '@available(*, noasync)'",
144+
"@available(*, noasync) struct S { @Suite struct S {} }":
145+
"Attribute 'Suite' cannot be applied to this structure because it has been marked '@available(*, noasync)'",
138146
]
139147
)
140148
func invalidLexicalContext(input: String, expectedMessage: String) throws {

0 commit comments

Comments
 (0)