From 84007dd265f5c45cf0cbc48acf4f94159860ede9 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 28 Jul 2026 21:56:23 -0700 Subject: [PATCH 1/6] skippable_lints: Account for lint capping Dependencies get built with `--cap-lints allow`, so there's value in allowing capped lints to be skipped, even if the crate itself doesn't allow them. --- compiler/rustc_lint/src/levels.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/compiler/rustc_lint/src/levels.rs b/compiler/rustc_lint/src/levels.rs index 5671e86283f9b..e5ee52df9e312 100644 --- a/compiler/rustc_lint/src/levels.rs +++ b/compiler/rustc_lint/src/levels.rs @@ -117,6 +117,7 @@ impl LintLevelSets { fn skippable_lints(tcx: TyCtxt<'_>, (): ()) -> UnordSet { let store = unerased_lint_store(&tcx.sess); let root_map = tcx.shallow_lint_levels_on(hir::CRATE_OWNER_ID); + let lint_cap_allow = tcx.sess.opts.lint_cap == Some(Level::Allow); let mut skippable: FxHashSet = store .get_lints() @@ -130,8 +131,10 @@ fn skippable_lints(tcx: TyCtxt<'_>, (): ()) -> UnordSet { .filter(|lint| { let level_spec = root_map.lint_level_spec_at_node(tcx, LintId::of(lint), hir::CRATE_HIR_ID); - // Only include lints that are allowed at crate root or by default. - level_spec.is_allow() + let level = level_spec.level(); + // Only include lints that are allowed at crate root, or by capping, or by default. + level == Level::Allow + || (lint_cap_allow && matches!(level, Level::Warn | Level::Deny | Level::Forbid)) || (matches!(level_spec.src, LintLevelSource::Default) && lint.default_level(tcx.sess.edition()) == Level::Allow) }) @@ -144,8 +147,12 @@ fn skippable_lints(tcx: TyCtxt<'_>, (): ()) -> UnordSet { // All lints that appear with a non-allow level must be run. for (_, specs) in map.specs.iter() { for (lint, level_spec) in specs.iter() { - if !level_spec.is_allow() { - skippable.remove(lint); + match level_spec.level() { + Level::Allow => {} + Level::Warn | Level::Deny | Level::Forbid if lint_cap_allow => {} + _ => { + skippable.remove(lint); + } } } } From d21e6f6579c4ca0c0b8a33b0098e65471c8f59ec Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Tue, 28 Jul 2026 22:45:30 -0700 Subject: [PATCH 2/6] late_lint_mod: Don't construct `LateContext` if skipping `late_lint_mod` constructs `LateContext` immediately, before figuring out if the whole pass will be skipped. Move its construction into `late_lint_mod_inner`, so it doesn't get called if the pass gets skipped. --- compiler/rustc_lint/src/late.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/compiler/rustc_lint/src/late.rs b/compiler/rustc_lint/src/late.rs index 227bff83cabbf..1c898814b9aa3 100644 --- a/compiler/rustc_lint/src/late.rs +++ b/compiler/rustc_lint/src/late.rs @@ -337,17 +337,6 @@ pub fn late_lint_mod<'tcx, T: LateLintPass<'tcx> + 'tcx>( mod_id: LocalModId, builtin_lints: T, ) { - let context = LateContext { - tcx, - enclosing_body: None, - cached_typeck_results: Cell::new(None), - param_env: ty::ParamEnv::empty(), - effective_visibilities: tcx.effective_visibilities(()), - last_node_with_lint_attrs: tcx.local_def_id_to_hir_id(mod_id), - generics: None, - only_module: true, - }; - let skippable_lints = tcx.skippable_lints(()); // Note: `passes` is often empty. In that case, it's faster to run @@ -362,23 +351,33 @@ pub fn late_lint_mod<'tcx, T: LateLintPass<'tcx> + 'tcx>( let builtin_lints_must_run = is_lint_pass_required(skippable_lints, &builtin_lints.get_lints()); if passes.is_empty() { if builtin_lints_must_run { - late_lint_mod_inner(tcx, mod_id, context, builtin_lints); + late_lint_mod_inner(tcx, mod_id, builtin_lints); } } else { if builtin_lints_must_run { passes.push(Box::new(builtin_lints) as Box>); } let pass = RuntimeCombinedLateLintPass { passes }; - late_lint_mod_inner(tcx, mod_id, context, pass); + late_lint_mod_inner(tcx, mod_id, pass); } } fn late_lint_mod_inner<'tcx, T: LateLintPass<'tcx>>( tcx: TyCtxt<'tcx>, mod_id: LocalModId, - context: LateContext<'tcx>, pass: T, ) { + let context = LateContext { + tcx, + enclosing_body: None, + cached_typeck_results: Cell::new(None), + param_env: ty::ParamEnv::empty(), + effective_visibilities: tcx.effective_visibilities(()), + last_node_with_lint_attrs: tcx.local_def_id_to_hir_id(mod_id), + generics: None, + only_module: true, + }; + let mut cx = LateContextAndPass { context, pass }; let (module, _span, hir_id) = tcx.hir_get_module(mod_id); From 2f02c41a2ace77187bcfc6aba99d3966d3244506 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Wed, 29 Jul 2026 00:56:10 -0700 Subject: [PATCH 3/6] liveness: `use` lints to avoid repeating their paths --- compiler/rustc_mir_transform/src/liveness.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/compiler/rustc_mir_transform/src/liveness.rs b/compiler/rustc_mir_transform/src/liveness.rs index 32951ea0162a6..23a318c8cb442 100644 --- a/compiler/rustc_mir_transform/src/liveness.rs +++ b/compiler/rustc_mir_transform/src/liveness.rs @@ -15,6 +15,7 @@ use rustc_middle::ty::{self, Ty, TyCtxt}; use rustc_mir_dataflow::fmt::DebugWithContext; use rustc_mir_dataflow::{Analysis, Backward, ResultsCursor}; use rustc_session::lint; +use rustc_session::lint::builtin::{UNUSED_ASSIGNMENTS, UNUSED_VARIABLES}; use rustc_span::Span; use rustc_span::edit_distance::find_best_match_for_name; use rustc_span::symbol::{Symbol, kw, sym}; @@ -1074,7 +1075,7 @@ impl<'a, 'tcx> AssignmentResult<'a, 'tcx> { diagnostics::UnusedVariableSugg::TryPrefix { spans: vec![def_span], name, typo } }; tcx.emit_node_span_lint( - lint::builtin::UNUSED_VARIABLES, + UNUSED_VARIABLES, hir_id, def_span, diagnostics::UnusedVariable { @@ -1124,7 +1125,7 @@ impl<'a, 'tcx> AssignmentResult<'a, 'tcx> { let typo = maybe_suggest_typo(); tcx.emit_node_span_lint( - lint::builtin::UNUSED_VARIABLES, + UNUSED_VARIABLES, hir_id, def_span, diagnostics::UnusedVarAssignedOnly { name, typo }, @@ -1166,7 +1167,7 @@ impl<'a, 'tcx> AssignmentResult<'a, 'tcx> { }; tcx.emit_node_span_lint( - lint::builtin::UNUSED_VARIABLES, + UNUSED_VARIABLES, hir_id, spans, diagnostics::UnusedVariable { @@ -1258,20 +1259,20 @@ impl<'a, 'tcx> AssignmentResult<'a, 'tcx> { if suggestion.is_none() && is_direct { overwrite } else { None }; let help = suggestion.is_none() && overwrite.is_none(); tcx.emit_node_span_lint( - lint::builtin::UNUSED_ASSIGNMENTS, + UNUSED_ASSIGNMENTS, hir_id, source_info.span, diagnostics::UnusedAssign { name, overwrite, help, suggestion }, ) } AccessKind::Param => tcx.emit_node_span_lint( - lint::builtin::UNUSED_ASSIGNMENTS, + UNUSED_ASSIGNMENTS, hir_id, source_info.span, diagnostics::UnusedAssignPassed { name }, ), AccessKind::Capture => tcx.emit_node_span_lint( - lint::builtin::UNUSED_ASSIGNMENTS, + UNUSED_ASSIGNMENTS, hir_id, decl_span, diagnostics::UnusedCaptureMaybeCaptureRef { name }, From cff54273da584ed9104ff36748040d66f076e155 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Wed, 29 Jul 2026 00:56:33 -0700 Subject: [PATCH 4/6] check_liveness: Skip based on `skippable_lints` These lints will be skippable when building a crate with `--cap-lints allow`, which occurs for every dependency crate. In such cases, we can skip the whole query. --- compiler/rustc_middle/src/queries.rs | 1 + compiler/rustc_mir_transform/src/liveness.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_middle/src/queries.rs b/compiler/rustc_middle/src/queries.rs index 28a35013b6516..5a431440bd528 100644 --- a/compiler/rustc_middle/src/queries.rs +++ b/compiler/rustc_middle/src/queries.rs @@ -1198,6 +1198,7 @@ rustc_queries! { desc { "checking privacy in {}", describe_as_module(key.to_local_def_id(), tcx) } } + /// Checks for liveness of variables within a function. No-op if unused lints are skippable. query check_liveness(key: LocalDefId) -> &'tcx rustc_index::bit_set::DenseBitSet { arena_cache desc { "checking liveness of variables in `{}`", tcx.def_path_str(key.to_def_id()) } diff --git a/compiler/rustc_mir_transform/src/liveness.rs b/compiler/rustc_mir_transform/src/liveness.rs index 23a318c8cb442..5ae42a738cb75 100644 --- a/compiler/rustc_mir_transform/src/liveness.rs +++ b/compiler/rustc_mir_transform/src/liveness.rs @@ -14,7 +14,7 @@ use rustc_middle::ty::print::with_no_trimmed_paths; use rustc_middle::ty::{self, Ty, TyCtxt}; use rustc_mir_dataflow::fmt::DebugWithContext; use rustc_mir_dataflow::{Analysis, Backward, ResultsCursor}; -use rustc_session::lint; +use rustc_session::lint::LintId; use rustc_session::lint::builtin::{UNUSED_ASSIGNMENTS, UNUSED_VARIABLES}; use rustc_span::Span; use rustc_span::edit_distance::find_best_match_for_name; @@ -54,6 +54,13 @@ struct Access { #[tracing::instrument(level = "debug", skip(tcx), ret)] pub(crate) fn check_liveness<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId) -> DenseBitSet { + let skippable_lints = tcx.skippable_lints(()); + if skippable_lints.contains(&LintId::of(UNUSED_ASSIGNMENTS)) + && skippable_lints.contains(&LintId::of(UNUSED_VARIABLES)) + { + return DenseBitSet::new_empty(0); + } + // Don't run on synthetic MIR, as that will ICE trying to access HIR. if tcx.is_synthetic_mir(def_id) { return DenseBitSet::new_empty(0); From da92bde4ab304f9aa7c108a8a250e37f09ea575d Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Wed, 29 Jul 2026 07:56:49 -0700 Subject: [PATCH 5/6] skippable_lints: Handle `-Zfuture-incompat-test` `-Zfuture-incompat-test` forces non-allow lints to report as future-compat. Unfortunately, rather than doing so by modifying the lints themselves, it requires checking separately. Check it and prevent skipping lints if enabled. --- compiler/rustc_lint/src/levels.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/rustc_lint/src/levels.rs b/compiler/rustc_lint/src/levels.rs index e5ee52df9e312..b0e5e3b2362a9 100644 --- a/compiler/rustc_lint/src/levels.rs +++ b/compiler/rustc_lint/src/levels.rs @@ -126,7 +126,10 @@ fn skippable_lints(tcx: TyCtxt<'_>, (): ()) -> UnordSet { // Lints that show up in future-compat reports must always be run. let has_future_breakage = lint.future_incompatible.is_some_and(|fut| fut.report_in_deps); - !has_future_breakage && !lint.eval_always + // `-Zfuture-incompat-test` forces non-allow lints to report as future-compat + let test = tcx.sess.opts.unstable_opts.future_incompat_test + && lint.default_level != Level::Allow; + !has_future_breakage && !test && !lint.eval_always }) .filter(|lint| { let level_spec = From 1a95622d219a22f316d7eaa378c33d9bfe220582 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Wed, 29 Jul 2026 01:16:40 -0700 Subject: [PATCH 6/6] check_mod_deathness: Skip based on `skippable_lints` These lints will be skippable when building a crate with `--cap-lints allow`, which occurs for every dependency crate. In such cases, we can skip the whole query. --- compiler/rustc_passes/src/dead.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_passes/src/dead.rs b/compiler/rustc_passes/src/dead.rs index 9b3a4099b8bee..9b3cf9d2b9c3c 100644 --- a/compiler/rustc_passes/src/dead.rs +++ b/compiler/rustc_passes/src/dead.rs @@ -23,7 +23,7 @@ use rustc_middle::ty::{self, AssocTag, TyCtxt}; use rustc_middle::{bug, span_bug}; use rustc_session::config::CrateType; use rustc_session::lint::builtin::{DEAD_CODE, DEAD_CODE_PUB_IN_BINARY}; -use rustc_session::lint::{self, Lint, StableLintExpectationId}; +use rustc_session::lint::{self, Lint, LintId, StableLintExpectationId}; use rustc_span::{Symbol, kw}; use crate::diagnostics::{ @@ -1328,6 +1328,14 @@ impl<'tcx> DeadVisitor<'tcx> { } fn check_mod_deathness(tcx: TyCtxt<'_>, module: LocalModId) { + let skippable_lints = tcx.skippable_lints(()); + let is_exec = tcx.crate_types().contains(&CrateType::Executable); + if (!is_exec || skippable_lints.contains(&LintId::of(DEAD_CODE_PUB_IN_BINARY))) + && skippable_lints.contains(&LintId::of(DEAD_CODE)) + { + return; + } + let Ok(DeadCodeLivenessSummary { pre_deferred_seeding, final_result }) = tcx.live_symbols_and_ignored_derived_traits(()).as_ref() else { @@ -1336,7 +1344,7 @@ fn check_mod_deathness(tcx: TyCtxt<'_>, module: LocalModId) { let module_items = tcx.hir_module_items(module); - if tcx.crate_types().contains(&CrateType::Executable) { + if is_exec { let is_unused_pub = |def_id: LocalDefId| { tcx.effective_visibilities(()).is_public_at_level(def_id, Level::Reachable) && !pre_deferred_seeding.live_symbols.contains(&def_id)