From 0ce98474aaebca657fb0ba72f25123476703dfcb Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 20 Dec 2024 11:29:03 -0500 Subject: [PATCH 1/5] Don't use `@_semantics`, use our own attribute instead. 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. Note there are no unit tests for this macro: it doesn't actually _do_ anything on its own! --- Sources/Testing/Test+Macro.swift | 21 +++++++++++ Sources/TestingMacros/PragmaMacro.swift | 36 +++++++++++++++++++ .../MacroExpansionContextAdditions.swift | 16 +++++---- Sources/TestingMacros/TestingMacrosMain.swift | 1 + Tests/TestingTests/IssueTests.swift | 4 +-- 5 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 Sources/TestingMacros/PragmaMacro.swift diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 6f8536ac1..bd5b51223 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -484,6 +484,27 @@ extension Test { } } +// MARK: - Test pragmas + +/// A macro used similarly to `#pragma` in C or `@_semantics` in the standard +/// library. +/// +/// - Parameters: +/// - arguments: Zero or more context-specific arguments. +/// +/// The use cases for this macro are subject to change over time as the needs of +/// the testing library change. The implementation of this macro in the +/// TestingMacros target determines how different arguments are handled. +/// +/// - Note: This macro has compile-time effects _only_ and should not affect a +/// compiled test target. +/// +/// - Warning: This macro is used to implement the `@Test` macro. Do not use it +/// directly. +@attached(peer) public macro __testing( + semantics arguments: _const String... +) = #externalMacro(module: "TestingMacros", type: "PragmaMacro") + // MARK: - Helper functions /// A function that abstracts away whether or not the `try` keyword is needed on diff --git a/Sources/TestingMacros/PragmaMacro.swift b/Sources/TestingMacros/PragmaMacro.swift new file mode 100644 index 000000000..6d42c38fb --- /dev/null +++ b/Sources/TestingMacros/PragmaMacro.swift @@ -0,0 +1,36 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +public import SwiftSyntax +public import SwiftSyntaxMacros + +/// A type describing the expansion of the `@__testing` attribute macro. +/// +/// Supported uses: +/// +/// - `@__testing(semantics: "nomacrowarnings")`: suppress warning diagnostics +/// generated by macros. (The implementation of this use case is held in trust +/// at ``MacroExpansionContext/areWarningsSuppressed``. +/// +/// This type is used to implement the `@__testing` attribute macro. Do not use +/// it directly. +public struct PragmaMacro: PeerMacro, Sendable { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + return [] + } + + public static var formatMode: FormatMode { + .disabled + } +} diff --git a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift index ca0137b5d..d8680fe57 100644 --- a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift @@ -84,8 +84,8 @@ extension MacroExpansionContext { /// lexical context. /// /// The value of this property is `true` if the current lexical context - /// contains a node with the `@_semantics("testing.macros.nowarnings")` - /// attribute applied to it. + /// contains a node with the `@__testing(semantics: "nowarnings")` attribute + /// applied to it. /// /// - Warning: This functionality is not part of the public interface of the /// testing library. It may be modified or removed in a future update. @@ -97,10 +97,14 @@ extension MacroExpansionContext { } for attribute in lexicalContext.attributes { if case let .attribute(attribute) = attribute, - attribute.attributeNameText == "_semantics", - case let .string(argument) = attribute.arguments, - argument.representedLiteralValue == "testing.macros.nowarnings" { - return true + attribute.attributeNameText == "__testing", + case let .argumentList(arguments) = attribute.arguments { + return arguments.contains { argument in + guard let argument = arguments.first?.expression.as(StringLiteralExprSyntax.self) else { + return false + } + return argument.representedLiteralValue == "nomacrowarnings" + } } } } diff --git a/Sources/TestingMacros/TestingMacrosMain.swift b/Sources/TestingMacros/TestingMacrosMain.swift index c6904a6e7..1894f4282 100644 --- a/Sources/TestingMacros/TestingMacrosMain.swift +++ b/Sources/TestingMacros/TestingMacrosMain.swift @@ -30,6 +30,7 @@ struct TestingMacrosMain: CompilerPlugin { ExitTestRequireMacro.self, TagMacro.self, SourceLocationMacro.self, + PragmaMacro.self, ] } } diff --git a/Tests/TestingTests/IssueTests.swift b/Tests/TestingTests/IssueTests.swift index 631ff0c54..a73f0706d 100644 --- a/Tests/TestingTests/IssueTests.swift +++ b/Tests/TestingTests/IssueTests.swift @@ -992,7 +992,7 @@ final class IssueTests: XCTestCase { await fulfillment(of: [errorCaught, apiMisused, expectationFailed], timeout: 0.0) } - @_semantics("testing.macros.nowarnings") + @__testing(semantics: "nomacrowarnings") func testErrorCheckingWithRequire_ResultValueIsNever_VariousSyntaxes() throws { // Basic expressions succeed and don't diagnose. #expect(throws: Never.self) {} @@ -1004,7 +1004,7 @@ final class IssueTests: XCTestCase { // Casting to any Error throws an API misuse error because Never cannot be // instantiated. NOTE: inner function needed for lexical context. - @_semantics("testing.macros.nowarnings") + @__testing(semantics: "nomacrowarnings") func castToAnyError() throws { let _: any Error = try #require(throws: Never.self) {} } From 9be37f421b73d4b547c204bef5fed3b83f369f99 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 20 Dec 2024 11:33:20 -0500 Subject: [PATCH 2/5] Update CMake --- Sources/TestingMacros/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/TestingMacros/CMakeLists.txt b/Sources/TestingMacros/CMakeLists.txt index ad58fc35b..acf09f339 100644 --- a/Sources/TestingMacros/CMakeLists.txt +++ b/Sources/TestingMacros/CMakeLists.txt @@ -81,6 +81,7 @@ endif() target_sources(TestingMacros PRIVATE ConditionMacro.swift + PragmaMacro.swift SourceLocationMacro.swift SuiteDeclarationMacro.swift Support/Additions/DeclGroupSyntaxAdditions.swift From e24030d3ec98c1763fbce1a6adb76bf5336718cd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 20 Dec 2024 12:39:48 -0500 Subject: [PATCH 3/5] Refactor a bit and add a unit test --- Sources/TestingMacros/PragmaMacro.swift | 43 +++++++++++++++++++ .../MacroExpansionContextAdditions.swift | 24 +++-------- .../TestingMacrosTests/PragmaMacroTests.swift | 29 +++++++++++++ 3 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 Tests/TestingMacrosTests/PragmaMacroTests.swift diff --git a/Sources/TestingMacros/PragmaMacro.swift b/Sources/TestingMacros/PragmaMacro.swift index 6d42c38fb..22e185480 100644 --- a/Sources/TestingMacros/PragmaMacro.swift +++ b/Sources/TestingMacros/PragmaMacro.swift @@ -34,3 +34,46 @@ public struct PragmaMacro: PeerMacro, Sendable { .disabled } } + +/// Get all pragma attributes (`@__testing`) associated with a syntax node. +/// +/// - Parameters: +/// - node: The syntax node to inspect. +/// +/// - Returns: The set of pragma attributes strings associated with `node`. +/// +/// Attributes conditionally applied with `#if` are ignored. +func pragmas(on node: some WithAttributesSyntax) -> [AttributeSyntax] { + node.attributes + .compactMap { attribute in + if case let .attribute(attribute) = attribute { + return attribute + } + return nil + }.filter { attribute in + attribute.attributeNameText == "__testing" + } +} + +/// Get all "semantics" attributed to a syntax node using the +/// `@__testing(semantics:)` attribute. +/// +/// - Parameters: +/// - node: The syntax node to inspect. +/// +/// - Returns: The set of "semantics" strings associated with `node`. +/// +/// Attributes conditionally applied with `#if` are ignored. +func semantics(of node: some WithAttributesSyntax) -> [String] { + pragmas(on: node) + .compactMap { attribute in + if case let .argumentList(arguments) = attribute.arguments { + return arguments + } + return nil + }.filter { arguments in + arguments.first?.label?.textWithoutBackticks == "semantics" + }.flatMap { argument in + argument.compactMap { $0.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue } + } +} diff --git a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift index d8680fe57..8bcf2522a 100644 --- a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift @@ -91,25 +91,13 @@ extension MacroExpansionContext { /// testing library. It may be modified or removed in a future update. var areWarningsSuppressed: Bool { #if DEBUG - for lexicalContext in self.lexicalContext { - guard let lexicalContext = lexicalContext.asProtocol((any WithAttributesSyntax).self) else { - continue - } - for attribute in lexicalContext.attributes { - if case let .attribute(attribute) = attribute, - attribute.attributeNameText == "__testing", - case let .argumentList(arguments) = attribute.arguments { - return arguments.contains { argument in - guard let argument = arguments.first?.expression.as(StringLiteralExprSyntax.self) else { - return false - } - return argument.representedLiteralValue == "nomacrowarnings" - } - } - } - } -#endif + return lexicalContext + .compactMap { $0.asProtocol((any WithAttributesSyntax).self) } + .flatMap { semantics(of: $0) } + .contains("nomacrowarnings") +#else return false +#endif } /// Emit a diagnostic message. diff --git a/Tests/TestingMacrosTests/PragmaMacroTests.swift b/Tests/TestingMacrosTests/PragmaMacroTests.swift new file mode 100644 index 000000000..cfd4a3f13 --- /dev/null +++ b/Tests/TestingMacrosTests/PragmaMacroTests.swift @@ -0,0 +1,29 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +import Testing +@testable import TestingMacros + +import SwiftParser +import SwiftSyntax + +@Suite("PragmaMacro Tests") +struct PragmaMacroTests { + @Test func findSemantics() throws { + let node = """ + @__testing(semantics: "abc123") + @__testing(semantics: "def456") + let x = 0 + """ as DeclSyntax + let nodeWithAttributes = try #require(node.asProtocol((any WithAttributesSyntax).self)) + let semantics = semantics(of: nodeWithAttributes) + #expect(semantics == ["abc123", "def456"]) + } +} From 40dd791d61476448f01c7ffdaf026b1c8a0e24f2 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 20 Dec 2024 17:50:13 -0500 Subject: [PATCH 4/5] Detect fully-qualified attribute name --- Sources/TestingMacros/PragmaMacro.swift | 2 +- Tests/TestingMacrosTests/PragmaMacroTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/TestingMacros/PragmaMacro.swift b/Sources/TestingMacros/PragmaMacro.swift index 22e185480..48027b213 100644 --- a/Sources/TestingMacros/PragmaMacro.swift +++ b/Sources/TestingMacros/PragmaMacro.swift @@ -51,7 +51,7 @@ func pragmas(on node: some WithAttributesSyntax) -> [AttributeSyntax] { } return nil }.filter { attribute in - attribute.attributeNameText == "__testing" + attribute.attributeName.isNamed("__testing", inModuleNamed: "Testing") } } diff --git a/Tests/TestingMacrosTests/PragmaMacroTests.swift b/Tests/TestingMacrosTests/PragmaMacroTests.swift index cfd4a3f13..9e85419da 100644 --- a/Tests/TestingMacrosTests/PragmaMacroTests.swift +++ b/Tests/TestingMacrosTests/PragmaMacroTests.swift @@ -18,7 +18,7 @@ import SwiftSyntax struct PragmaMacroTests { @Test func findSemantics() throws { let node = """ - @__testing(semantics: "abc123") + @Testing.__testing(semantics: "abc123") @__testing(semantics: "def456") let x = 0 """ as DeclSyntax From 4e63cc046a52f03441770af8462165c2d2c5e667 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 20 Dec 2024 19:33:14 -0500 Subject: [PATCH 5/5] Update Sources/Testing/Test+Macro.swift Co-authored-by: Stuart Montgomery --- Sources/Testing/Test+Macro.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index bd5b51223..0fb29562d 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -499,8 +499,8 @@ extension Test { /// - Note: This macro has compile-time effects _only_ and should not affect a /// compiled test target. /// -/// - Warning: This macro is used to implement the `@Test` macro. Do not use it -/// directly. +/// - Warning: This macro is used to implement other macros declared by the testing +/// library. Do not use it directly. @attached(peer) public macro __testing( semantics arguments: _const String... ) = #externalMacro(module: "TestingMacros", type: "PragmaMacro")