Skip to content

Commit a98ed04

Browse files
committed
Adopt lexicalContext from swift-syntax-6.0.0.
This PR conditionally adopts the new `lexicalContext` member of `MacroExpansionContext` added in swiftlang/swift-syntax#1554. If the SwiftSyntax600 pseudo-module is available, then this member should be available for use and can be used to perform additional diagnostics for tests and to get the names of their containing types. With this PR, if built against an older toolchain (5.11 or earlier), the old hacky "is there leading whitespace?" mechanism is still used. A future PR will recursively perform suite-level diagnostics on the lexical contexts containing tests and suites, so that a test cannot be (easily) inserted into a type that cannot be used as a suite. Resolves rdar://109439578.
1 parent a82de3b commit a98ed04

File tree

8 files changed

+214
-69
lines changed

8 files changed

+214
-69
lines changed

Sources/TestingMacros/SuiteDeclarationMacro.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
2121
providingMembersOf declaration: some DeclGroupSyntax,
2222
in context: some MacroExpansionContext
2323
) throws -> [DeclSyntax] {
24-
_diagnoseIssues(with: declaration, suiteAttribute: node, in: context)
24+
guard _diagnoseIssues(with: declaration, suiteAttribute: node, in: context) else {
25+
return []
26+
}
2527
return _createTestContainerDecls(for: declaration, suiteAttribute: node, in: context)
2628
}
2729

@@ -33,7 +35,7 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
3335
// The peer macro expansion of this macro is only used to diagnose misuses
3436
// on symbols that are not decl groups.
3537
if declaration.asProtocol((any DeclGroupSyntax).self) == nil {
36-
_diagnoseIssues(with: declaration, suiteAttribute: node, in: context)
38+
_ = _diagnoseIssues(with: declaration, suiteAttribute: node, in: context)
3739
}
3840
return []
3941
}
@@ -44,23 +46,31 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
4446
/// - declaration: The type declaration to diagnose.
4547
/// - suiteAttribute: The `@Suite` attribute applied to `declaration`.
4648
/// - context: The macro context in which the expression is being parsed.
49+
///
50+
/// - Returns: Whether or not macro expansion should continue (i.e. stopping
51+
/// if a fatal error was diagnosed.)
4752
private static func _diagnoseIssues(
4853
with declaration: some SyntaxProtocol,
4954
suiteAttribute: AttributeSyntax,
5055
in context: some MacroExpansionContext
51-
) {
56+
) -> Bool {
5257
var diagnostics = [DiagnosticMessage]()
5358
defer {
54-
diagnostics.forEach(context.diagnose)
59+
context.diagnose(diagnostics)
5560
}
5661

5762
// The @Suite attribute is only supported on type declarations, all of which
5863
// are DeclGroupSyntax types.
5964
guard let declaration = declaration.asProtocol((any DeclGroupSyntax).self) else {
6065
diagnostics.append(.attributeNotSupported(suiteAttribute, on: declaration))
61-
return
66+
return false
6267
}
6368

69+
#if canImport(SwiftSyntax600)
70+
// Check if the lexical context is appropriate for a suite or test.
71+
diagnostics += diagnoseIssuesWithLexicalContext(containing: declaration, attribute: suiteAttribute, in: context)
72+
#endif
73+
6474
// Generic suites are not supported.
6575
if let genericClause = declaration.asProtocol((any WithGenericParametersSyntax).self)?.genericParameterClause {
6676
diagnostics.append(.genericDeclarationNotSupported(declaration, whenUsing: suiteAttribute, becauseOf: genericClause))
@@ -115,6 +125,8 @@ public struct SuiteDeclarationMacro: MemberMacro, PeerMacro, Sendable {
115125
diagnostics.append(.availabilityAttributeNotSupported(noasyncAttribute, on: declaration, whenUsing: suiteAttribute))
116126
}
117127
}
128+
129+
return !diagnostics.lazy.map(\.severity).contains(.error)
118130
}
119131

120132
/// Create a declaration for a type that conforms to the `__TestContainer`

Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,17 @@ extension FunctionDeclSyntax {
9494
if signature.effectSpecifiers?.asyncSpecifier != nil {
9595
selector += "WithCompletionHandler"
9696
colonToken = .colonToken()
97-
} else if signature.effectSpecifiers?.throwsSpecifier != nil {
98-
selector += "AndReturnError"
99-
colonToken = .colonToken()
97+
} else {
98+
let hasThrowsSpecifier: Bool
99+
#if canImport(SwiftSyntax600)
100+
hasThrowsSpecifier = signature.effectSpecifiers?.throwsClause != nil
101+
#else
102+
hasThrowsSpecifier = signature.effectSpecifiers?.throwsSpecifier != nil
103+
#endif
104+
if hasThrowsSpecifier {
105+
selector += "AndReturnError"
106+
colonToken = .colonToken()
107+
}
100108
}
101109
return ObjCSelectorPieceListSyntax {
102110
ObjCSelectorPieceSyntax(name: .identifier(selector), colon: colonToken)

Sources/TestingMacros/Support/TagConstraints.swift renamed to Sources/TestingMacros/Support/DiagnosticMessage+Diagnosing.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,24 @@ func diagnoseIssuesWithTags(in traitExprs: [ExprSyntax], addedTo attribute: Attr
8787
}
8888
}
8989
}
90+
91+
#if canImport(SwiftSyntax600)
92+
/// Diagnose issues with the lexical context containing a declaration.
93+
///
94+
/// - Parameters:
95+
/// - 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.
98+
///
99+
/// - Returns: An array of zero or more diagnostic messages related to the
100+
/// lexical context containing `decl`.
101+
func diagnoseIssuesWithLexicalContext(
102+
containing decl: some DeclSyntaxProtocol,
103+
attribute: AttributeSyntax,
104+
in context: some MacroExpansionContext
105+
) -> [DiagnosticMessage] {
106+
context.lexicalContext
107+
.filter { !$0.isProtocol((any DeclGroupSyntax).self) }
108+
.map { .containingNodeUnsupported($0, whenUsing: attribute) }
109+
}
110+
#endif

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,27 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
225225
)
226226
}
227227

228+
#if canImport(SwiftSyntax600)
229+
/// Create a diagnostic message stating that the given attribute cannot be
230+
/// used within a lexical context.
231+
///
232+
/// - Parameters:
233+
/// - node: The lexical context preventing the use of `attribute`.
234+
/// - attribute: The `@Test` or `@Suite` attribute.
235+
///
236+
/// - Returns: A diagnostic message.
237+
static func containingNodeUnsupported(_ node: some SyntaxProtocol, whenUsing attribute: AttributeSyntax) -> Self {
238+
// It would be great if the diagnostic pointed to the containing lexical
239+
// context that was unsupported, but that node may be synthesized and does
240+
// not have reliable location information.
241+
Self(
242+
syntax: Syntax(attribute),
243+
message: "The @\(attribute.attributeNameText) attribute cannot be applied within \(_kindString(for: node, includeA: true)).",
244+
severity: .error
245+
)
246+
}
247+
#endif
248+
228249
/// Create a diagnostic message stating that the given attribute has no effect
229250
/// when applied to the given extension declaration.
230251
///
@@ -406,7 +427,6 @@ extension MacroExpansionContext {
406427
/// - message: The diagnostic message to emit. The `node` and `position`
407428
/// arguments to `Diagnostic.init()` are derived from the message's
408429
/// `syntax` property.
409-
/// - fixIts: Any Fix-Its to apply.
410430
func diagnose(_ message: DiagnosticMessage) {
411431
diagnose(
412432
Diagnostic(
@@ -418,6 +438,16 @@ extension MacroExpansionContext {
418438
)
419439
}
420440

441+
/// Emit a sequence of diagnostic messages.
442+
///
443+
/// - Parameters:
444+
/// - messages: The diagnostic messages to emit.
445+
func diagnose(_ messages: some Sequence<DiagnosticMessage>) {
446+
for message in messages {
447+
diagnose(message)
448+
}
449+
}
450+
421451
/// Emit a diagnostic message for debugging purposes during development of the
422452
/// testing library.
423453
///

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
2121
providingPeersOf declaration: some DeclSyntaxProtocol,
2222
in context: some MacroExpansionContext
2323
) throws -> [DeclSyntax] {
24-
_diagnoseIssues(with: declaration, testAttribute: node, in: context)
24+
guard _diagnoseIssues(with: declaration, testAttribute: node, in: context) else {
25+
return []
26+
}
2527

2628
guard let function = declaration.as(FunctionDeclSyntax.self) else {
2729
return []
@@ -45,6 +47,17 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
4547
testAttribute: AttributeSyntax,
4648
in context: some MacroExpansionContext
4749
) -> TypeSyntax? {
50+
#if canImport(SwiftSyntax600)
51+
let types = context.lexicalContext
52+
.compactMap { $0.asProtocol((any DeclGroupSyntax).self) }
53+
.map(\.type)
54+
.reversed()
55+
if types.isEmpty {
56+
return nil
57+
}
58+
let typeName = types.map(\.trimmedDescription).joined(separator: ".")
59+
return "\(raw: typeName)"
60+
#else
4861
// Find the beginning of the first attribute on the declaration, including
4962
// those embedded in #if statements, to account for patterns like
5063
// `@MainActor @Test func` where there's a space ahead of @Test, but the
@@ -79,6 +92,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
7992
return TypeSyntax(IdentifierTypeSyntax(name: .keyword(.Self)))
8093
}
8194
return nil
95+
#endif
8296
}
8397

8498
/// Diagnose issues with a `@Test` declaration.
@@ -87,22 +101,30 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
87101
/// - declaration: The function declaration to diagnose.
88102
/// - testAttribute: The `@Test` attribute applied to `declaration`.
89103
/// - context: The macro context in which the expression is being parsed.
104+
///
105+
/// - Returns: Whether or not macro expansion should continue (i.e. stopping
106+
/// if a fatal error was diagnosed.)
90107
private static func _diagnoseIssues(
91108
with declaration: some DeclSyntaxProtocol,
92109
testAttribute: AttributeSyntax,
93110
in context: some MacroExpansionContext
94-
) {
111+
) -> Bool {
95112
var diagnostics = [DiagnosticMessage]()
96113
defer {
97-
diagnostics.forEach(context.diagnose)
114+
context.diagnose(diagnostics)
98115
}
99116

100117
// The @Test attribute is only supported on function declarations.
101118
guard let function = declaration.as(FunctionDeclSyntax.self) else {
102119
diagnostics.append(.attributeNotSupported(testAttribute, on: declaration))
103-
return
120+
return false
104121
}
105122

123+
#if canImport(SwiftSyntax600)
124+
// Check if the lexical context is appropriate for a suite or test.
125+
diagnostics += diagnoseIssuesWithLexicalContext(containing: declaration, attribute: testAttribute, in: context)
126+
#endif
127+
106128
// Only one @Test attribute is supported.
107129
let suiteAttributes = function.attributes(named: "Test", in: context)
108130
if suiteAttributes.count > 1 {
@@ -144,6 +166,8 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
144166
}
145167
}
146168
}
169+
170+
return !diagnostics.lazy.map(\.severity).contains(.error)
147171
}
148172

149173
/// Create a function call parameter list used to call a function from its
@@ -406,6 +430,11 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
406430
) -> [DeclSyntax] {
407431
var result = [DeclSyntax]()
408432

433+
#if canImport(SwiftSyntax600)
434+
// Get the name of the type containing the function for passing to the test
435+
// factory function later.
436+
let typealiasExpr: ExprSyntax = typeName.map { "\($0).self" } ?? "nil"
437+
#else
409438
// We cannot directly refer to Self here because it will end up being
410439
// resolved as the __TestContainer type we generate. Create a uniquely-named
411440
// reference to Self outside the context of the generated type, and use it
@@ -415,7 +444,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
415444
// inside a static computed property instead of a typealias (where covariant
416445
// Self is disallowed.)
417446
//
418-
// This "typealias" will not be necessary when rdar://105470382 is resolved.
447+
// This "typealias" is not necessary when swift-syntax-6.0.0 is available.
419448
var typealiasExpr: ExprSyntax = "nil"
420449
if let typeName {
421450
let typealiasName = context.makeUniqueName(thunking: functionDecl)
@@ -430,6 +459,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
430459

431460
typealiasExpr = "\(typealiasName)"
432461
}
462+
#endif
433463

434464
// Parse the @Test attribute.
435465
let attributeInfo = AttributeInfo(byParsing: testAttribute, on: functionDecl, in: context)

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ struct TestDeclarationMacroTests {
6767
"@Test enum E {}":
6868
"Attribute 'Test' cannot be applied to an enumeration",
6969
70-
7170
// Availability
7271
"@available(*, unavailable) @Suite struct S {}":
7372
"Attribute 'Suite' cannot be applied to this structure because it has been marked '@available(*, unavailable)'",
@@ -117,6 +116,26 @@ struct TestDeclarationMacroTests {
117116
}
118117
}
119118

119+
#if canImport(SwiftSyntax600)
120+
@Test("Error diagnostics emitted for invalid lexical contexts",
121+
arguments: [
122+
"struct S { func f() { @Test func f() {} } }":
123+
"The @Test attribute cannot be applied within a function.",
124+
"struct S { func f() { @Suite struct S { } } }":
125+
"The @Suite attribute cannot be applied within a function.",
126+
]
127+
)
128+
func invalidLexicalContext(input: String, expectedMessage: String) throws {
129+
let (_, diagnostics) = try parse(input)
130+
131+
#expect(diagnostics.count > 0)
132+
for diagnostic in diagnostics {
133+
#expect(diagnostic.diagMessage.severity == .error)
134+
#expect(diagnostic.message == expectedMessage)
135+
}
136+
}
137+
#endif
138+
120139
@Test("Warning diagnostics emitted on API misuse",
121140
arguments: [
122141
// return types
@@ -189,13 +208,9 @@ struct TestDeclarationMacroTests {
189208
}
190209
}
191210

192-
@Test("Different kinds of functions are handled correctly",
193-
arguments: [
211+
static var functionTypeInputs: [(String, String?, String?)] {
212+
var result: [(String, String?, String?)] = [
194213
("@Test func f() {}", nil, nil),
195-
("struct S {\n\t@Test func f() {} }", "Self", "let"),
196-
("struct S {\n\t@Test mutating func f() {} }", "Self", "var"),
197-
("struct S {\n\t@Test static func f() {} }", "Self", nil),
198-
("final class S {\n\t@Test class func f() {} }", "Self", nil),
199214
("@Test @available(*, noasync) @MainActor func f() {}", nil, "MainActor.run"),
200215
("@Test @_unavailableFromAsync @MainActor func f() {}", nil, "MainActor.run"),
201216
("@Test @available(*, noasync) func f() {}", nil, "__requiringTry"),
@@ -220,7 +235,27 @@ struct TestDeclarationMacroTests {
220235
nil
221236
),
222237
]
223-
)
238+
239+
#if canImport(SwiftSyntax600)
240+
result += [
241+
("struct S_NAME {\n\t@Test func f() {} }", "S_NAME", "let"),
242+
("struct S_NAME {\n\t@Test mutating func f() {} }", "S_NAME", "var"),
243+
("struct S_NAME {\n\t@Test static func f() {} }", "S_NAME", nil),
244+
("final class C_NAME {\n\t@Test class func f() {} }", "C_NAME", nil),
245+
]
246+
#else
247+
result += [
248+
("struct S {\n\t@Test func f() {} }", "Self", "let"),
249+
("struct S {\n\t@Test mutating func f() {} }", "Self", "var"),
250+
("struct S {\n\t@Test static func f() {} }", "Self", nil),
251+
("final class C {\n\t@Test class func f() {} }", "Self", nil),
252+
]
253+
#endif
254+
255+
return result
256+
}
257+
258+
@Test("Different kinds of functions are handled correctly", arguments: functionTypeInputs)
224259
func differentFunctionTypes(input: String, expectedTypeName: String?, otherCode: String?) throws {
225260
let (output, _) = try parse(input)
226261

Tests/TestingMacrosTests/TestSupport/Parse.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,17 @@ func parse(_ sourceCode: String, activeMacros activeMacroNames: [String] = [], r
3535
}
3636
let operatorTable = OperatorTable.standardOperators
3737
let originalSyntax = try operatorTable.foldAll(Parser.parse(source: sourceCode))
38+
#if canImport(SwiftSyntax600)
39+
let context = BasicMacroExpansionContext(lexicalContext: [], expansionDiscriminator: "", sourceFiles: [:])
40+
let syntax = try operatorTable.foldAll(
41+
originalSyntax.expand(macros: activeMacros) { syntax in
42+
BasicMacroExpansionContext(sharingWith: context, lexicalContext: syntax.allMacroLexicalContexts())
43+
}
44+
)
45+
#else
3846
let context = BasicMacroExpansionContext(expansionDiscriminator: "", sourceFiles: [:])
3947
let syntax = try operatorTable.foldAll(originalSyntax.expand(macros: activeMacros, in: context))
48+
#endif
4049
var sourceCode = String(describing: syntax.formatted().trimmed)
4150
if removeWhitespace {
4251
sourceCode = sourceCode.filter { !$0.isWhitespace }

0 commit comments

Comments
 (0)