Skip to content

Add generic allow-in-tests configuration - #17520

Open
dcsommer wants to merge 1 commit into
rust-lang:masterfrom
dcsommer:test-config
Open

Add generic allow-in-tests configuration#17520
dcsommer wants to merge 1 commit into
rust-lang:masterfrom
dcsommer:test-config

Conversation

@dcsommer

@dcsommer dcsommer commented Aug 7, 2026

Copy link
Copy Markdown

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-tests options 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 an is_in_test call. Lints that were previously stateless have to be converted from declare_lint_pass! to impl_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:

 CHANGELOG.md                                   |  1 +
 book/src/lint_configuration.md                 | 10 +++++
 clippy_config/src/conf.rs                      |  3 +++
 clippy_lints/src/lib.rs                        |  2 +-
 clippy_lints/src/panic_in_result_fn.rs         | 31 ++++++++++++-----
 tests/ui-toml/panic_in_result_fn/clippy.toml   |  1 +
 tests/ui-toml/.../panic_in_result_fn.rs        | 24 ++++++++++++
 tests/ui-toml/.../conf_unknown_key.stderr      |  1 +

Across the eight existing options that is ~41 lines of near-identical plumbing in clippy_lints alone (methods/mod.rs 10, methods/unwrap_expect_used.rs 8, indexing_slicing.rs 5, write/mod.rs 5, panic_unimplemented.rs 4, dbg_macro.rs 3, large_stack_frames.rs 3, useless_vec.rs 3), 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:

dbg_macro.rs            is_in_test(cx.tcx, expr.hir_id)
indexing_slicing.rs     is_in_test(cx.tcx, expr.hir_id)
panic_unimplemented.rs  is_in_test(cx.tcx, expr.hir_id)
large_stack_frames.rs   is_in_test(cx.tcx, cx.tcx.local_def_id_to_hir_id(local_def_id))

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], so allow-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 through span_lint_and_then or span_lint_hir_and_then, so one check there covers all of them. A ClippyLintContext trait (supertrait of LintContext) supplies the in-test check: LateContext answers via is_in_test(tcx, last_node_with_lint_attrs), EarlyContext always returns false.

allow-in-tests = ["dbg_macro", "unwrap_used"]

Lint names are resolved against declared_lints::LINTS at registration time and stored as a FxHashSet<&'static str>, so the hot path is a OnceLock::get() plus an is_empty() check before any hashing — no cost for the overwhelmingly common case of an unset option. Unknown names warn with a span into clippy.toml, suggesting the new name for renamed lints or an edit-distance match otherwise.

The one structural cost is that clippy_utils gains a process-global OnceLock, set from register_lint_passes. clippy_config depends on clippy_utils, so Conf can'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 true now warns and points at the replacement, via a new #[replaced_by_allow_in_tests(...)] attribute in define_Conf!:

warning: `allow-print-in-tests` is deprecated
 --> clippy.toml:3:1
  |
3 | allow-print-in-tests = true
  | ^^^^^^^^^^^^^^^^^^^^
  |
  = help: use `allow-in-tests = ["print_stderr", "print_stdout"]` instead, which works for any lint

It fires only on true; false is the default and has no allow-in-tests equivalent, so warning there would suggest inverting the meaning. This follows the existing #[rename] precedent, which warns indefinitely without removal — clippy.toml is not edition-gated, and book/src/configuration.md already states the file is unstable.

Open questions for maintainers

  • allow-large-stack-frames-in-tests is deliberately not deprecated: it defaults to true, so its meaningful setting is false, which allow-in-tests cannot express. Is leaving it out right, or should allow-in-tests grow a way to re-enable a lint in tests?
  • This applies to late lint passes only, since test code becomes recognizable once the HIR is built. Documented as a caveat; every lint with an existing allow-*-in-tests is a late pass, so behaviour is preserved.
  • Should the deprecation warning be opt-in for a release cycle rather than on by default? It's a single if in the macro expansion.
  • #[replaced_by_allow_in_tests] must precede #[lints] because cargo dev fmt always re-emits #[lints] last. Happy to teach the formatter about it instead.

Checklist

  • Added passing UI tests (including committed .stderr files)
  • cargo test passes locally
  • Added documentation
  • Ran cargo dev fmt and cargo bless --test config-metadata

changelog: Added [allow-in-tests] configuration to suppress any lint in test code, and soft-deprecated the per-lint allow-*-in-tests options

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.
@dcsommer
dcsommer marked this pull request as ready for review August 7, 2026 20:19
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties S-waiting-on-community-reviews Status: This is awaiting for positive reviews from the community before a maintainer is assigned. labels Aug 7, 2026

@DanielEScherzer DanielEScherzer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this big of a change probably will need an FCP, but some community review

View changes since this review

Comment thread clippy_config/src/conf.rs
concat!("`", $name_str, "` is deprecated"),
)
.with_help(format!(
"use `allow-in-tests = [{}]` instead, which works for any lint",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

but it doesn't work for "any lint", only for late pass lints

Comment thread clippy_config/src/conf.rs
///
/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread clippy_lints/src/lib.rs
/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what about non-late-pass lints - can they be reported too?

Comment on lines 97 to +101
## `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`.

@CommanderStorm CommanderStorm Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

View changes since the review

@blyxyas

blyxyas commented Aug 10, 2026

Copy link
Copy Markdown
Member

Was AI used in this PR? If so, we require disclosure.

@dcsommer

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-community-reviews Status: This is awaiting for positive reviews from the community before a maintainer is assigned. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants