In ObjC we can use -Wswitch-enum compiler option to enforce switch to handle individual enum cases exhaustively, avoiding potential bugs of relying on default and forgetting to handle newly added cases in future.
To make it clear this is what I mean:
typedef NS_ENUM(NSInteger, Style) {
StyleA,
StyleB,
StyleC,
};
// This switch triggers "Enumeration value 'StyleC' not explicitly handled in switch" in ObjC
switch (style) {
case StyleA:
…
break;
case StyleB:
…
break;
default:
…
break;
}
// However, this switch doesn't trigger warning in Swift
switch style {
case .a:
…
case .b:
…
default:
…
}
Do we have anything similar in Swift?
I know Swift switch is much more complicated and can handle many other situations beyond enums but it will still be very useful to protect enum switches.
Swift already requires switch statements to be exhaustive. For an enum, this means you must list all the cases or provide a default (or @unknown default).
I think, if I'm understanding the behavior of -Wswitch-enum correctly, that the request here is similar to this topic: that is, the goal is to warn if the default is covering actual known cases. Effectively, if exhaustive switching is possible, then it must be done explicitly, with all known cases listed out.
I think this then runs up against the problem presented in the thread linked above where if somebody wanted to enforce this codebase-wide they would need to ban switching over non-exhaustive values (e.g. Int/String) whether the use of @unknown default is an error rather than a warning.
Maybe this isn't what you're looking for, but it looks like the swift compiler gives them to you in the diagnostic and in code completion if you fill in a new case, like clang does:
enum Foo { case a,b,c }
func foo(x: Foo) {
switch x {
case .a: break
@unknown default: break
}
}
<source>:4:5: warning: switch must be exhaustive
2 |
3 | func foo(x: Foo) {
4 | switch x {
| |- warning: switch must be exhaustive
| |- note: add missing case: '.b'
| |- note: add missing case: '.c'
| `- note: add missing cases
5 | case .a: break
6 | @unknown default: break
Compiler returned: 0
Maybe I didn't make it clear. I see how @unknown default could help but I need to find all the vanilla default (not @unknown default) usages first and replace them with @unknown default so that the compiler can help me catch the missing cases.
If we have -Wswitch-enum like clang, the compiler could help me find the vanilla default (not @unknown default) usages w/o exhaustive case handling so that I can fix them.
On the other hand, if I've already found all the vanilla default usages I'll directly remove the default and update the switches to handle all the cases explicitly and exhaustively, instead of changing them to @unknown default.
Swift's design for enums and switches is fundamentally different from C's. In C, an enum is basically just an integer and its enumerators are basically just constants. There's nothing stopping you from stuffing some random integer into an enum variable that doesn't correspond to any of the enumerators. Because of that, it makes sense for a switch statement to have both a case statement for each declared enumerator and a default statement to cover those random undeclared values.
In Swift, by contrast, an enum can never take on a value other than the ones explicitly declared.* That means that if your switch has an exhaustive set of case statements, there is never a reason to also have a default statement—the compiler can prove it will never be used. So if you want exhaustivity checking, you can just leave off the default statement, because you know it can't happen in the first place. The only reason to ever use default is because you know there are declared cases you haven't covered.
Because of these design features, Swift has no need for something like the warning you're proposing. It basically boils down to just banning default statements, which is more appropriate as a linter rule than a warning.
* Exception: enums declared in certain libraries might take on "undeclared" values because a later version of the library added a case that didn't exist when your code was compiled. You can handle these while preserving exhaustivity checking by using @unknown default, which still requires you to write an explicit case statement for any known cases.
To what Becca said, I'll just add that you don't need to set up a specialized linter to find any non-@unknowndefaults in your current code. Any reasonable code editor will have a way to search for a regular expression in the current project; in Xcode, this is "Find in workspace", and then change Find > Text to Find > Regular Expression. You want to search for ^\s*default:.
Some editors will let you narrow that down to just *.swift files; I'm not sure how to do that in Xcode, unfortunately.
Except that default is required for certain values which can never be exhaustive, like String. So such a rule would also amount to “never switch on a String”. One could argue this is a reasonable price to pay for wanting to ban default in a codebase, but I do see the “missing middle” that people are reaching for.
For finding and fixing the existing issues I could do it manually if there are no better ways, though I really prefer if some tool could help as I have millions of lines of code.
More importantly, I want to prevent such issues in future so I still need either a linter or compiler support.
Yeah, wanting to keep them out persistently is a harder problem, and I could definitely see room for a warning about adding a non-@unknowndefault to a switch that could have been exhaustive. I agree that, in the abstract, it'd be a better match for a linter than the compiler — except that it'd need type information, which it's not easy to recover in a source-based Swift tool right now.
I do think that people undervalue low-tech solutions for things like this, though. That "linter" could easily just be a script that searches for ^\s*default:\s*$ in your Swift files, the idea being that if you've got a good reason to use default:, you should either add @unknown or a comment on the same line.
I want to point out that because of Objc/C interop, Swift may be switching on an enum with random int values, and in that case we do want @unknown default (though not default).
Swift has no need for something like the warning you're proposing. It basically boils down to just banning default statements, which is more appropriate as a linter rule than a warning.
[/quote]
I think Swift makes lots of design decisions to avoid footguns, even if there isn't strictly a program-execution safety need. And default is a potential footgun for product bugs.
In a world where @unknown default came first and threw a warning, would we really design a default and avoid exhaustiveness? I've worked in many large companies and every one of them has enabled exhaustive switching in Clang for product correctness reasons, not enum-out-of-bounds reasons.
Swift also admits case _ for use as a potential “no really I know what I’m doing” opt-out. It’s not quite as ‘scary’ as @unknown but likely not what people are reaching for when they reflexively write default.
FWIW, i don't think adding some level of support for 'prefer @unknown default' behavior to either of the popular linting tools in the ecosystem would be terribly difficult – e.g. i got a basic draft implementation doing something along those lines set up in SwiftLint after a few minutes.
i agree that this does seem more like a linter concern than a diagnostic that the compiler should produce[1], but only having access to syntax in most external tools means if you want to try and prevent someone's forgetting to handle new future cases of non-frozen enums, there's some pressure to just apply a blanket rule to 'use only @unknown default', regardless of the thing you're switching over. that seems like it might work okay since you'd be incentivized to:
replace the default with explicit case handling for known frozen and known unfrozen enum cases
annotate it with the @unknown attribute to handle future nonfrozen enum cases
add a linter exception with a documented rationale (maybe just shifts the problem somewhat, but is maybe easier to identify or ban entirely in a codebase)
it does seem a bit awkward to have an @unknown default case when switching on a frozen enum, though it seems like it works just fine.
+1 to there being potential for product bugs here. i recently fixed an issued caused by exactly this problem – a default case was applied to a non-frozen enum from a system framework, which masked an issue when a new case was introduced and wasn't appropriately handled.
certainly without some form of explicit 'opt-in', but i'm not sure adding/removing arbitrary diagnostics like you can do in clang is something swift wants to replicate. ↩︎