diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 6f8536ac1..0fb29562d 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 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") + // MARK: - Helper functions /// A function that abstracts away whether or not the `try` keyword is needed on 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 diff --git a/Sources/TestingMacros/PragmaMacro.swift b/Sources/TestingMacros/PragmaMacro.swift new file mode 100644 index 000000000..48027b213 --- /dev/null +++ b/Sources/TestingMacros/PragmaMacro.swift @@ -0,0 +1,79 @@ +// +// 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 + } +} + +/// 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.attributeName.isNamed("__testing", inModuleNamed: "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 ca0137b5d..8bcf2522a 100644 --- a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift @@ -84,28 +84,20 @@ 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. 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 == "_semantics", - case let .string(argument) = attribute.arguments, - argument.representedLiteralValue == "testing.macros.nowarnings" { - return true - } - } - } -#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/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/TestingMacrosTests/PragmaMacroTests.swift b/Tests/TestingMacrosTests/PragmaMacroTests.swift new file mode 100644 index 000000000..9e85419da --- /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.__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"]) + } +} 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) {} }