Swift Macro - How do I add a new case to an enum?

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
}

I don't believe it is currently possible to add an additional case to an enum, similar bug reported:

You may instead try to mimic the OptionSet example from the book to generate your static values instead, adding your additional case.

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:

1 Like

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?

I had some time, so here's a starter:

// 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.

2 Likes

thank you @Pippin, this is super helpful. I did happen to ask the same question here before posting in the forums here: swift - How do I add a new case to an enum? - Stack Overflow, what are your thoughts?

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!