How do I add a new case to an enum?

252 views Asked by At

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
}
1

There are 1 answers

1
Sweeper On BEST ANSWER

Looking at the crash log, I can see that something went wrong when generating the rawValue for the RawRepresentable conformance. It could be possible that the switch in the generated rawValue is not exhaustive because it didn't take into account your new case.

Swift macro expansions shouldn't depend on the order of expansion. All the macros are given the original AST and they expand "at the same time". I think this might also be true for how : String gets lowered. You are depending on : String lowering after your macro expands, which apparently is not the case here.

What this basically means is that you also have to generate the rawValue implementation.

Here is an example, only for String raw values:

public struct AddEnumCase: MemberMacro {
    public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [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: \(String.self))") {
            try SwitchExprSyntax("switch rawValue") {
                for caseList in cases {
                    for `case` in caseList {
                        SwitchCaseSyntax("""
                           case Self.\(raw: `case`.name).rawValue:
                             self = .\(raw: `case`.name)
                           """)
                    }
                }
                SwitchCaseSyntax("""
                   default:
                     self = .developer
                   """)
            }
        }
        
        let rawValue = try VariableDeclSyntax("var rawValue: \(String.self)") {
            try SwitchExprSyntax("switch self") {
                for caseList in cases {
                    for `case` in caseList {
                        if let rawValueExpr = `case`.rawValue?.value {
                            SwitchCaseSyntax("""
                               case .\(raw: `case`.name):
                                 return \(rawValueExpr)
                               """)
                        } else {
                            SwitchCaseSyntax("""
                           case .\(raw: `case`.name):
                             return \(literal: `case`.name.text)
                           """)
                        }
                    }
                }
            }
        }
        
        return [DeclSyntax(newElement), DeclSyntax(initializer), DeclSyntax(rawValue)]
    }
}

For numeric raw values, you would need to keep a counter that increments for each case, which is a bit more involved. To 100% replicate what Swift would have generated, you would also need to set the counter to any explicitly declared raw values. This is left as an exercise for the reader. (:D)