Add generic allow-in-tests configuration - #17520
Conversation
Adds an array-typed `allow-in-tests` option that suppresses any Clippy lint in test code, superseding the per-lint `allow-*-in-tests` options. Suppression happens once at diagnostic emission in `clippy_utils`, so it covers every lint rather than needing per-lint plumbing. Lint names are resolved against the declared lints at startup; unknown names warn with a span into `clippy.toml`. The seven migratable `allow-*-in-tests` options are soft-deprecated: they keep working, but setting one to `true` now warns and points at the replacement. `allow-large-stack-frames-in-tests` is excluded because its meaningful setting is `false`, which `allow-in-tests` cannot express.
| concat!("`", $name_str, "` is deprecated"), | ||
| ) | ||
| .with_help(format!( | ||
| "use `allow-in-tests = [{}]` instead, which works for any lint", |
There was a problem hiding this comment.
but it doesn't work for "any lint", only for late pass lints
| /// | ||
| /// This supersedes the per-lint `allow-<lint>-in-tests` options, which are deprecated but | ||
| /// still honored: a lint is suppressed in test code if it is listed here or its own | ||
| /// `allow-<lint>-in-tests` option is set. |
There was a problem hiding this comment.
what if the allow-*-in-tests is explicitly set to false, but here the lint is listed - it should be made clear that if either place says the lint should be allowed then it is
| /// Resolves the lint names given in the `allow-in-tests` configuration and hands them to | ||
| /// `clippy_utils`, which drops their diagnostics when they are emitted from test code. | ||
| /// | ||
| /// Names which don't refer to a Clippy lint are reported and ignored. |
There was a problem hiding this comment.
what about non-late-pass lints - can they be reported too?
| ## `allow-expect-in-tests` | ||
| Whether `expect` should be allowed in test functions or `#[cfg(test)]` | ||
|
|
||
| Deprecated in favor of [`allow-in-tests`](#allow-in-tests), which works for any | ||
| lint. This option still works, but new configurations should use `allow-in-tests`. |
There was a problem hiding this comment.
Community review:
I am not sure that this is a good idea.
I think it is not uncommon to want to allow .expect, but not dbg! in tests.
Therefore this seems like a lot of churn for some users and not an 100% pure win at that.
|
Was AI used in this PR? If so, we require disclosure. |
Thanks for the flag. Yes, I used AI (Opus 5) and will figure out where to disclose that. |
NOTE: this PR was drafted with Opus 5. I (the human) reviewed all content, including by-hand fixes, prior to submission.
Motivation
Clippy has eight
allow-*-in-testsoptions today:allow-dbg-in-tests,allow-expect-in-tests,allow-indexing-slicing-in-tests,allow-large-stack-frames-in-tests,allow-panic-in-tests,allow-panic-in-result-fn-in-tests,allow-print-in-tests,allow-unwrap-in-tests. They all express the same idea — suppress this lint in test code — but each one is implemented from scratch inside the lint it controls.What each one costs to add
Every option needs a config field, a field on the lint's struct, a
new(conf)constructor, threading through helper functions, and anis_in_testcall. Lints that were previously stateless have to be converted fromdeclare_lint_pass!toimpl_lint_pass!and registered differently. A recent addition (#17516) of a single option touched 8 files for 66 insertions, of which only a handful were the actual behavior:Across the eight existing options that is ~41 lines of near-identical plumbing in
clippy_lintsalone (methods/mod.rs10,methods/unwrap_expect_used.rs8,indexing_slicing.rs5,write/mod.rs5,panic_unimplemented.rs4,dbg_macro.rs3,large_stack_frames.rs3,useless_vec.rs3), plus eight config entries, eight book sections and eight CHANGELOG links.The cost is also recurring rather than one-off: the list has grown steadily, and each new request means repeating the pattern in another lint. With
allow-in-tests, extending coverage to a lint costs zero code — it already works.It also removes a source of behavioral drift
Because each lint implements the check itself, each one picks its own node to test:
Nothing enforces consistency, and a new implementation can easily pick a node that behaves subtly differently from the rest. Doing the check once at emission means every lint resolves it against the same node rustc uses to resolve
#[allow], soallow-in-tests = ["foo"]matches#[allow(clippy::foo)]semantics by construction.For users
One option name to learn and grep for instead of discovering a bespoke name per lint, and it works for lints that never got their own option, which is the majority of them.
Approach
Rather than threading a flag into each lint, suppression happens once at diagnostic emission in
clippy_utils::diagnostics. Every lint funnels throughspan_lint_and_thenorspan_lint_hir_and_then, so one check there covers all of them. AClippyLintContexttrait (supertrait ofLintContext) supplies the in-test check:LateContextanswers viais_in_test(tcx, last_node_with_lint_attrs),EarlyContextalways returnsfalse.Lint names are resolved against
declared_lints::LINTSat registration time and stored as aFxHashSet<&'static str>, so the hot path is aOnceLock::get()plus anis_empty()check before any hashing — no cost for the overwhelmingly common case of an unset option. Unknown names warn with a span intoclippy.toml, suggesting the new name for renamed lints or an edit-distance match otherwise.The one structural cost is that
clippy_utilsgains a process-globalOnceLock, set fromregister_lint_passes.clippy_configdepends onclippy_utils, soConfcan't be read from the diagnostic helpers directly. There is existing precedent for this shape (TEST_ITEM_NAMES_CACHE,SEEN_MSRV_ATTR), but I'd rather be told now if it's unacceptable.Soft deprecation
The seven migratable options keep working, but setting one to
truenow warns and points at the replacement, via a new#[replaced_by_allow_in_tests(...)]attribute indefine_Conf!:It fires only on
true;falseis the default and has noallow-in-testsequivalent, so warning there would suggest inverting the meaning. This follows the existing#[rename]precedent, which warns indefinitely without removal —clippy.tomlis not edition-gated, andbook/src/configuration.mdalready states the file is unstable.Open questions for maintainers
allow-large-stack-frames-in-testsis deliberately not deprecated: it defaults totrue, so its meaningful setting isfalse, whichallow-in-testscannot express. Is leaving it out right, or shouldallow-in-testsgrow a way to re-enable a lint in tests?allow-*-in-testsis a late pass, so behaviour is preserved.ifin the macro expansion.#[replaced_by_allow_in_tests]must precede#[lints]becausecargo dev fmtalways re-emits#[lints]last. Happy to teach the formatter about it instead.Checklist
.stderrfiles)cargo testpasses locallycargo dev fmtandcargo bless --test config-metadatachangelog: Added [
allow-in-tests] configuration to suppress any lint in test code, and soft-deprecated the per-lintallow-*-in-testsoptions