Skip to content

Commit 9446983

Browse files
authored
Adopt lexicalContext from swift-syntax-6.0.0. (#279)
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. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent a82de3b commit 9446983

File tree

8 files changed

+267
-88
lines changed

8 files changed

+267
-88
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: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,13 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
138138
result = ("subscript", "a")
139139
case .enumCaseDecl:
140140
result = ("enumeration case", "an")
141+
#if canImport(SwiftSyntax600)
142+
case .typeAliasDecl:
143+
result = ("typealias", "a")
144+
#else
141145
case .typealiasDecl:
142146
result = ("typealias", "a")
147+
#endif
143148
case .macroDecl:
144149
result = ("macro", "a")
145150
case .protocolDecl:
@@ -225,6 +230,27 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
225230
)
226231
}
227232

233+
#if canImport(SwiftSyntax600)
234+
/// Create a diagnostic message stating that the given attribute cannot be
235+
/// used within a lexical context.
236+
///
237+
/// - Parameters:
238+
/// - node: The lexical context preventing the use of `attribute`.
239+
/// - attribute: The `@Test` or `@Suite` attribute.
240+
///
241+
/// - Returns: A diagnostic message.
242+
static func containingNodeUnsupported(_ node: some SyntaxProtocol, whenUsing attribute: AttributeSyntax) -> Self {
243+
// It would be great if the diagnostic pointed to the containing lexical
244+
// context that was unsupported, but that node may be synthesized and does
245+
// not have reliable location information.
246+
Self(
247+
syntax: Syntax(attribute),
248+
message: "The @\(attribute.attributeNameText) attribute cannot be applied within \(_kindString(for: node, includeA: true)).",
249+
severity: .error
250+
)
251+
}
252+
#endif
253+
228254
/// Create a diagnostic message stating that the given attribute has no effect
229255
/// when applied to the given extension declaration.
230256
///
@@ -406,7 +432,6 @@ extension MacroExpansionContext {
406432
/// - message: The diagnostic message to emit. The `node` and `position`
407433
/// arguments to `Diagnostic.init()` are derived from the message's
408434
/// `syntax` property.
409-
/// - fixIts: Any Fix-Its to apply.
410435
func diagnose(_ message: DiagnosticMessage) {
411436
diagnose(
412437
Diagnostic(
@@ -418,6 +443,16 @@ extension MacroExpansionContext {
418443
)
419444
}
420445

446+
/// Emit a sequence of diagnostic messages.
447+
///
448+
/// - Parameters:
449+
/// - messages: The diagnostic messages to emit.
450+
func diagnose(_ messages: some Sequence<DiagnosticMessage>) {
451+
for message in messages {
452+
diagnose(message)
453+
}
454+
}
455+
421456
/// Emit a diagnostic message for debugging purposes during development of the
422457
/// testing library.
423458
///

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 83 additions & 24 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 {
@@ -113,13 +135,22 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
113135

114136
// We don't support inout, isolated, or _const parameters on test functions.
115137
for parameter in parameterList {
116-
if let specifier = parameter.type.as(AttributedTypeSyntax.self)?.specifier {
117-
switch specifier.tokenKind {
118-
case .keyword(.inout), .keyword(.isolated), .keyword(._const):
138+
let invalidSpecifierKeywords: [TokenKind] = [.keyword(.inout), .keyword(.isolated), .keyword(._const),]
139+
if let parameterType = parameter.type.as(AttributedTypeSyntax.self) {
140+
#if canImport(SwiftSyntax600)
141+
for specifier in parameterType.specifiers {
142+
guard case let .simpleTypeSpecifier(specifier) = specifier else {
143+
continue
144+
}
145+
if invalidSpecifierKeywords.contains(specifier.specifier.tokenKind) {
146+
diagnostics.append(.specifierNotSupported(specifier.specifier, on: parameter, whenUsing: testAttribute))
147+
}
148+
}
149+
#else
150+
if let specifier = parameterType.specifier, invalidSpecifierKeywords.contains(specifier.tokenKind) {
119151
diagnostics.append(.specifierNotSupported(specifier, on: parameter, whenUsing: testAttribute))
120-
default:
121-
break
122152
}
153+
#endif
123154
}
124155
}
125156

@@ -144,6 +175,8 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
144175
}
145176
}
146177
}
178+
179+
return !diagnostics.lazy.map(\.severity).contains(.error)
147180
}
148181

149182
/// Create a function call parameter list used to call a function from its
@@ -220,21 +253,41 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
220253
private static func _createCaptureListExpr(
221254
from parametersWithLabels: some Sequence<(DeclReferenceExprSyntax, FunctionParameterSyntax)>
222255
) -> ClosureCaptureClauseSyntax {
223-
ClosureCaptureClauseSyntax {
224-
for (label, parameter) in parametersWithLabels {
225-
if case let .keyword(specifierKeyword) = parameter.type.as(AttributedTypeSyntax.self)?.specifier?.tokenKind,
226-
specifierKeyword == .borrowing || specifierKeyword == .consuming {
227-
ClosureCaptureSyntax(
228-
name: label.baseName,
229-
equal: .equalToken(),
230-
expression: CopyExprSyntax(
231-
copyKeyword: .keyword(.copy).with(\.trailingTrivia, .space),
232-
expression: label
233-
)
234-
)
235-
} else {
236-
ClosureCaptureSyntax(expression: label)
256+
let specifierKeywordsNeedingCopy: [TokenKind] = [.keyword(.borrowing), .keyword(.consuming),]
257+
let closureCaptures = parametersWithLabels.lazy.map { label, parameter in
258+
var needsCopy = false
259+
if let parameterType = parameter.type.as(AttributedTypeSyntax.self) {
260+
#if canImport(SwiftSyntax600)
261+
needsCopy = parameterType.specifiers.contains { specifier in
262+
guard case let .simpleTypeSpecifier(specifier) = specifier else {
263+
return false
264+
}
265+
return specifierKeywordsNeedingCopy.contains(specifier.specifier.tokenKind)
266+
}
267+
#else
268+
if let specifier = parameterType.specifier {
269+
needsCopy = specifierKeywordsNeedingCopy.contains(specifier.tokenKind)
237270
}
271+
#endif
272+
}
273+
274+
if needsCopy {
275+
return ClosureCaptureSyntax(
276+
name: label.baseName,
277+
equal: .equalToken(),
278+
expression: CopyExprSyntax(
279+
copyKeyword: .keyword(.copy).with(\.trailingTrivia, .space),
280+
expression: label
281+
)
282+
)
283+
} else {
284+
return ClosureCaptureSyntax(expression: label)
285+
}
286+
}
287+
288+
return ClosureCaptureClauseSyntax {
289+
for closureCapture in closureCaptures {
290+
closureCapture
238291
}
239292
}
240293
}
@@ -406,6 +459,11 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
406459
) -> [DeclSyntax] {
407460
var result = [DeclSyntax]()
408461

462+
#if canImport(SwiftSyntax600)
463+
// Get the name of the type containing the function for passing to the test
464+
// factory function later.
465+
let typealiasExpr: ExprSyntax = typeName.map { "\($0).self" } ?? "nil"
466+
#else
409467
// We cannot directly refer to Self here because it will end up being
410468
// resolved as the __TestContainer type we generate. Create a uniquely-named
411469
// reference to Self outside the context of the generated type, and use it
@@ -415,7 +473,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
415473
// inside a static computed property instead of a typealias (where covariant
416474
// Self is disallowed.)
417475
//
418-
// This "typealias" will not be necessary when rdar://105470382 is resolved.
476+
// This "typealias" is not necessary when swift-syntax-6.0.0 is available.
419477
var typealiasExpr: ExprSyntax = "nil"
420478
if let typeName {
421479
let typealiasName = context.makeUniqueName(thunking: functionDecl)
@@ -430,6 +488,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
430488

431489
typealiasExpr = "\(typealiasName)"
432490
}
491+
#endif
433492

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

0 commit comments

Comments
 (0)