A language empowering everyone to build reliable and efficient software.
#[non_exhaustive]
has been around for over 5 years, allowing crates to add enum variants without it being a breaking change. There even is a allow-by-default clippy lint for enums that are not marked non_exhaustive
. But there is still no stable way to get a warning or error when missing a variant that already exists.
The closest is non_exhaustive_omitted_patterns
(Tracking issue), which is not yet stabilized (no comments on the tracking issue for almost a year). But I think the approach this is going for is not the right one either. When thinking about whether your code is complete and you matched all (existing) variant combinations you effectively care about whether the wildcard/catch all arm is ever executed (while this only checks if all variants have been mentioned). The reason for this is apparently related to performance concerns, but Rust already does this analysis for exhaustive enums, so I don't see this as an issue if done in the right place:
In order for this lint to work properly with nested patterns, we would need to change the exhaustiveness algorithm to explore a lot more cases, which isn't feasible performance-wise.
Doing it this way may be useful, but in my opinion it doesn't achieve the goal of "I want to be sure this doesn't miss anything unless the dependency updates and adds a new variant".
Another one was #[warn(reachable)]
or #[deny(reachable)]
(here, here, here), which actually does what you want. But it seems to have been replaced by non_exhaustive_omitted_patterns
.
I'm not an expert on compiler internals, so implementing the following may not be easy, but the compiler already has to do complete exhaustiveness checking, so I don't fully understand why there couldn't be an "if this branch ever ends up in the binary output a warning". The most naive way to do this could be to remove the catch-all and see if the code still compiles. If it doesn't: Print a warning and add the catch-all back in. That way you have the ability to add variants and warnings when you don't have one (unless you opt-into it being a compile error with #[deny(reachable)]
.
We even have the opposite #[warn(unreachable_patterns)]
lint, but it doesn't work on non_exhaustive
enums (since future versions can make this relevant of course). So a system for checking if all variants are matched already exists somewhere. There just isn't the logic/warning for handling missing arms of non_exhaustive
enums.
My opinion
- This isn't something that should be left open for over 5 years, with people talking about it even longer ago
- I don't think the current
non_exhaustive_omitted_patterns
is the way to go, it only solves parts of the problem. - When compiling a crate you know the exact version of your dependency (rustc also knows this, not just cargo). You know which variants currently exist → Handling
non_exhaustive
enums like normal enums and only add/enable the wildcard/catch-all branch if needed [1] (and printing a warning) should work. Updating the dependency still wouldn't be a breaking change (unless you opt-in to having this be treated as an error).- This could even be extended to a general
compile_warn!("message")
macro, similar tocompile_error!("message")
instead of an attribute/lint. Both currently cannot be used for this, see example below.
- This could even be extended to a general
- I'm not sure if the warning could be a enabled by default or if you'd have to opt-in in some way.
Some ways it could be done (most are examples from the issues linked above)
// Current unstable implementation (doesn't emit a warning for this as
// long as Enum only has A and B). Useful in some situations, but IMO
// not in general for non-exhaustive enums.
#[warn(non_exhaustive_omitted_patterns)]
match (x, y) {
(Enum::A, Enum::A) => true,
(Enum::B, Enum::B) => true,
_ => false,
}
// What I'd prefer for this, as it makes this match more like non
// `non_exhaustive` matches, only using the fallback if the dependency
// is updated and adds a new variant.
match expr {
Expr::Array(e) => {...}
Expr::Assign(e) => {...}
#[warn(reachable)]
_ => { /* some sane fallback */ }
}
Here is another option that unfortunately doesn't work as of today, even though it probably should. EDIT: You still want type checking, borrow checking and everything else in the unused branch, so maybe this isn't how it should be (though that doesn't really matter for compile_error
. Therefore you'd still want to process/analyze/compile the unused branch. So to make this one work with compile_error
you'd probably need to delay the "stop compilation" of compile_error
until the "can this even be executed" check is done.
The compile_error is probably encountered and processed before the exhaustiveness checks and compilation always fails. I think this is the most readable in terms of name-clarity (avoiding the naming issue that seems to have stalled non_exhaustive_omitted_patterns
) and one of the most flexible, but it is probably quite a bit harder to implement, as it relates to in which order things are done in the compiler.
match expr {
Expr::Array(e) => {}
Expr::Assign(e) => {}
_ => {
compile_error!("Missing non_exhaustive pattern")
}
}
match expr {
Expr::Array(e) => {}
Expr::Assign(e) => {}
_ => {
compile_warn!("Missing non_exhaustive pattern")
}
}
How is it that the only way to handle this properly on stable rust (with non_exhaustive
existing for 5+ years) is a runtime panic? Am I missing something here?
Or if compiling in such a way where the dependency isn't linked at compile time. ↩︎