Shouldn't it be possible to #![allow(non_exhaustive)]

One of my favorite features of Rust is that you can write code in a way that ensures the compiler will guide you when things are changed in the future. One way I achieve this is by always matching on every enum variant and use struct deconstruction without a .. pattern when possible. This way, when new members or enum varians are added my code stops compiling and I can go and fix it.

Recently however it is more and more common for library authors to add the non_exhaustive attribute to enums. Because of this I am no longer able to force my code to fail when the library changes. The reason for this seems to me to be that crate authors want to change their enums without changing the semver of their crate.

While this is not a problem in many cases, what I don't get is why there is no way to ignore this attribute. At least for enums. I want my crate to stop compiling when new variants are added, but have no idea how to achieve that today.

Edit: It appears this is already solved by clippy, it just has to be manually enabled trough #![deny(clippy::wildcard_enum_match_arm)], thanks @illicitonion for pointing it out.

5 Likes

I guess I get where the sentiment comes from, but I don’t think this ought to be done. The reason is that it’s going to wreak havoc with versioning.

The presence of #[non_exhaustive] attribute weakens the API contract in that it disallows users of the library to assume that they know every enum variant that exists. This means that the library can add new variants in minor releases and remain source-compatible: that is, a minor release remains drop-in compatible at the source level, with no changes required to the users of the library.

With your #![allow(non_exhaustive)], you’d be no longer programming against the same API contract as everyone else, and the semver promise ‘minor releases never break the build’ falls apart: you no longer have a way of knowing which upgrades are supposed to be drop-in and which may require adjustments. And you may say: well, I’m okay with that. But you may be a dependency of someone else who is not okay with that. You might as well fork the library because you’re no longer writing against the same interface as everyone else.

If a crate overuses #[non_exhaustive], take it up with the authors of that specific crate. Just be aware that what you are asking is to strengthen the API contract the library exposes, which the authors probably had good reasons to want to avoid.

5 Likes
10 Likes

I agree. One case where I'd want this is whenever you want to turn one enum into another in an application. Like making sure you make a correct decision for error handling (was that SQL error caused by the user or did the program have a bug?) or to ensure complete transformation (like ensuring that error messages are covered).

Without any way of doing exhaustiveness control, even when writing an application without any downstream stability needs, you're locked into a runtime catchall fallback. In the worst case you'll wait for a user to tell you about running into some "other error" unless you build extra facilities to capture and alert you to some fallback happening.

2 Likes

I knew this, I just wasn't able to say it in so many words. I don't agree with the reasoning, but assume this was discussed at length when the attribute was added. To me this attribute goes against one of the main argumemts for rust and I remember being sad when it was added.

Either way, all I ask is that I can be the boss of my own code and be able to guarantie at compile time I won't fail.

The lint linked above would probably solve this for me.

3 Likes

This. I think #[non_exhaustive] is used inappropriately more often than not.

In my experience, some crate authors are overly obsessed with not breaking any APIs, which I think is completely unwarranted, since one can just depend on older versions of a crate and do so reliably.

I sympathize with OP, because on the consumers' side, it's very frustrating when I can't have my code fail to compile if something changes, because I then risk introducing bugs silently into my code. That sort of uncertainty is the worst.

Unfortunately, however, I don't think an #[allow(non_exhaustive)] attribute can be added to Rust, simply because #[non_exhaustive] is not a lint, it's a different kind of core language attribute. I.e., this is impossible for the same reason it would be impossible to add lints such as #[allow(wrong_types)] or #[allow(wrong_lifetimes)].

Maybe, just maybe, we could deprecate #[non_exhaustive] and add an equivalent mechanism that is designed to be an #[allow(…)]-able lint. This of course doesn't solve the problem with existing crates, you'd have to go to the crate authors and ask them nicely to update their code to use this hypothetical new feature instead of #[non_exhaustive].

1 Like

So... a forbid(reachable_nonexhaustive) lint that applies to the _ case?

(It being a lint would mean not breaking semver, because lints are capped anyway.)

Sounds like something that should be in clippy.

Unfortunately, however, I don't think an #[allow(non_exhaustive)] attribute can be added to Rust, simply because #[non_exhaustive]

Just to be clear, I don't really care how it is done or if it is a lint. I just want the functionality in any way that makes the most sense. I was trying to say that a solution to rust#81657 would likely cover my use case.

1 Like

Would work fine for my use-case at least.

I tried to search for it there before asking here, but didn't see anything.

You may want to open an issue on clippy about a reachable_nonexhaustive restriction lint, that would signal on a match against a non-exhaustive enum if the _ case is reachable, but not on a named default case. (then it's up to you not to use named default cases where you want the lint to signal.)

There are places where you don't want such a lint (matching on IO errors comes to mind) and having an easy way around it (naming the default case) is probably a good idea. But we can imagine plenty of cases where strict error handling would come in handy, even if a future version of an upstream crate changes things.

I think there is already a clippy restriction lint for this :slight_smile: Here are the docs for it

This test case shows the output it will show if you match _ or .. for a non-exhaustive enum

4 Likes

You are correct, it was just not enabled by my #[deny(clippy::all)], when I added it specifically it worked as expected. It also triggers on some cases where I did want a wildcard, but I rather change those than not have it.

Thanks

1 Like

Quick terminology note: restriction lints can't be put into a collection like clippy:all because some of them are mutually exclusive.

1 Like

Wonder if there should be a separate lint that doesn't signal on non-_ cases (so e.g. foo would be valid while _ would warn/error.)

This thread was just pointed out to me, and I just wanted to mention a different use case that I didn't notice discussed.

Today non_exhaustive does not apply to match statements inside a crate. This is good, though I do agree with everyone in this thread who says it would be nice as a consumer of an API to make that determination on the usage side.

One area that might be nice to consider as well, if for some reason folks decide this isn't important in general, is for workspaces. I think that most folks who use workspaces tend (though obviously this isn't always true) to release all workspace crates at the same time. What this generally means is that the same restrictions on crate boundaries sometimes make sense for workspace boundaries as well. In a nutshell, I would love it if inside the same workspace, non_exhaustive is disabled as it is inside a crate.

4 Likes

Do you have an example use-case in mind?

I'm struggling to think of a situation where "did you do give a name to the value" correlates strongly with "did you mean to include variants that didn't exist when you wrote the match"...

io::Error.

It makes sense to handle one or a few variants, and then propagate the rest upstream. std does that in some places.

It might not be perfect, but I ended up adding #[allow(clippy::wildcard_enum_match_arm)] in front of the match statements I wanted to allow the wildcard. When you add it in the middle of code, it applies to the next statement.

2 Likes

Yeah, but that's a misfeature.

This is not true though. When an enum grows a new variant, it is not the same. All pattern matches have to grow an extra arm at the behavioral level, and there is no guarantee that the "correct" behavior is to have a wildcard arm; a classic example would be a function fn map(fn(T) -> U, MyEnum<T>) -> MyEnum<U>, where the correct behavior is to map a new variant to the new variant applied to mapped parameters, and a wildcard arm may not even be implementable except by unreachable!().

And if you did implement map using unreachable!(), this is strictly worse than an exhaustive match, because you will silently get a runtime error when upstream makes a "semver backward compatible" change to add a variant, instead of a compile error that points out that the change, while "backward compatible", is not compatible with your code.

3 Likes