Skip to content

Commit 9a0102a

Browse files
authored
Re-enable some pre-swift-syntax-600 code. (#380)
The Swift 5.10 compiler does not populate the new `lexicalContext` property during macro expansion. This PR ensures we maintain minimal 5.10 compatibility by continuing to use our whitespace trick to "get" the containing type of a test or suite. This change can be reverted once support for the Swift 5.10 compiler is dropped. ### 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 e930b41 commit 9a0102a

File tree

5 files changed

+87
-4
lines changed

5 files changed

+87
-4
lines changed

Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ extension MacroExpansionContext {
2828
///
2929
/// If the lexical context includes functions, closures, or some other
3030
/// non-type scope, the value of this property is `nil`.
31-
var typeOfLexicalContext: TypeSyntax? {
31+
///
32+
/// When using the Swift 6 or newer compiler, `node` is ignored. The argument
33+
/// can be removed once the testing library is updated to require the Swift 6
34+
/// compiler.
35+
func typeOfLexicalContext(containing node: some WithAttributesSyntax) -> TypeSyntax? {
36+
#if compiler(>=5.11)
3237
var typeNames = [String]()
3338
for lexicalContext in lexicalContext.reversed() {
3439
guard let decl = lexicalContext.asProtocol((any DeclGroupSyntax).self) else {
@@ -41,6 +46,42 @@ extension MacroExpansionContext {
4146
}
4247

4348
return "\(raw: typeNames.joined(separator: "."))"
49+
#else
50+
// Find the beginning of the first attribute on the declaration, including
51+
// those embedded in #if statements, to account for patterns like
52+
// `@MainActor @Test func` where there's a space ahead of @Test, but the
53+
// whole function is still at the top level.
54+
func firstAttribute(in attributes: AttributeListSyntax) -> AttributeSyntax? {
55+
attributes.lazy
56+
.compactMap { attribute in
57+
switch (attribute as AttributeListSyntax.Element?) {
58+
case let .ifConfigDecl(ifConfigDecl):
59+
ifConfigDecl.clauses.lazy
60+
.compactMap { clause in
61+
if case let .attributes(attributes) = clause.elements {
62+
return firstAttribute(in: attributes)
63+
}
64+
return nil
65+
}.first
66+
case let .attribute(attribute):
67+
attribute
68+
default:
69+
nil
70+
}
71+
}.first
72+
}
73+
let firstAttribute = firstAttribute(in: node.attributes)!
74+
75+
// HACK: If the test function appears to be indented, assume it is nested in
76+
// a type. Use `Self` as the presumptive name of the type.
77+
//
78+
// This hack works around rdar://105470382.
79+
if let lastLeadingTrivia = firstAttribute.leadingTrivia.pieces.last,
80+
lastLeadingTrivia.isWhitespace && !lastLeadingTrivia.isNewline {
81+
return TypeSyntax(IdentifierTypeSyntax(name: .keyword(.Self)))
82+
}
83+
return nil
84+
#endif
4485
}
4586
}
4687

Sources/TestingMacros/TagMacro.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ public struct TagMacro: PeerMacro, AccessorMacro, Sendable {
4040

4141
// Figure out what type the tag is declared on. It must be declared on Tag
4242
// or a type nested in Tag.
43-
guard let type = context.typeOfLexicalContext else {
43+
guard let type = context.typeOfLexicalContext(containing: variableDecl) else {
4444
context.diagnose(.nonMemberTagDeclarationNotSupported(variableDecl, whenUsing: node))
4545
return _fallbackAccessorDecls
4646
}
4747

48+
#if compiler(>=5.11)
4849
// Check that the tag is declared within Tag's namespace.
4950
let typeNameTokens: [String] = type.tokens(viewMode: .fixedUp).lazy
5051
.filter { $0.tokenKind != .period }
@@ -74,6 +75,7 @@ public struct TagMacro: PeerMacro, AccessorMacro, Sendable {
7475
return _fallbackAccessorDecls
7576
}
7677
}
78+
#endif
7779

7880
// We know the tag is nested in Tag. Now check that it is a static member.
7981
guard variableDecl.modifiers.map(\.name.tokenKind).contains(.keyword(.static)) else {

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
2626
}
2727

2828
let functionDecl = declaration.cast(FunctionDeclSyntax.self)
29-
let typeName = context.typeOfLexicalContext
29+
let typeName = context.typeOfLexicalContext(containing: functionDecl)
3030

3131
return _createTestContainerDecls(for: functionDecl, on: typeName, testAttribute: node, in: context)
3232
}
@@ -381,9 +381,36 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
381381
) -> [DeclSyntax] {
382382
var result = [DeclSyntax]()
383383

384+
#if compiler(>=5.11)
384385
// Get the name of the type containing the function for passing to the test
385386
// factory function later.
386387
let typealiasExpr: ExprSyntax = typeName.map { "\($0).self" } ?? "nil"
388+
#else
389+
// We cannot directly refer to Self here because it will end up being
390+
// resolved as the __TestContainer type we generate. Create a uniquely-named
391+
// reference to Self outside the context of the generated type, and use it
392+
// when the generated type needs to refer to the containing type.
393+
//
394+
// To support covariant Self on classes, we embed the reference to Self
395+
// inside a static computed property instead of a typealias (where covariant
396+
// Self is disallowed.)
397+
//
398+
// This "typealias" is not necessary when using the Swift 6 compiler.
399+
var typealiasExpr: ExprSyntax = "nil"
400+
if let typeName {
401+
let typealiasName = context.makeUniqueName(thunking: functionDecl)
402+
result.append(
403+
"""
404+
@available(*, deprecated, message: "This property is an implementation detail of the testing library. Do not use it directly.")
405+
private static nonisolated var \(typealiasName): Any.Type {
406+
\(typeName).self
407+
}
408+
"""
409+
)
410+
411+
typealiasExpr = "\(typealiasName)"
412+
}
413+
#endif
387414

388415
if typeName != nil, let genericGuardDecl = makeGenericGuardDecl(guardingAgainst: functionDecl, in: context) {
389416
result.append(genericGuardDecl)

Tests/TestingMacrosTests/TagMacroTests.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ import SwiftSyntax
1717
import SwiftSyntaxBuilder
1818
import SwiftSyntaxMacros
1919

20-
@Suite("TagMacro Tests")
20+
#if compiler(>=5.11)
21+
private let swift6Compiler = true
22+
#else
23+
private let swift6Compiler = false
24+
#endif
25+
26+
@Suite("TagMacro Tests", .enabled(if: swift6Compiler, "@Tag tests require lexical context"))
2127
struct TagMacroTests {
2228
@Test("@Tag macro",
2329
arguments: [

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@ import SwiftSyntax
1717
import SwiftSyntaxBuilder
1818
import SwiftSyntaxMacros
1919

20+
#if compiler(>=5.11)
21+
private let swift6Compiler = true
22+
#else
23+
private let swift6Compiler = false
24+
#endif
25+
2026
@Suite("TestDeclarationMacro Tests")
2127
struct TestDeclarationMacroTests {
2228
@Test("Error diagnostics emitted on API misuse",
29+
.enabled(if: swift6Compiler, "Some error diagnostics require lexical context"),
2330
arguments: [
2431
// Generic declarations
2532
"@Suite struct S<T> {}":

0 commit comments

Comments
 (0)