JaSHin
JaSHin

Reputation: 277

Add cases from another enum using Swift macro

I want to create a macro which copies cases from one enum to another. I would imagine something like this:

enum ChildEnum {
    case childA
    case childB
}


@CaseProxy(ChildEnum.self)
enum ParentEnum {
    case parent
}

which will generate:

enum ParentEnum {
    case parent

    ///Generated
    case childA
    case childB
}

I tried to create the macro, but I don't know how to use node: AttributeSyntax to get to the members of the given type that I pass as an argument.

Upvotes: 2

Views: 207

Answers (1)

Sweeper
Sweeper

Reputation: 270708

With this design, you cannot. Macros are not full-fledged source generators. They operate on the AST, not the semantics of the code. As far as the macro is concerned, ChildEnum.self is just the word ChildEnum, a period, and the word self. It cannot know what cases ChildEnum has, unless you pass all the cases' names into the macro.

And if you do pass all the cases into @CaseProxy, that kind of defeats the purpose of having a macro. You might as well just write them inside the enum declaration normally.

An alternative design would be to have an additional declaration macro take in a () -> Void closure. Inside the closure, you can declare enums:

#ProxyableEnums {

    enum ChildEnum {
        case childA
        case childB
    }
    
    @CaseProxy(ChildEnum.self)
    enum ParentEnum {
        case parent
    }

}

Now both enums are "inputs" to #ProxyableEnums, and so #ProxyableEnums can read the contents of the closure, and see what cases each enum has. Then, it can find enums that are annotated with @CaseProxy. Using this information, #ProxyableEnums will expand to one or more enum declarations - for every enum without @CaseProxy, it will expand to that same enum declaration, and for every enum with @CaseProxy, it will expand to an enum declaration with the added cases. Notably, @CaseProxy doesn't expand to anything - it's just a "marker" for #ProxyableEnums.

The limitation here is that #ProxyableEnums cannot be used at the top level, because it is a macro that can create arbitrary names. Also, the child and parent enums must be in the same file.

Here is a very simple implementation:

enum CaseProxy: MemberMacro {
    static func expansion(
        of node: AttributeSyntax, 
        providingMembersOf declaration: some DeclGroupSyntax, 
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        []
    }
}

enum ProxyableEnums: DeclarationMacro {
    static func getEnumDeclarations(_ closureBody: CodeBlockItemListSyntax) -> [EnumDeclSyntax] {
        closureBody.compactMap { $0.item.as(EnumDeclSyntax.self) }
    }
    
    static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        let enumDecls = getEnumDeclarations(
            node.trailingClosure!.statements
        )
        
        var map = [String: EnumDeclSyntax]()
        for decl in enumDecls {
            map[decl.name.text] = decl
        }
        for decl in enumDecls {
            if let caseProxy = decl.attributes.findAttribute("CaseProxy"), 
                case let .argumentList(args) = caseProxy.arguments,
                let type = args.first?.expression.as(MemberAccessExprSyntax.self)?
                    .base?.as(DeclReferenceExprSyntax.self)?.baseName.text,
               let proxiedCases = map[type]?.memberBlock.members
                    .compactMap({ $0.decl.as(EnumCaseDeclSyntax.self) }) {
                map[decl.name.text]?.memberBlock.members
                    .append(contentsOf: proxiedCases.map { MemberBlockItemSyntax(decl: $0) })
            }
        }
        
        return map.values.map(DeclSyntax.init)
    }
}

extension AttributeListSyntax {
    func findAttribute(_ name: String) -> AttributeSyntax? {
        for elem in self {
            if case let .attribute(attr) = elem, 
                attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text == name {
                return attr
            }
        }
        return nil
    }
}

// ...

@freestanding(declaration, names: arbitrary)
public macro ProxyableEnums(_ f: () -> Void) = #externalMacro(module: "...", type: "ProxyableEnums")

@attached(member)
public macro CaseProxy<T>(_ type: T.Type) = #externalMacro(module: "...", type: "CaseProxy")

Upvotes: 0

Related Questions