swiftlang/swift-syntax

`assertMacroExpansion` passes 0 protocols to `ExtensionMacro`.

drekka opened this issue · 4 comments

Description

I've been writing at attached extension macro to apply some protocols to certain types which looks something like this:

public protocol PX {}

/// THis defines the macro as it will be seen by the source code.
@attached(extension, conformances: PX, Hashable, Identifiable, names: named(hash(into:)))
public macro pathElement() = #externalMacro(module: "SwiftUIExtensionsMacros", type: "PathElementMacro")

With an implementation of:

public enum PathElementMacro: ExtensionMacro {

    public static func expansion(
      of node: AttributeSyntax,
      attachedTo declaration: some DeclGroupSyntax,
      providingExtensionsOf type: some TypeSyntaxProtocol,
      conformingTo protocols: [TypeSyntax],
      in context: some MacroExpansionContext
    ) throws -> [ExtensionDeclSyntax] {
        let count:Int = protocols.count
        let equatableExtension = try ExtensionDeclSyntax("extension \(type.trimmed): Hashable { let x = \(raw: count) }")
            return [equatableExtension]
    }
}

Obviously this is not a finished macro as I''m still working out what it will do. However it does show the number of protocols passed which I eventually intend to use in generating extensions.

The bug is that when I debug this macro using this unit test code:

assertMacroExpansion(
                """
                @pathElement
                struct X: Hashable {}
                """,
                expandedSource: """
                struct X: Hashable {}

                extension X: Identifiable {
                   let id = UUID()
                }
                """,
                macros: [
                    "pathElement": PathElementMacro.self,
                ]
            )

and breakpoint inside the macro, the protocols array argument is always zero length. However when I run this macro on a live source code file, I see let x = 3 in the generated extension indicating that the 3 protocols were passed as expected.

Any I correct? Is this a missed bug?

Or have I missed something in my test code?

Steps to Reproduce

No response

Synced to Apple’s issue tracker as rdar://131441049

Hey there!

Thanks for bringing this up.

There's a bit of a mismatch between what your test is doing and how the macro actually behaves in real code–since the unit tests expansion is based on a totally different expansion system than source code (compiler) expansion.

If you want to cover conformances in your unit tests, you have to provide a list of conformances to the assertMacroExpansion function. The function has an argument called macroSpecs that you need to fill up if you want to receive conformances in your macro implementation from unit tests.

Here's the documentation for this argument:

/// - macroSpecs: The macros that should be expanded, provided as a dictionary
/// mapping macro names (e.g., `"CodableMacro"`) to specification with macro type
/// (e.g., `CodableMacro.self`) and a list of conformances macro provides
/// (e.g., `["Decodable", "Encodable"]`).

Basically, there's this MacroSpec struct with a conformances property that you need to fill in for your tests. It's a new thing that came with swift-syntax version 6.0, which is part of the upcoming Swift 6.0 release. If you're really keen, you can check it out on the main branch right now.

Thanks @Matejkob However I'm on a client's project where we have to support iOS16 and Swift 5. Is there any way to address this without Swift 6?

Ok, I updated to use the main branch and now I can interrogate the protocols.