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.
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 obvious limitation here is that 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")