diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift index f43aa38a998..66d48d2a21d 100644 --- a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift +++ b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift @@ -623,6 +623,7 @@ let diagnosticDomain: String = "SwiftSyntaxMacroExpansion" private enum MacroApplicationError: DiagnosticMessage, Error { case accessorMacroOnVariableWithMultipleBindings case peerMacroOnVariableWithMultipleBindings + case memberMacroOnInvalidDecl(macroName: String) case malformedAccessor var diagnosticID: MessageID { @@ -637,6 +638,8 @@ private enum MacroApplicationError: DiagnosticMessage, Error { return "accessor macro can only be applied to a single variable" case .peerMacroOnVariableWithMultipleBindings: return "peer macro can only be applied to a single variable" + case .memberMacroOnInvalidDecl(let macroName): + return "macro '\(macroName)' can only be applied to a struct, enum, class, extension, or actor" case .malformedAccessor: return """ macro returned a malformed accessor. Accessors should start with an introducer like 'get' or 'set'. @@ -702,6 +705,18 @@ private class MacroApplication: SyntaxRewriter { let attributedNode = node.asProtocol(WithAttributesSyntax.self), !attributedNode.attributes.isEmpty { + // Check if there are any member macros generated on a non-group decl. + if !(node.isProtocol(DeclGroupSyntax.self) || node.is(ExtensionDeclSyntax.self)) { + let memberMacros = self.macroAttributes(attachedTo: declSyntax, ofType: MemberMacro.Type.self) + for (attribute, _, _) in memberMacros { + let macroName = attribute.attributeName.trimmedDescription + contextGenerator(Syntax(node)).addDiagnostics( + from: MacroApplicationError.memberMacroOnInvalidDecl(macroName: macroName), + node: attribute + ) + } + } + // Apply body and preamble macros. if let nodeWithBody = node.asProtocol(WithOptionalCodeBlockSyntax.self), let declNodeWithBody = nodeWithBody as? any DeclSyntaxProtocol & WithOptionalCodeBlockSyntax diff --git a/Tests/SwiftSyntaxMacroExpansionTest/Issue2206Tests.swift b/Tests/SwiftSyntaxMacroExpansionTest/Issue2206Tests.swift new file mode 100644 index 00000000000..5f4225f99da --- /dev/null +++ b/Tests/SwiftSyntaxMacroExpansionTest/Issue2206Tests.swift @@ -0,0 +1,41 @@ + +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +private struct NoOpMemberMacro: MemberMacro { + static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + return [] + } +} + +final class Issue2206Tests: XCTestCase { + private let indentationWidth: Trivia = .spaces(2) + + func testMemberMacroOnVariable() { + // Issue #2206: assertMacroExpansion should emit an error if member macro is applied to declaration that can’t have members + // Currently this is expected to FAIL because the diagnostic is swallowed. + assertMacroExpansion( + """ + @Test + var x: Int + """, + expandedSource: """ + var x: Int + """, + diagnostics: [ + DiagnosticSpec(message: "macro 'Test' can only be applied to a struct, enum, class, extension, or actor", line: 1, column: 1) + ], + macros: ["Test": NoOpMemberMacro.self], + indentationWidth: indentationWidth + ) + } +}