From c727506157bea28d11e95a3711c7174a5d6fc333 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 16:08:06 -0400 Subject: [PATCH 1/5] Treat `@_unavailableInEmbedded` like other availability attributes during macro expansion. This PR adds support for mapping `@_unavailableInEmbedded` to a condition trait like we do with `@available()` etc. --- .../Testing/Traits/ConditionTrait+Macro.swift | 24 ++++++++++++++ .../WithAttributesSyntaxAdditions.swift | 11 +++++++ .../Support/AvailabilityGuards.swift | 33 +++++++++++++++++++ Tests/TestingTests/RunnerTests.swift | 19 +++++++++++ 4 files changed, 87 insertions(+) diff --git a/Sources/Testing/Traits/ConditionTrait+Macro.swift b/Sources/Testing/Traits/ConditionTrait+Macro.swift index dbddcfc1f..89aaa67d0 100644 --- a/Sources/Testing/Traits/ConditionTrait+Macro.swift +++ b/Sources/Testing/Traits/ConditionTrait+Macro.swift @@ -124,4 +124,28 @@ extension Trait where Self == ConditionTrait { sourceLocation: sourceLocation ) } + + /// Create a trait controlling availability of a test based on an + /// `@available(*, unavailable)` attribute applied to it. + /// + /// - Parameters: + /// - message: The `message` parameter of the availability attribute. + /// - sourceLocation: The source location of the test. + /// + /// - Returns: A trait. + /// + /// - Warning: This function is used to implement the `@Test` macro. Do not + /// call it directly. + public static func __unavailableInEmbedded(sourceLocation: SourceLocation) -> Self { +#if hasFeature(Embedded) + let isEmbedded = true +#else + let isEmbedded = false +#endif + return Self( + kind: .unconditional(!isEmbedded), + comments: ["Marked @_unavailableInEmbedded"], + sourceLocation: sourceLocation + ) + } } diff --git a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift index 52d85bbd4..45f3af9bb 100644 --- a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift @@ -114,6 +114,17 @@ extension WithAttributesSyntax { }.first { $0.attributeNameText == "_unavailableFromAsync" } } + /// The first `@_unavailableInEmbedded` attribute on this instance, if any. + var noembeddedAttribute: AttributeSyntax? { + availability(when: .noasync).first?.attribute ?? attributes.lazy + .compactMap { attribute in + if case let .attribute(attribute) = attribute { + return attribute + } + return nil + }.first { $0.attributeNameText == "_unavailableInEmbedded" } + } + /// Find all attributes on this node, if any, with the given name. /// /// - Parameters: diff --git a/Sources/TestingMacros/Support/AvailabilityGuards.swift b/Sources/TestingMacros/Support/AvailabilityGuards.swift index e9f4ba762..98d07e40f 100644 --- a/Sources/TestingMacros/Support/AvailabilityGuards.swift +++ b/Sources/TestingMacros/Support/AvailabilityGuards.swift @@ -140,6 +140,24 @@ private func _createAvailabilityTraitExpr( } } +/// Create an expression that contains a test trait for symbols that are +/// unavailable in Embedded Swift. +/// +/// - Parameters: +/// - attribute: The `@_unavailableInEmbedded` attribute. +/// - context: The macro context in which the expression is being parsed. +/// +/// - Returns: An instance of `ExprSyntax` representing an instance of +/// ``Trait`` that can be used to prevent a test from running in Embedded +/// Swift. +private func _createNoEmbeddedAvailabilityTraitExpr( + from attribute: AttributeSyntax, + in context: some MacroExpansionContext +) -> ExprSyntax { + let sourceLocationExpr = createSourceLocationExpr(of: attribute, context: context) + return ".__unavailableInEmbedded(sourceLocation: \(sourceLocationExpr))" +} + /// Create an expression that contains test traits for availability (i.e. /// `.enabled(if: ...)`). /// @@ -169,6 +187,10 @@ func createAvailabilityTraitExprs( _createAvailabilityTraitExpr(from: availability, when: .obsoleted, in: context) } + if let noembeddedAttribute = decl.noembeddedAttribute { + result += [_createNoEmbeddedAvailabilityTraitExpr(from: noembeddedAttribute, in: context)] + } + return result } @@ -290,5 +312,16 @@ func createSyntaxNode( } } + // Handle Embedded Swift. + if decl.noembeddedAttribute != nil { + result = """ + #if !hasFeature(Embedded) + \(result) + #else + \(exitStatement) + #endif + """ + } + return result } diff --git a/Tests/TestingTests/RunnerTests.swift b/Tests/TestingTests/RunnerTests.swift index 335f8be37..d84b00376 100644 --- a/Tests/TestingTests/RunnerTests.swift +++ b/Tests/TestingTests/RunnerTests.swift @@ -819,6 +819,25 @@ final class RunnerTests: XCTestCase { await fulfillment(of: [testStarted], timeout: 0.0) } + @Suite(.hidden) struct UnavailableInEmbeddedTests { + @Test(.hidden) + @_unavailableInEmbedded + func embedded() {} + } + + func testUnavailableInEmbeddedAttribute() async throws { + let testStarted = expectation(description: "Test started") + testStarted.expectedFulfillmentCount = 3 + var configuration = Configuration() + configuration.eventHandler = { event, _ in + if case .testStarted = event.kind { + testStarted.fulfill() + } + } + await runTest(for: UnavailableInEmbeddedTests.self, configuration: configuration) + await fulfillment(of: [testStarted], timeout: 0.0) + } + #if !SWT_NO_GLOBAL_ACTORS @TaskLocal static var isMainActorIsolationEnforced = false From ea36527ab26b119a8515973685d537e0718d724d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 16:30:34 -0400 Subject: [PATCH 2/5] Fix DocC --- Sources/Testing/Traits/ConditionTrait+Macro.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Testing/Traits/ConditionTrait+Macro.swift b/Sources/Testing/Traits/ConditionTrait+Macro.swift index 89aaa67d0..fd489b9e6 100644 --- a/Sources/Testing/Traits/ConditionTrait+Macro.swift +++ b/Sources/Testing/Traits/ConditionTrait+Macro.swift @@ -126,10 +126,9 @@ extension Trait where Self == ConditionTrait { } /// Create a trait controlling availability of a test based on an - /// `@available(*, unavailable)` attribute applied to it. + /// `@_unavailableInEmbedded` attribute applied to it. /// /// - Parameters: - /// - message: The `message` parameter of the availability attribute. /// - sourceLocation: The source location of the test. /// /// - Returns: A trait. From 15782999338785464e8b639fbd176a49052fccc4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 16:34:52 -0400 Subject: [PATCH 3/5] D.R.Y. --- .../FunctionDeclSyntaxAdditions.swift | 10 ++----- .../WithAttributesSyntaxAdditions.swift | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift index fa390775a..108c3d74a 100644 --- a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift @@ -87,14 +87,8 @@ extension FunctionDeclSyntax { var xcTestCompatibleSelector: ObjCSelectorPieceListSyntax? { // First, look for an @objc attribute with an explicit selector, and use // that if found. - let objcAttribute = attributes.lazy - .compactMap { - if case let .attribute(attribute) = $0 { - return attribute - } - return nil - }.first { $0.attributeNameText == "objc" } - if let objcAttribute, case let .objCName(objCName) = objcAttribute.arguments { + if objcAttribute = firstAttribute(named: "objc"), + case let .objCName(objCName) = objcAttribute.arguments { if true == objCName.first?.name?.textWithoutBackticks.hasPrefix("test") { return objCName } diff --git a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift index 45f3af9bb..210f3fad6 100644 --- a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift @@ -102,27 +102,33 @@ extension WithAttributesSyntax { } } - /// The first `@available(*, noasync)` or `@_unavailableFromAsync` attribute - /// on this instance, if any. - var noasyncAttribute: AttributeSyntax? { - availability(when: .noasync).first?.attribute ?? attributes.lazy + /// Find the first attribute on this syntax node with the given name. + /// + /// - Parameters: + /// - name: The name of the attribute to search for. + /// + /// - Returns: The first `AttributeSyntax` node on `self` with the given name, + /// or `nil` if none was found. + func firstAttribute(named name: String) -> AttributeSyntax? { + attributes.lazy .compactMap { attribute in if case let .attribute(attribute) = attribute { return attribute } return nil - }.first { $0.attributeNameText == "_unavailableFromAsync" } + }.first { $0.attributeNameText == name } + } + + /// The first `@available(*, noasync)` or `@_unavailableFromAsync` attribute + /// on this instance, if any. + var noasyncAttribute: AttributeSyntax? { + availability(when: .noasync).first?.attribute + ?? firstAttribute(named: "_unavailableFromAsync") } /// The first `@_unavailableInEmbedded` attribute on this instance, if any. var noembeddedAttribute: AttributeSyntax? { - availability(when: .noasync).first?.attribute ?? attributes.lazy - .compactMap { attribute in - if case let .attribute(attribute) = attribute { - return attribute - } - return nil - }.first { $0.attributeNameText == "_unavailableInEmbedded" } + firstAttribute(named: "_unavailableInEmbedded") } /// Find all attributes on this node, if any, with the given name. From fc87d84f192a90ba2c617defcd96a17dcd4a1966 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 16:41:30 -0400 Subject: [PATCH 4/5] D.R.Y. (again) --- .../FunctionDeclSyntaxAdditions.swift | 2 +- .../WithAttributesSyntaxAdditions.swift | 21 ++----------------- Tests/TestingTests/RunnerTests.swift | 4 ++++ 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift index 108c3d74a..8065d299e 100644 --- a/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/FunctionDeclSyntaxAdditions.swift @@ -87,7 +87,7 @@ extension FunctionDeclSyntax { var xcTestCompatibleSelector: ObjCSelectorPieceListSyntax? { // First, look for an @objc attribute with an explicit selector, and use // that if found. - if objcAttribute = firstAttribute(named: "objc"), + if let objcAttribute = attributes(named: "objc", inModuleNamed: "Swift").first, case let .objCName(objCName) = objcAttribute.arguments { if true == objCName.first?.name?.textWithoutBackticks.hasPrefix("test") { return objCName diff --git a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift index 210f3fad6..fb9397491 100644 --- a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift @@ -102,33 +102,16 @@ extension WithAttributesSyntax { } } - /// Find the first attribute on this syntax node with the given name. - /// - /// - Parameters: - /// - name: The name of the attribute to search for. - /// - /// - Returns: The first `AttributeSyntax` node on `self` with the given name, - /// or `nil` if none was found. - func firstAttribute(named name: String) -> AttributeSyntax? { - attributes.lazy - .compactMap { attribute in - if case let .attribute(attribute) = attribute { - return attribute - } - return nil - }.first { $0.attributeNameText == name } - } - /// The first `@available(*, noasync)` or `@_unavailableFromAsync` attribute /// on this instance, if any. var noasyncAttribute: AttributeSyntax? { availability(when: .noasync).first?.attribute - ?? firstAttribute(named: "_unavailableFromAsync") + ?? attributes(named: "_unavailableFromAsync", inModuleNamed: "Swift").first } /// The first `@_unavailableInEmbedded` attribute on this instance, if any. var noembeddedAttribute: AttributeSyntax? { - firstAttribute(named: "_unavailableInEmbedded") + attributes(named: "_unavailableInEmbedded", inModuleNamed: "Swift").first } /// Find all attributes on this node, if any, with the given name. diff --git a/Tests/TestingTests/RunnerTests.swift b/Tests/TestingTests/RunnerTests.swift index d84b00376..c254e5ba9 100644 --- a/Tests/TestingTests/RunnerTests.swift +++ b/Tests/TestingTests/RunnerTests.swift @@ -827,7 +827,11 @@ final class RunnerTests: XCTestCase { func testUnavailableInEmbeddedAttribute() async throws { let testStarted = expectation(description: "Test started") +#if !hasFeature(Embedded) testStarted.expectedFulfillmentCount = 3 +#else + testStarted.isInverted = true +#endif var configuration = Configuration() configuration.eventHandler = { event, _ in if case .testStarted = event.kind { From 8eb51ce8c0965516cfcca7cb67dfc871e83a1c1e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 25 Sep 2025 16:49:16 -0400 Subject: [PATCH 5/5] Simplify trait emission a bit --- .../WithAttributesSyntaxAdditions.swift | 2 +- .../Support/AvailabilityGuards.swift | 25 +++---------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift index fb9397491..aa778a00c 100644 --- a/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/WithAttributesSyntaxAdditions.swift @@ -110,7 +110,7 @@ extension WithAttributesSyntax { } /// The first `@_unavailableInEmbedded` attribute on this instance, if any. - var noembeddedAttribute: AttributeSyntax? { + var unavailableInEmbeddedAttribute: AttributeSyntax? { attributes(named: "_unavailableInEmbedded", inModuleNamed: "Swift").first } diff --git a/Sources/TestingMacros/Support/AvailabilityGuards.swift b/Sources/TestingMacros/Support/AvailabilityGuards.swift index 98d07e40f..deb3a0f8b 100644 --- a/Sources/TestingMacros/Support/AvailabilityGuards.swift +++ b/Sources/TestingMacros/Support/AvailabilityGuards.swift @@ -140,24 +140,6 @@ private func _createAvailabilityTraitExpr( } } -/// Create an expression that contains a test trait for symbols that are -/// unavailable in Embedded Swift. -/// -/// - Parameters: -/// - attribute: The `@_unavailableInEmbedded` attribute. -/// - context: The macro context in which the expression is being parsed. -/// -/// - Returns: An instance of `ExprSyntax` representing an instance of -/// ``Trait`` that can be used to prevent a test from running in Embedded -/// Swift. -private func _createNoEmbeddedAvailabilityTraitExpr( - from attribute: AttributeSyntax, - in context: some MacroExpansionContext -) -> ExprSyntax { - let sourceLocationExpr = createSourceLocationExpr(of: attribute, context: context) - return ".__unavailableInEmbedded(sourceLocation: \(sourceLocationExpr))" -} - /// Create an expression that contains test traits for availability (i.e. /// `.enabled(if: ...)`). /// @@ -187,8 +169,9 @@ func createAvailabilityTraitExprs( _createAvailabilityTraitExpr(from: availability, when: .obsoleted, in: context) } - if let noembeddedAttribute = decl.noembeddedAttribute { - result += [_createNoEmbeddedAvailabilityTraitExpr(from: noembeddedAttribute, in: context)] + if let attribute = decl.unavailableInEmbeddedAttribute { + let sourceLocationExpr = createSourceLocationExpr(of: attribute, context: context) + result += [".__unavailableInEmbedded(sourceLocation: \(sourceLocationExpr))"] } return result @@ -313,7 +296,7 @@ func createSyntaxNode( } // Handle Embedded Swift. - if decl.noembeddedAttribute != nil { + if decl.unavailableInEmbeddedAttribute != nil { result = """ #if !hasFeature(Embedded) \(result)