Anything similar to -Wswitch-enum for Swift?

Right, I think with any linter-based version of this you'll inevitably need to opt with a linter exception unless you're willing to give up switching on Strings, Ints, and other non-exhaustive values entirely. Joe's proposal above works for the simple case of enum-like Strings but doesn't handle the full universe of use cases for switch (range matching, matching against in-scope bindings, custom ~= operators, switch over user-defined structs, etc.). But also as John says, having to use a linter 'disable' command for these cases does not seem like the biggest downside to me.

Just did an audit on our large codebase, a surprising amount of switches were for non-exhaustive types, ranges, and other non-enum matching, like ~35% of switches. This is just one sample, but I think that number is far too high to rely on a linter and disabling a linter.

I think we're at the point where this is the best option. Does this require an evolution proposal?

1 Like

No, compiler warnings just require normal code review.

2 Likes

I think the blocker here is that @unknown default for non-exhaustive values is an error, so if the goal is to change that then we'd need a proposal. From what I can tell (based on what Joe mentioned up-thread) it's already the case that @unknown default for non-resilient enums achieves the desired "warn if known cases not explicitly handled" request.

EDIT: oh, your previous message said "non-@unknown".

I believe the proposed warning is for ever using non-@unknown default in a switch that could be exhaustive (other than unknown cases).

1 Like

Yeah, got tripped up by the double-negative :slight_smile:

Do we already have warnings (or warning groups) today that are opt-in-only? Even though it's 'just' a warning I don't think we could justify enabling such a warning by default.

We have some decent infrastructure now in the compiler for opt-in warnings. The proposed warning could be implemented as ignored (suppressed) by default and interested users could opt-in with an explicit flag, e.g. -Wwarning StrictExhaustiveSwitches.

5 Likes

I'd really rather see the tooling improved to make this viable as a lint warning, rather than as a language warning, since it's really a stylistic issue. (default is not unsafe in any way, especially in the way Swift thinks about safety, so it's perfectly valid most of the time. If you feel it is, we may as well add warnings about not handling all possibilities in if constructs, as that would catch the same types of bugs.)

1 Like

Technically, you’d also have to apply that to the ternary operator as well, but I think we can easily justify applying this warning to only switches. In Swift, switches are known for enforcing case coverage. Using default is an escape-hatch, to be used when one either really doesn’t care about other cases, or when one knows that all other possible cases should be handled the same way. (e.g. with a fatalError()) It seems to me that a warning about using default where it’s not necessary is in line with how many people want switching over enums to behave. I don’t think we can say the same about if statements in general, even if they involve a case expression.

Compiler already warns when it's not necessary (when you already handle every other case). This is more about banning it in most cases, which I'd say is a stylistic rather than correctness choice.

And hence the request for an opt-in warning. Until the compiler is able to provide type information to external tools, only the compiler has enough information to even attempt this.

1 Like

And hence my response that effort would be better spent on tooling to enable such an integration, rather than following C/C++ into the warning jungle (which Swift assiduously avoided until recently). Though I think third-party tools could use the SourceKitten library, or something similar, that calls into the build tools to extract such info, it's just redundant when the compiler already produces that work.

My initial reaction to that was "wow", however in practice I don't think I'll use it:

  • it feels like a hack
  • it is longer
  • the app will perform two switches instead of one under the hood - one to get from rawValue to an enum, another the actual switch by enum.
  • you have to repeat those names twice, and sometimes three times:
enum KnownKeyword: String {
    case someCaseName = "-someCaseName" // 1, 2
}
...
case .someCaseName: ... // 3
  • imagine the same trick for the use case of switching by integers between 1 and 100, you'd need to declare an enum case n1, n2, n3, ... n100 - would be silly.

This trick would only be interesting if you have many places you need to switch over the same set of possibilities, and that set of possibilities is subject to somewhat frequent change, so you get benefit from the compiler helping you work through all the places you need to add handling. A little extra bit of repetition may be worth the extra compiler help.

The first switch will use a specialized string-matching function to find the associated raw value. The second switch is likely to be a jump table. This is probably better than the naive case-by-case string comparison we would do otherwise (though, granted, it would be nice to recognize and optimise a series of string equality comparisons and optimize them into a branch.)

1 Like

Although I find @Joe_Groff’s example instructive, having a compiler option as proposed by the OP would be nevertheless really useful in helping to find all the places where a default is used. :slight_smile:

Seems like you could do that simply by searching for \ndefault:, where you replace the newline with whatever your search syntax supports. (In Xcode I just copy paste one, since it doesn't like the literal.) Now, building the proper tooling where I can query a codebase for things like switches with defaults, that would be great, but we don't need compiler diagnostics to do that.

Whether this is in an external tooling or the compiler itself – I'd appreciate having a "fix-it" that would suggest changing code from using default to a list of individual missing cases.

4 Likes

Speaking of linter rules, is there a linter that would suggest changing this code into a switch?

is expression == .a {
    ...
} else if .b == /*same*/expression {
    ...
} else {
    ...
}
switch expression {
    case .a ...
    case .b ...
    default: ... // †
}

Ditto for a ternary, which could be changed into a switch expression:

expression == .a : oneOutcome : anotherOutcome
switch expression { case .a: oneOutcome; default: anotherOutcome } // †

† along with the subsequent warning we are talking about

Thank you for having had this discussion. Something that strikes me is that it does seem really valuable to offer an option to have “Switch must be exhaustive” be an error instead of a warning when switching on a non-frozen C enum w/ @unknown default in place, in order to catch when a new value is added to the enum. For vast codebases, having the update of the enum declaration to introduce a new value triggering a build breaking error is the only way to ensure code gets updated – the warning is just not sufficient.

I see SWIFT_UPCOMING_FEATURE_NONFROZEN_ENUM_EXHAUSTIVITY to ensure @unknown default is present for non-frozen enums, but is there something similar for case exhaustivity? Like SWIFT_UPCOMING_FEATURE_NONFROZEN_C_ENUM_CASE_EXHAUSTIVITY?

If not, what would be next for such an addition? Does it require a full blown proposal?

Thanks for the discussion!

Update: Nevermind, treating warnings as errors is sufficient. Thanks!