If you add an enum case inside a macro, the compiler will allow non-exhaustive switch statements to compile. This can lead to runtime crashes if the switch statement executes on the newly added enum case.
Unintuitively, if you reference the added case in the same file as the switch statement, the compiler will correctly require the switch to be exhaustive.
You can test this in a sample repo I put together here, or walk through the following code blocks.
Macro
public struct EnumAddCaseMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard declaration.is(EnumDeclSyntax.self) else {
throw CustomError.message("Can only be attached to an enum")
}
return ["case addedCase"]
}
}
Crashing (but compiling) Code (file 1)
@AddCase
enum TestEnum {
case one
}
let myEnum = getEnum()
switch myEnum {
case .one:
print("test")
}
Helper Code (file 2)
func getEnum() -> TestEnum {
return .addedCase
}
This bug inhibits adding enum cases via macros, since Xcode won’t autocomplete your switch statement with the macro-added cases so you can easily miss adding them and lead to a runtime crash in a feature that is supposed to be incredibly safe.
Hmm… was this ever an "officially" supported feature of macros? Or is this potentially one of those things that looking back should have been explicitly forbidden but we did not have the extra code in place to guard against that from happening?
I think it is supposed to be supported because of this test that validates adding a member to enum.
This test only validates adding a function to an enum, but it has a comment or something like: case unknown. I think that means that adding an enum case (in this case case unknown) is also supposed to be supported, although it’s unfortunate that this test itself doesn’t validate it.
Yeah, there do seem to be a number of sharp edges with this approach right now.
I’ve considered OptionSet, but it doesn’t feel like a great semantic replacement here. It makes exhaustive switching harder, and it also allows combinations like [.read, .write], which aren’t valid states for what I’m modeling. One of the main reasons I’m reaching for an enum is to preserve those guarantees.