-
Notifications
You must be signed in to change notification settings - Fork 10.6k
[StrictExhaustiveSwitches] Replace default switch case with known enumerated cases #85168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
@swift-ci Please test |
|
Thanks for implementing my feature request! I see this in your test case I'm wondering whether we could also warn against using For example, ObjC could pass an int 3 to a Swift function that takes an enum with 2 defined cases (with raw values 0 and 1). If the |
|
@an0 I specifically didn't tackle the @unkonwn default case in this PR because of the feedback I got on this other forums post that discussed rejecting code based on the presence or lack of resiliency. As this change merely adds warnings, I don't think it would be unreasonable to add a second that warns if:
The last case is likely the most tricky because of the vulnerability you described above, as NS_ENUM - declared values are merely typedef for a number and are as such unbound. That said, because the (missing) unknown default warning becomes an error in Swift 6 mode, this becomes less about adding an opt-in warning than it does diverging language features, the pain of which has been discussed. That said there's another source change available: If the enum is typedef with the macro NS_CLOSED_ENUM instead, the swift compiler will not warn on unexpected cases but as you pointed out, it's still possible to call an but that is also outside the scope of this PR That said, I'll leave the unknown default case outside the scope of this PR and follow up with another where we can discuss the impact of resiliency as that opens up the exhaustiveness of the enum and with it another can of worms. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a solid change, thank you for submitting.
| int diagnosedCases = 0; | ||
|
|
||
| processUncoveredSpaces([&](const Space &space, | ||
| bool onlyOneUncoveredSpace) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
param was unused, removed here
|
@xwu do you mind reviewing from https://forums.swift.org/t/allow-unknown-default-on-all-enum-switching-to-enforce-default-case-handling/82469/16? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lovely, in my view.
| "handle unknown values using \"@unknown default\"", ()) | ||
|
|
||
| GROUPED_WARNING(decompose_default_case, StrictExhaustiveSwitches,none, | ||
| "switch has known exhaustive cases, remove default case", ()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd fiddle with the wording: switches are exhaustive, not cases. Perhaps something like, "switch can be made exhaustive without use of 'default'; replace with known cases".
include/swift/Basic/Features.def
Outdated
| EXPERIMENTAL_FEATURE(ImplicitLastExprResults, false) | ||
|
|
||
| /// Decompose `default` case in a `switch` statement into missing | ||
| /// known iteratable `case` statements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "iterable"
| case 2...10: return true | ||
| } | ||
|
|
||
| // Unkonwn default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // Unkonwn default | |
| // Unknown default |
The one @unknown default case I'd like to see handled right off the bat (IMO), perhaps even without the new compiler option, is unambiguously unnecessary use of non-@unknown default when an @unknown default will do. That is, if all known cases of a resilient enum are already handled, it should be at least a warning to unwittingly defeat future diagnostics by writing plain default. (This may already be diagnosed--I'm not sure. I'd imagine it was not possible to have these diagnostics in place upon first introduction for source compatibility reasons but at least in Swift 6 mode that shouldn't be a problem anymore.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah good catch. I'll check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the new experimental feature? The warning group should give everyone control about the new warning, doesn't it? If you want to default it to be a warning, and people are using -warnings-as-errors, they can demote it back to warning with -Wwarning StrictExhaustiveSwitches.
|
Hmm, I would disagree with that. An opt-in mode for an explicit language subset (like opt-in strict memory safety, or embeeded mode) has its uses for the reasons outlined on the forums, but I don't see how using a feature exactly as designed can be made something warned about by default. It should remain now and forever the case that using |
|
My problem is with the concept of "feature". This is not a feature of the language, just a new warning. If you want the warning disabled by default, so people need to opt in with |
|
@drodriguez getting a little semantic but I'm not married to using an experimental feature. I do think it would be better to just use DefaultIgnore and enable with -Wwarning but I couldn't find how to check if the diagnostic was enabled, which you need to check to do the exhaustiveness checking. If the user doesn't want these warnings, and I expect if they are DefaultIgnore to be the majority of cases, we probably shouldn't do the exhaustiveness checking here but I needed some way to "turn on" the checks. If we use an experimental feature AND DefaultIgnore, then you get into a weird case where the diagnostics are weird. So either setting the diagnostic level should enable the feature (by whatever mechanism) or vice-versa. There was a note in Features.def about a feature enabling other diagnostics but I couldn't find where it was actually applied (looks out of date). @xwu any suggestions? |
|
@AdamCmiel Additionally, it seems that if your warning is |
Implementation details about the diagnostics engine are less my jam; as long as banning |
f21b7e9 to
1fd4444
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor feedback, but the bunch of the implementation looks good to me.
|
|
||
| GROUPED_WARNING(unsafe_superclass,StrictMemorySafety,none, | ||
| "%kindbase0 has superclass involving unsafe type %1", | ||
| "%kindbase0 has superclass involving unsafe type %1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: you might want to remove the whitespace changes to make the commit more clear.
| } | ||
|
|
||
| static bool hasStrictExhaustiveChecksEnabled(ASTContext &Context) { | ||
| return !(Context.Diags.isIgnoredDiagnostic(diag::decompose_default_case.ID)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: If you run clang-format does it add the parenthesis? They are not really necessary.
| } else if (!defaultCase) { | ||
| diagnoseMissingCases(Switch->getCases().empty() | ||
| ? RequiresDefault::EmptySwitchBody | ||
| : RequiresDefault::UncoveredSwitch, | ||
| uncovered, unknownCase); | ||
| uncovered, unknownCase, defaultCase); | ||
| } | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe the comment in 1082-1084 should change to say what happens when the exhaustive switch is requested. It looks like the comment is saying that the code cannot reach here with a default: case, but now it can, and it seems that it is simply accepted (which was the behaviour before, IIUC).
| std::optional<decltype(diag::non_exhaustive_switch)> mainDiagType = | ||
| diag::non_exhaustive_switch; | ||
|
|
||
| bool downgrade = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: you might want to leave it where it was, close to where it is used.
| // Switch actually is exhaustive with known cases, | ||
| // so don't emit exhaustive diagnostic | ||
| mainDiagType = std::nullopt; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mainDiagType.reset();| // RUN: %target-swift-frontend %t/Enums.swift %t/NewDiagnostics.swift %t/PreventRegressions.swift %t/WithError.swift -swift-version 6 -Wwarning StrictExhaustiveSwitches -typecheck -verify | ||
| // RUN: %target-swift-emit-silgen %t/Enums.swift %t/NewDiagnostics.swift %t/PreventRegressions.swift %t/SILVerify.swift -swift-version 6 -Wwarning StrictExhaustiveSwitches -o /dev/null -verify | ||
|
|
||
| // REQUIRES: swift_feature_StrictExhaustiveSwitches |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not longer a feature?
|
@swift-ci please smoke test |
1fd4444 to
61d6823
Compare
Implementing the default behavior from this forum thread, similar to -Wswitch-enum
Optionally (with -enable-experimental-feature StrictExhaustiveSwitches) replace a default case label with the known set of exhaustive cases if the compiler is able to determine that set of cases. This works great for the simple case of an enum, with @unknown default able to still handle the resilient (or NS_ENUM) case.