Skip to content

Commit 5b4d6d6

Browse files
Don't use @_semantics, use our own attribute instead. (#874)
This PR replaces our use of `@_semantics` with a custom attribute macro. `@_semantics` is reserved for use by the standard library and runtime and, while useful, can be emulated on our side of the module barrier without much difficulty. ### 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. --------- Co-authored-by: Stuart Montgomery <[email protected]>
1 parent 840c822 commit 5b4d6d6

File tree

7 files changed

+141
-18
lines changed

7 files changed

+141
-18
lines changed

Sources/Testing/Test+Macro.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,27 @@ extension Test {
484484
}
485485
}
486486

487+
// MARK: - Test pragmas
488+
489+
/// A macro used similarly to `#pragma` in C or `@_semantics` in the standard
490+
/// library.
491+
///
492+
/// - Parameters:
493+
/// - arguments: Zero or more context-specific arguments.
494+
///
495+
/// The use cases for this macro are subject to change over time as the needs of
496+
/// the testing library change. The implementation of this macro in the
497+
/// TestingMacros target determines how different arguments are handled.
498+
///
499+
/// - Note: This macro has compile-time effects _only_ and should not affect a
500+
/// compiled test target.
501+
///
502+
/// - Warning: This macro is used to implement other macros declared by the testing
503+
/// library. Do not use it directly.
504+
@attached(peer) public macro __testing(
505+
semantics arguments: _const String...
506+
) = #externalMacro(module: "TestingMacros", type: "PragmaMacro")
507+
487508
// MARK: - Helper functions
488509

489510
/// A function that abstracts away whether or not the `try` keyword is needed on

Sources/TestingMacros/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ endif()
8181

8282
target_sources(TestingMacros PRIVATE
8383
ConditionMacro.swift
84+
PragmaMacro.swift
8485
SourceLocationMacro.swift
8586
SuiteDeclarationMacro.swift
8687
Support/Additions/DeclGroupSyntaxAdditions.swift
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
public import SwiftSyntax
12+
public import SwiftSyntaxMacros
13+
14+
/// A type describing the expansion of the `@__testing` attribute macro.
15+
///
16+
/// Supported uses:
17+
///
18+
/// - `@__testing(semantics: "nomacrowarnings")`: suppress warning diagnostics
19+
/// generated by macros. (The implementation of this use case is held in trust
20+
/// at ``MacroExpansionContext/areWarningsSuppressed``.
21+
///
22+
/// This type is used to implement the `@__testing` attribute macro. Do not use
23+
/// it directly.
24+
public struct PragmaMacro: PeerMacro, Sendable {
25+
public static func expansion(
26+
of node: AttributeSyntax,
27+
providingPeersOf declaration: some DeclSyntaxProtocol,
28+
in context: some MacroExpansionContext
29+
) throws -> [DeclSyntax] {
30+
return []
31+
}
32+
33+
public static var formatMode: FormatMode {
34+
.disabled
35+
}
36+
}
37+
38+
/// Get all pragma attributes (`@__testing`) associated with a syntax node.
39+
///
40+
/// - Parameters:
41+
/// - node: The syntax node to inspect.
42+
///
43+
/// - Returns: The set of pragma attributes strings associated with `node`.
44+
///
45+
/// Attributes conditionally applied with `#if` are ignored.
46+
func pragmas(on node: some WithAttributesSyntax) -> [AttributeSyntax] {
47+
node.attributes
48+
.compactMap { attribute in
49+
if case let .attribute(attribute) = attribute {
50+
return attribute
51+
}
52+
return nil
53+
}.filter { attribute in
54+
attribute.attributeName.isNamed("__testing", inModuleNamed: "Testing")
55+
}
56+
}
57+
58+
/// Get all "semantics" attributed to a syntax node using the
59+
/// `@__testing(semantics:)` attribute.
60+
///
61+
/// - Parameters:
62+
/// - node: The syntax node to inspect.
63+
///
64+
/// - Returns: The set of "semantics" strings associated with `node`.
65+
///
66+
/// Attributes conditionally applied with `#if` are ignored.
67+
func semantics(of node: some WithAttributesSyntax) -> [String] {
68+
pragmas(on: node)
69+
.compactMap { attribute in
70+
if case let .argumentList(arguments) = attribute.arguments {
71+
return arguments
72+
}
73+
return nil
74+
}.filter { arguments in
75+
arguments.first?.label?.textWithoutBackticks == "semantics"
76+
}.flatMap { argument in
77+
argument.compactMap { $0.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue }
78+
}
79+
}

Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,28 +84,20 @@ extension MacroExpansionContext {
8484
/// lexical context.
8585
///
8686
/// The value of this property is `true` if the current lexical context
87-
/// contains a node with the `@_semantics("testing.macros.nowarnings")`
88-
/// attribute applied to it.
87+
/// contains a node with the `@__testing(semantics: "nowarnings")` attribute
88+
/// applied to it.
8989
///
9090
/// - Warning: This functionality is not part of the public interface of the
9191
/// testing library. It may be modified or removed in a future update.
9292
var areWarningsSuppressed: Bool {
9393
#if DEBUG
94-
for lexicalContext in self.lexicalContext {
95-
guard let lexicalContext = lexicalContext.asProtocol((any WithAttributesSyntax).self) else {
96-
continue
97-
}
98-
for attribute in lexicalContext.attributes {
99-
if case let .attribute(attribute) = attribute,
100-
attribute.attributeNameText == "_semantics",
101-
case let .string(argument) = attribute.arguments,
102-
argument.representedLiteralValue == "testing.macros.nowarnings" {
103-
return true
104-
}
105-
}
106-
}
107-
#endif
94+
return lexicalContext
95+
.compactMap { $0.asProtocol((any WithAttributesSyntax).self) }
96+
.flatMap { semantics(of: $0) }
97+
.contains("nomacrowarnings")
98+
#else
10899
return false
100+
#endif
109101
}
110102

111103
/// Emit a diagnostic message.

Sources/TestingMacros/TestingMacrosMain.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct TestingMacrosMain: CompilerPlugin {
3030
ExitTestRequireMacro.self,
3131
TagMacro.self,
3232
SourceLocationMacro.self,
33+
PragmaMacro.self,
3334
]
3435
}
3536
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
import Testing
12+
@testable import TestingMacros
13+
14+
import SwiftParser
15+
import SwiftSyntax
16+
17+
@Suite("PragmaMacro Tests")
18+
struct PragmaMacroTests {
19+
@Test func findSemantics() throws {
20+
let node = """
21+
@Testing.__testing(semantics: "abc123")
22+
@__testing(semantics: "def456")
23+
let x = 0
24+
""" as DeclSyntax
25+
let nodeWithAttributes = try #require(node.asProtocol((any WithAttributesSyntax).self))
26+
let semantics = semantics(of: nodeWithAttributes)
27+
#expect(semantics == ["abc123", "def456"])
28+
}
29+
}

Tests/TestingTests/IssueTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,7 @@ final class IssueTests: XCTestCase {
992992
await fulfillment(of: [errorCaught, apiMisused, expectationFailed], timeout: 0.0)
993993
}
994994

995-
@_semantics("testing.macros.nowarnings")
995+
@__testing(semantics: "nomacrowarnings")
996996
func testErrorCheckingWithRequire_ResultValueIsNever_VariousSyntaxes() throws {
997997
// Basic expressions succeed and don't diagnose.
998998
#expect(throws: Never.self) {}
@@ -1004,7 +1004,7 @@ final class IssueTests: XCTestCase {
10041004

10051005
// Casting to any Error throws an API misuse error because Never cannot be
10061006
// instantiated. NOTE: inner function needed for lexical context.
1007-
@_semantics("testing.macros.nowarnings")
1007+
@__testing(semantics: "nomacrowarnings")
10081008
func castToAnyError() throws {
10091009
let _: any Error = try #require(throws: Never.self) {}
10101010
}

0 commit comments

Comments
 (0)