Skip to content

Conversation

@AdamCmiel
Copy link

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.

@AdamCmiel AdamCmiel changed the title Handle switch default [StrictExhaustiveSwitches] Replace default switch case with known enumerated cases Oct 28, 2025
@AdamCmiel
Copy link
Author

@swift-ci Please test

@an0
Copy link
Contributor

an0 commented Oct 28, 2025

Thanks for implementing my feature request!

I see this in your test case

  // exhaustive enum
  switch ReasonableEnum.zero {
  case .zero, .one: return true
  @unknown default: return false
  }

I'm wondering whether we could also warn against using @unknown default for frozen enums (which is majority), even if the switch is exhaustive, to catch call sites that pass in invalid values with a hard crash.

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 switch has @unknown default the invalid value 3 will fall into @unknown default branch but it is better to remove @unknown default and let the switch crash so that we could catch the bad ObjC call site.

@AdamCmiel
Copy link
Author

AdamCmiel commented Oct 28, 2025

@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:

  • a switch statement is using an unknown default case when:
    • the type of the switch's subject is defined in the same module
    • the type of the switch's subject is defined in a non-resilient module
    • the type of the switch's subject is defined by a clang decl closed enum in a non-resilient module

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 @objc func from the clang side with a value outside of the enum - perhaps we should warn in clang here instead if that can be statically determined, but yes I do think this should crash for a NS_CLOSED_ENUM on the swift side if it's not possible to typecheck, perhaps with some synthesized version of:

// for a given closed enum decl
typedef NS_CLOSED_ENUM(NSInteger, ObjCEnum) ...
extension ObjCEnum: CaseIterable {}
func takesAnObjCEnum(value: ObjCEnum) {
  // inserted prologue assert that the value is of a known case:
  assert(ObjCEnum.allCases.contains(value))

  // user-defined function body...
}

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.

Copy link

@NSProgrammer NSProgrammer left a 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) {
Copy link
Author

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

@AdamCmiel
Copy link
Author

Copy link
Collaborator

@xwu xwu left a 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", ())
Copy link
Collaborator

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".

EXPERIMENTAL_FEATURE(ImplicitLastExprResults, false)

/// Decompose `default` case in a `switch` statement into missing
/// known iteratable `case` statements.
Copy link
Collaborator

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
Copy link
Collaborator

@xwu xwu Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.)

Copy link
Author

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

Copy link
Contributor

@drodriguez drodriguez left a 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.

@xwu
Copy link
Collaborator

xwu commented Oct 29, 2025

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 default to mean default—that is, to cover all leftover cases without uttering them—is expressly permitted without warning. This should not require toggling anything or even silencing the warning in source.

@drodriguez
Copy link
Contributor

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 -Wwarning or -Werror, why not use the DiagnosticOptions::DefaultIgnore in this diagnostic? This will avoid adding yet another feature that (if I understand correctly your position) will never be possible to remove.

@AdamCmiel
Copy link
Author

@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?

@drodriguez
Copy link
Contributor

drodriguez commented Oct 30, 2025

@AdamCmiel ctx.Diags.isIgnoredDiagnostic(diag::YourNameHere.ID) is what other DefaultIgnore warnings do.

Additionally, it seems that if your warning is DefaultIgnore, even if you emit the diagnostic, the emission doesn't mean that it will be shown to the user. The Diagnostic Manager seems to check for the level of the diagnostic before "printing" it.

@xwu
Copy link
Collaborator

xwu commented Oct 30, 2025

@xwu any suggestions?

Implementation details about the diagnostics engine are less my jam; as long as banning default is opt-in and not opt-out, whether it's considered a feature, language subset, warning toggle, it's all the same to me :)

@AdamCmiel AdamCmiel force-pushed the handle-switch-default branch from f21b7e9 to 1fd4444 Compare October 31, 2025 18:22
Copy link
Contributor

@drodriguez drodriguez left a 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",
Copy link
Contributor

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));
Copy link
Contributor

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.

Comment on lines +1091 to 1097
} else if (!defaultCase) {
diagnoseMissingCases(Switch->getCases().empty()
? RequiresDefault::EmptySwitchBody
: RequiresDefault::UncoveredSwitch,
uncovered, unknownCase);
uncovered, unknownCase, defaultCase);
}
return;
Copy link
Contributor

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;
Copy link
Contributor

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.

Comment on lines +1134 to +1140
// Switch actually is exhaustive with known cases,
// so don't emit exhaustive diagnostic
mainDiagType = std::nullopt;
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not longer a feature?

@drodriguez
Copy link
Contributor

@swift-ci please smoke test

@AdamCmiel AdamCmiel force-pushed the handle-switch-default branch from 1fd4444 to 61d6823 Compare October 31, 2025 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants