RFC 2383 proposed the addition of lint expectations in the form of an attribute. The RFC named this attribute #[expect]
and described the functionality as follows:
This RFC adds an
expect
lint attribute that functions identically toallow
, but will cause a lint to be emitted when the code it decorates does not raise a lint warning.
The RFC included two examples:
Example 1
โโโโ#[expect(unused_mut, reason = "Everything is mut until I get this figured out")]
โโโโfn foo() -> usize {
โโโโ let mut a = Vec::new();
โโโโ a.len()
โโโโ}
The expectation is fulfilled and nothing is emitted.
Example 2
โโโโ#[expect(unused_mut, reason = "...")]
โโโโfn foo() {
โโโโ let a = Vec::new();
โโโโ a.len()
โโโโ}
which should emit a warning like this:
โโโโwarning: expected lint `unused_mut` did not appear
โโโโreason: Everything is mut until I get this figured out
โโโโ --> src/lib.rs:1:1
โโโโ |
โโโโ1 | #[expect(unused_mut, reason = "...")]
โโโโ | -------^^^^^^^^^^-----------------
โโโโ | |
โโโโ | help: remove this `#[expect(...)]`
โโโโ |
โโโโ = note: #[warn(expectation_missing)] on by default
A working implementation of the #[expect]
attribute can be found on the Playground
See:
The RFC suggested the attribute name #[expect]
. In the comments, some concerns were expressed, that the name might be too generic and/or should be reserved for other features. #[should_lint]
or [expect_lint]
were proposed as an alternative names.
Options
#[expect]
#[allow]
, #[warn]
, #[deny]
#[should_lint]
| [expect_lint]
The RFC states that a lint expectation should suppress lint emissions inside the marked scope and emit a warning if nothing was emitted. This leads to the question: How should nested lint attributes be handled?
Example q2.1 (Playground):
#![feature(lint_reasons)]
#[expect(unused_variables)]
pub fn f() {
#[warn(unused_variables)]
let x = 1;
}
Options:
The lint level is changed by nested attributes, and the expectation won't be fulfilled by the lint emission.
For example q2.1: The unused_variables
lint would be emitted for x
since the lint level was changed to warn
. The unused_variables
expectation would not be fulfilled. (This is how the current implementation works.)
#[expect]
attribute can be overridden, like other lint levels canunused_variables
should be emitted for x
#[allow]
attributes can be replaced with #[expect]
, without changing how lints are emitted#[expect]
attribute can be removed in steps, adding #[warn]
to inner scopes until no lint is suppressed anymoreThe expectation suppresses all lint emissions
For example q2.1: The unused_variables
lint would be suppressed and the expectation would be fulfilled
#[expect]
would behave differently than #[allow]
and other attributesDeny nesting other lint attributes inside #[expect]
, similar to #[forbid]
For example q2.1: The code would emit an error, similar to how #[forbid]
can't be overridden.
#[allow]
with #[expect]
could cause errors#[expect]
attribute#[allow]
attributes, to prevent lint triggers in generated code. Forbidding nested lint attributes could cause unexpected errors.The RFC states that the lint expectation should be fulfilled by suppressing otherwise emitted lints. This leads to the question: Should the expectation be fulfilled if the lint was allowed before?
Example q3.1 (Playground):
#![feature(lint_reasons)]
#![allow(unused_unsafe)]
#[allow(unsafe_code)]
fn main() {
#[expect(unsafe_code)]
unsafe {
}
}
Options
The #[expect]
attributes suppresses all lint emissions that would be triggered for a #[warn]
attribute.
For example q3.1: The lint expectation would be fulfilled, since #[warn]
instead of #[expect]
would have issued a warning.
The #[expect]
attribute only suppresses lints if they have the warn level or higher at the attribute.
For example q3.1: The lint expectation would be unfulfilled as the lint is already allowed and no warning is emitted before the expectation was added
#![allow]
to the project module could have a cascading effect on existing #[expect]
attributesscottmcm: Looking at a bunch of the ยฑ, I get the feeling that there's an implied "is this a new level or a different feature?" overall question. How should people think about this? Would there ever be an -E warnings
like there's a -D warnings
?
xFrednet: Yes, I feel like this was one of the main points of discussion. I feel like it's a different level, but -E warnings
won't be a thing.
joshtriplett: I think we might want to consider disallowing expect(warnings)
, since it's so non-specific. Which warning are you expecting? I have a hard time imagining it proving useful.
scottmcm: Some of the options seem to imply that you could expect something that's allowed. That seems like it'd require running those lints when we wouldn't otherwise have to. Is that a perf concern? Or could it be scoped just to where there's expect
s in play so it's an opt-in perf hit?
joshtriplett: I'd expect it to have exactly the peformance implications of warn
. (If we can optimize warn
on a scoped basis, we can optimize expect
the same way)
xFrednet: It's implemented like a warn, we use expect
in clippy (also as a test project) and haven't noticed any negative effects related to performance.
pnkfelix: I too am confused by the expect/warn example. :) Maybe that is in part because I'm not sure whether there are two distinct cases that should be handled separately: an expect where you indeed expect a warning and do not want it silenced, vs an expect where you want to also allow it (i.e. silence the warning)?
pnkfelix: maybe in other words: How else should I write down my expectation without silencing the warning? And how should I write down an epectation in that expect+warn case where my intent is to signal that I expect that warning to fire?
xFrednet: One question was how to handle force_warn
. Currently it fulfills the expectation but still emits the warning. Could be useful at a tool level.
josh: If we were redesigning Rust I might use allow
for what expect
does here, and use a longer attribute for "allow this and I don't care if it actually happens".
scottmcm: The one exception to that might be macros, where you may need to allow something that might or might not happen.
tmandry: Is it workable to think of this as lints "bubbling out" of the scope if they aren't caught by allow
/expect
to higher levels? forbid
would be a different category that restricts what you can do in lower level.
#![feature(lint_reasons)]
#[expect(unused_variables)]
pub fn f() {
#[warn(unused_variables)]
let x = 1;
}
tmandry: So in this model the warn on x
would fire, but the "unused expectation" would not.
xFrednet: I see it as more of a stack; in this scope this is the level.
tmandry: I'm wondering if that is more of an implementation-focused model, whether it's intuitive for users.
pnkfelix: Similar reaction
Josh: I would expect to get an unused_variable lint and not get an unfulfilled expectation lint.
xFrednet: Example: Let's say you had an expect at the top of a file, and then want to incrementally replace the expect with warn.
Josh: So you expect the expect to only "work" if it is actively suppressing a lint. Seems like a valid intuition.
xFrednet: What's the use case?
josh: I'd expect that the only purpose of adding a warn would be to promptly fix the warn.
xFrednet: Same thing can be achieved without that. Might be that you get a warning that the expectation is no longer fulfilled, you get two warnings instead of one.
Josh: Different intuitions, both seem valid, we need to figure out which one we want.
Bryan Garza: I think of it as colors applied to a scope. Wouldn't think of the warn as going back up in any way, just changes what happens going further in.
xFrednet: Also depends on how you phrase it. I think of it as "it suppresses a warning" but you could also phrase it as "if a warning is emitted" in the scope.
Mark:
#[expect(unsafe_code)]
pub fn f() {
unsafe {
}
}
In this example from the RFC, unsafe_code
is not emitted by default
xFrednet: Revision: "if it would be emitted if you had replaced expect
with warn
".
#[warn(unsafe_code)]
pub fn f() {
unsafe {
}
}
Josh: Imagine you have an expect
with a nested allow
in a macro. You don't want the allow
in the macro to fulfill your expectation. It's pretty clear that allow
should not cause an expect to be fulfilled; maybe for consistency's sake we should do the same for warn
.
Mark: Not sure I buy the argument that that's consistency, though. unused_variables is warn-by-default. I would expect that if you add warn
to a lint that's already warn-by-default it would be a no-op. Not sure if that's self-consistent.
pnkfelix: Coupling expect with allow the right thing?
Josh: Should look like the other ones; does anyone object to expect
?
scottmcm: To me it's a model question. If it's a lint level that behaves like others, it should be expect, otherwise (with e.g. Tyler's model) something like should_lint
leans into that.
pnkfelix:
xFrednet: lint
?
xFrednet: Different use case for expect
?
pnkfelix: Contracts maybe.
tmandry: +1 to contracts, but there are other names they could potentially use. precondition, postcondition, require(scottmcm)
tmandry: Agree that if it's more of a lint level then expect
makes sense.
Mark: A lot of this discussion is around contrived models, theoretical discussion. Can we hunt down actual usages and not worry about edge cases?
xFrednet: I went through Clippy codebase in https://github.com/rust-lang/rust-clippy/pull/8797 and saw if we could replace allows with expect. Only thing we noticed was that some allows were unneeded and could be removed, everything else was smooth.
Josh: Now I almost want a warn(allow)
. Warn me if I ever use allow in my code instead of expect
.
tmandry: I agree with looking at practical examples, slightly dubious about putting too much weight on a single one. (Though clippy is not a bad one)
Josh/pnkfelix: Seems unlikely we'll get one soon, it's unstable. Good idea to try it out in the compiler.
scottmcm?: Do we expect difference in use case between rustc lints and clippy lints?
Josh: Another use case: expect naming lints on a module with a bunch of C types.
scottmcm: Reason for expect vs allow?
Mark: Might be worth finding answers to questions around: CLI flags; are there cases where some subset of code under that is changing the level? If we can't find examples, maybe forbid it.
xFrednet: In the latter case you have macros, wouldn't necessarily forbid it.
xFrednet: Think of command line as not part of your code