Allow `@unknown default` on all enum switching to enforce default case handling

Swift’s switch statement handling is “unsafe” (in a introduces-product-bugs way, not in a unsafe program execution way) due to allowing default cases in enum switches. This suppresses any diagnostics when a new case is introduced and goes unhandled in the code, which could lead to bugs in the business logic:

enum ScreenType {
    case phone
    case tablet
    case watch // newly added
}

func takes(screen: ScreenType) {
    switch color {
    case .phone:
        // handle phone logic
    case tablet:
        // handle tablet logic
    default:
        // failed to update switch to handle watch logic :(
    }
}

Clang/Objc has compiler flags to prevent this scenario, but Swift does not, except by using @unknown default which does require new cases to be handled:

enum ScreenType {
    case phone
    case tablet
    case watch // newly added
}

func takes(screen: ScreenType) {
    // WARNING: Switch must be exhaustive
    switch color {
    case .phone:
        // handle phone logic
    case tablet:
        // handle tablet logic
    @unknown default:
        
    }
}

This is a warning, not an error. But many/most codebases in Swift promote warnings to errors.

Aside from the warning, the issue is that there’s no real way to enforce @uknown default across a codebase instead of default, because you can’t currently apply it to non-enum switches like:

    // Error: Switch must be exhaustive: 
    // Remove '@unknown' to handle remaining values
    switch str {
    case "hello":
        print("world")
    case "foo":
        print("bar")
    @unknown default:
        print("default")
    }

Current swift-syntax linters also do not offer a solution, as the type of the switch isn't necessarily specified in explicit annotations. This should also ideally be fixed at the compiler level.

Perhaps there is something I'm overlooking, and I'm not sold on @unknown default being the solution, but it seems like the simplest way to be able to enforce truly exhaustive switches.

1 Like

This was recently accepted as SE-0487.

I didn't see it mentioned in the proposal, and it doesn't look like it's available in 6.2 to try out. So with the implementation of SE-0487 you can apply @unknown default in all places where default currently must go (e.g. string switching)?

EDIT - despite SE-0487 showing as Accepted and not in 6.2, I did see a Nonfrozen Enum Exhaustivity flag to enable in Xcode. Unfortunately, this flag does not allow using @unknown default on strings either.

In a String switch you'll be forced to have a default or case let other anyway, since it's otherwise impossible to match all strings. The only place @unknown default is meaningful over exhaustiveness checking is an enum you might want to extend in future, which the proposal covers; if you want to be hardcore about enforcing it, you can lint against enums without the @nonexhaustive annotation, which will effectively require @unknown default or default in every switch.

2 Likes

Since enums can also come from Objc, linting on @nonexhaustive everywhere isn't a solution.

Maybe I'll just patch the compiler to allow @uknown default on strings/int switching.

What would @unknown default even mean for a string? The set of all valid strings is already practically infinite (roughly 256^(2^48) on 64-bit machines, a number big enough that it can't even be stored in a Double), so you'll be forced to include a non-@unknown default anyways for exhaustivity; and I don't see any way it could be extended in the future -- unless you want to guard against e.g. strings that contain characters from future versions of Unicode?

For infinite sets like strings/int, I don't think unknown default means much different than default; the former could subsume the latter if used there.

My proposal is to allow it simply so that we can write easy linters to never use a bare default: in a switch case, which fixes the "unsafe" default usage as mentioned in the original post.

You can already do that, no need for using @unknown at all, just ban using default. You could probably even write a regex to do it. But it's unclear what you're even proposing; using @unknown default outside nonfrozen and nonexhaustive enums seems just like default. It seems like you just want a blanket language flag that turns off the ability to use default, which isn't going to happen. So I suggest you get your linter configured and go from there.

1 Like

You can't ban using default. It is required to switch on ints/strings. I hope this whole conundrum is clear now.

Not really, since you haven't explained what you're actually looking for here. I guess a way to mark an enum such that it should never support default? The opposite of the new @nonexhaustive, where instead of allowing @unknown default, thereby always requiring default handling, it would be @noDefault, where default can't be used. Possible, I guess. A SourceKit-based analysis could still be used to ban default as part of a linter, it's just harder and slower.

The only linters I'm familiar with use SwiftSyntax, which doesn't necessarily have the type info to know if you are switching on a int/string (where default is required), or an enum.

So back to the beginning, @uknown default already has the nice feature of requiring all cases to be explicitly handled. Seems like the simplest solution is to allow it to be applied to string/int switching as well, so we can write a very simple plain default ban across an entire app.

I think perhaps we are having trouble understanding your pitch, as it is not possible to write out "all cases" of a string without default.

1 Like

Yeah, I can see that there is a lot of confusion. I hoped the examples in the original post were very clear what the issue is, but I guess not.

I'm not really sure how else to say it though: there is no mechanism in Swift to enforce exhaustive switch handling across all types of switches (including those from Objc), nor is there a reliable way to write a linter to do so today.

This is exactly my pitch. Change the language so that you can (optionally) write out all cases of a string by specifying @unknown default instead of default.

Why?

Because then you can write a guaranteed linter that anytime someone tries to write default: on a switch, they must instead use @unknown default:.

Why does this matter?

Because unlike default, @unknown default continues to enforce exhaustive switch handling. And in string and int switches, there is no way to write out all cases so you need some sort of catch-all. This proposal: allow that catch-all to be @uknown default.

Except that you would get absolutely no benefit from having an @unknown default on a switch of String or Int, because there will never be new cases for the compiler to warn you about. In fact, I would go as far as to say that @unkown is completely meaningless when applied to the default case for those kinds of types.

Can you explain what you mean by changing the language so that you can write out "all cases of a string"? There are effectively infinitely many cases.

1 Like

I understand that. As I've stated many times now in this thread, the point is to allow @unknown default (behaving identically to the a bare default on an int/string switch) in order to be able to enforce via a simple linter that one cannot use a bare default anywhere in a codebase. It is not about any benefit to string/int switching itself.

I meant, change the language so that you can use @uknown default on a string, with the same behavior for unbounded enums as default.

What a frustrating thread! :sweat_smile:

Here's a request in SwiftLint for a force_exhaustive_switch rule: No defaults in enum switch · Issue #131 · realm/SwiftLint · GitHub.

Again, this rule cannot be written in a linter in today's Swift, swift-syntax isn't powerful enough. There needs to be a language change to make it possible, and I proposed piggybacking off of @unknown default's current behavior to make that linter possible.

Surely some of you have wanted to guarantee new cases are handled in all scenarios?

1 Like

Sorry. I’m afraid that I at least am not understanding any better. You only use @unknown default instead of default when all known cases have been handled already. That is not possible to do with a string value, since it is always possible to know of some other string value at compile time.

I suppose it is technically possible to have string values that can only be expressed using future versions of Unicode, and perhaps one would want to handle that differently and want to do that via @unknown default, but I don’t think you’re talking about that? And in any case, you would still need a vanilla default case to handle exhaustively the values that are knowable. I don’t really understand what it would mean not to allow that, other than to disallow switching over strings entirely?

2 Likes

I think what @8675309 is requesting is to allow @uknown default in non-exhaustive cases anyway (with the exact same behavior as default), so that it's possible to enable a linter rule across a codebase to ban non-@unknown defaults, so that switch statements which can use @unknown default be required to use them.

(That being said — I would personally already enable such a rule on my team's codebase with today's default semantics and require that String/Int constants be converted into exhaustive enum cases, or simply disable the linter rule locally if really necessary.)

2 Likes

Boy I’m just not getting it today. If known defaults can be handled by unknown default implementations, aren’t we just changing the spelling of default to @unknown default?

1 Like