From c8d0515b14294e1ee0b952149bd804c015ff0391 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 7 Jan 2025 11:58:55 -0500 Subject: [PATCH 1/2] Add an attribute that emits a warning. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds an attribute (an overload of `@__testing()`) that emits a warning. This allows us to emit compile-time warnings from contexts where only attributes are semantically valid. We need this in particular for test content records because their section info is comprised of a big `IfConfigDecl` node and platforms that aren't covered (i.e. the `#else` clause) don't have a way to emit a diagnostic that says "we need to fix this platform's Swift Testing port!" like we can do in other contexts with `#warning()`: ```swift @_section("__DATA_CONST,__swift5_tests") @_section("swift5_tests") @_section(".sw5test$B") // ⚠️ #warning isn't valid here, what do we do!? @_used private static let record: __TestContentRecord = (...) ``` --- Sources/Testing/Test+Macro.swift | 16 ++++++++++++++-- Sources/TestingMacros/PragmaMacro.swift | 15 ++++++++++++++- .../TestingMacrosTests/PragmaMacroTests.swift | 18 +++++++++++++++--- .../TestingMacrosTests/TestSupport/Parse.swift | 1 + Tests/TestingTests/MiscellaneousTests.swift | 2 ++ 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/Test+Macro.swift b/Sources/Testing/Test+Macro.swift index 0fb29562d..86fb42c14 100644 --- a/Sources/Testing/Test+Macro.swift +++ b/Sources/Testing/Test+Macro.swift @@ -499,12 +499,24 @@ 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 other macros declared by the testing -/// library. 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") +/// A macro used similarly to `#warning()` but in a position where only an +/// attribute is valid. +/// +/// - Parameters: +/// - message: A string to emit as a warning. +/// +/// - Warning: This macro is used to implement other macros declared by the +/// testing library. Do not use it directly. +@attached(peer) public macro __testing( + warning message: _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 index 48027b213..783440764 100644 --- a/Sources/TestingMacros/PragmaMacro.swift +++ b/Sources/TestingMacros/PragmaMacro.swift @@ -17,7 +17,9 @@ public import SwiftSyntaxMacros /// /// - `@__testing(semantics: "nomacrowarnings")`: suppress warning diagnostics /// generated by macros. (The implementation of this use case is held in trust -/// at ``MacroExpansionContext/areWarningsSuppressed``. +/// at ``MacroExpansionContext/areWarningsSuppressed``.) +/// - `@__testing(warning: "...")`: emits `"..."` as a diagnostic message +/// attributed to the node to which the attribute is attached. /// /// This type is used to implement the `@__testing` attribute macro. Do not use /// it directly. @@ -27,6 +29,17 @@ public struct PragmaMacro: PeerMacro, Sendable { providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { + if case let .argumentList(arguments) = node.arguments, + arguments.first?.label?.textWithoutBackticks == "warning" { + let targetNode = Syntax(declaration) + let messages = arguments + .map(\.expression) + .compactMap { $0.as(StringLiteralExprSyntax.self) } + .compactMap(\.representedLiteralValue) + .map { DiagnosticMessage(syntax: targetNode, message: $0, severity: .warning) } + context.diagnose(messages) + } + return [] } diff --git a/Tests/TestingMacrosTests/PragmaMacroTests.swift b/Tests/TestingMacrosTests/PragmaMacroTests.swift index 9e85419da..9bc592eb6 100644 --- a/Tests/TestingMacrosTests/PragmaMacroTests.swift +++ b/Tests/TestingMacrosTests/PragmaMacroTests.swift @@ -18,12 +18,24 @@ import SwiftSyntax struct PragmaMacroTests { @Test func findSemantics() throws { let node = """ - @Testing.__testing(semantics: "abc123") - @__testing(semantics: "def456") - let x = 0 + @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"]) } + + @Test func warningGenerated() throws { + let sourceCode = """ + @__testing(warning: "abc123") + let x = 0 + """ + + let (_, diagnostics) = try parse(sourceCode) + #expect(diagnostics.count == 1) + #expect(diagnostics[0].message == "abc123") + #expect(diagnostics[0].diagMessage.severity == .warning) + } } diff --git a/Tests/TestingMacrosTests/TestSupport/Parse.swift b/Tests/TestingMacrosTests/TestSupport/Parse.swift index e6b36e3b2..ecff8de58 100644 --- a/Tests/TestingMacrosTests/TestSupport/Parse.swift +++ b/Tests/TestingMacrosTests/TestSupport/Parse.swift @@ -30,6 +30,7 @@ fileprivate let allMacros: [String: any Macro.Type] = [ "Suite": SuiteDeclarationMacro.self, "Test": TestDeclarationMacro.self, "Tag": TagMacro.self, + "__testing": PragmaMacro.self, ] func parse(_ sourceCode: String, activeMacros activeMacroNames: [String] = [], removeWhitespace: Bool = false) throws -> (sourceCode: String, diagnostics: [Diagnostic]) { diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 3c987a9ad..a8cd56a7b 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -598,6 +598,8 @@ struct MiscellaneousTests { @_section("swift5_tests") #elseif os(Windows) @_section(".sw5test$B") +#else + @__testing(warning: "Platform-specific implementation missing: test content section name unavailable") #endif @_used private static let record: __TestContentRecord = ( From 2500b5ecfac45e6be6a78aa35f7d03f709a98e35 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Tue, 7 Jan 2025 13:49:26 -0500 Subject: [PATCH 2/2] Test should check the node the warning is attached to --- Tests/TestingMacrosTests/PragmaMacroTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/TestingMacrosTests/PragmaMacroTests.swift b/Tests/TestingMacrosTests/PragmaMacroTests.swift index 9bc592eb6..0d430c036 100644 --- a/Tests/TestingMacrosTests/PragmaMacroTests.swift +++ b/Tests/TestingMacrosTests/PragmaMacroTests.swift @@ -37,5 +37,6 @@ struct PragmaMacroTests { #expect(diagnostics.count == 1) #expect(diagnostics[0].message == "abc123") #expect(diagnostics[0].diagMessage.severity == .warning) + #expect(diagnostics[0].node.is(VariableDeclSyntax.self)) } }