I'm trying to add a case to an enum using macros along with an init to default it to the added case. For example, say my enum is:
@AddEnumCase
enum Employee: String {
case manager
}
then the expanded version should give:
enum Employee: String {
case manager
case developer
init(rawValue: String) {
switch rawValue {
case Self.manager.rawValue:
self = .manager
default:
self = .developer
}
Here is my code so far. It builds successfully when nothing is typed on main.swift and the tests also succeeds. But when the macro is used in main.swift, it fails to build with:
Command SwiftCompile failed with a nonzero exit code
public struct AddEnumCase: MemberMacro {
public static func expansion(of node: ....) throws -> [SwiftSyntax.DeclSyntax] {
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { return [] }
guard let inheritanceType = enumDecl.inheritanceClause?.inheritedTypes.first?.type else { return [] }
let cases = enumDecl.memberBlock.members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self)?.elements) }
let newElement = try EnumCaseDeclSyntax("case developer")
let initializer = try InitializerDeclSyntax("init(rawValue: \(inheritanceType.trimmed))") {
try SwitchExprSyntax("switch rawValue") {
for name in cases {
SwitchCaseSyntax("""
case Self.\(raw: name).rawValue:
self = .\(raw: name)
""")
}
SwitchCaseSyntax("""
default:
self = .developer
""")
}
}
return [DeclSyntax(newElement), DeclSyntax(initializer)]
}
}
@attached(member, names: arbitrary)
public macro AddEnumCase() = #externalMacro(module: "MyMacros", type: "AddEnumCase")
The following code fails on main.swift
@AddEnumCase
enum Employee: String {
case Manager
}
Edit: after making a minimal example to report, I found out that it can add a case but crashes when conforming to a raw value. I went ahead and reported the bug:
thank you @Pippin, wasted couple of days on this one. Quick question - went through the OptionSet example - how do i add the case using the static var approach?
// may want to change to accepting an argument for the additional case name
@attached(member, names: arbitrary)
public macro AddEnumCase() = #externalMacro(module: "MyProjectMacrosMacros", type: "AddEnumCaseMacro")
public struct AddEnumCaseMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let declarationName = declaration.as(StructDeclSyntax.self)?.name.text else { throw MacroError.message("Not a struct declaration") }
// can perform more guards here:
// - private scope
// - expected enum name ("Values")
guard let enumDeclaration = declaration.memberBlock.members.compactMap({ $0.decl.as(EnumDeclSyntax.self) }).first else { throw MacroError.message("No enum declaration") }
let cases = enumDeclaration.memberBlock.members.compactMap({ $0.decl.as(EnumCaseDeclSyntax.self) })
guard !cases.isEmpty else { return [] }
let caseNames = cases.flatMap { $0.elements }
.map { $0.name.text }
let prefix: DeclSyntax = """
typealias RawValue = String
var rawValue: RawValue
private init(rawValue: RawValue) { self.rawValue = rawValue }
"""
var rawValues: String = ""
for name in caseNames {
rawValues.append("static let \(name) = \(declarationName)(rawValue: \"\(name)\")\n")
}
// adding additional value
rawValues.append("static let developer = \(declarationName)(rawValue: \"developer\")")
let values = DeclSyntax(stringLiteral: rawValues)
return [prefix, values]
}
}
// usage
@AddEnumCase
struct Foo {
enum Values {
case swift
}
}
print(Foo.developer.rawValue) // developer
Again, this is only a starter. You may also desire to add additional protocol conformances and in order to add additional fields it will require a bit of work since this is so tightly integrated.
I could see it was crashing because of the automatic rawValue conformance in the stack trace but I admit I didn't think much further to handle it myself. That looks like a great implementation so if it suits your needs, great!