authorbors <bors@rust-lang.org> 2026-06-25 08:22:49 UTC
committerbors <bors@rust-lang.org> 2026-06-25 08:22:49 UTC
log973ad0d0ab149bde2e96422833c1265c7a5be217
treeacaa8bb97cc8b9695f7c0f66a378da0c65c6ff19
parent73100eefe2afb831c4964f579c95beeb27b86e28
parent2724e23a54c66ce397c6028c0645ce51ad146e36

Auto merge of #158239 - nnethercote:rework-lint-pass-running, r=Urgau

Rework lint pass running Some cleanups relating to the running of lint passes. r? @Urgau

12 files changed, 93 insertions(+), 77 deletions(-)

compiler/rustc_hir_analysis/src/check/region.rs+1-1
......@@ -148,7 +148,7 @@ fn resolve_block<'tcx>(
148148 if !terminating
149149 && !visitor
150150 .tcx
151 .lints_that_dont_need_to_run(())
151 .skippable_lints(())
152152 .contains(&lint::LintId::of(lint::builtin::TAIL_EXPR_DROP_ORDER))
153153 {
154154 // If this temporary scope will be changing once the codebase adopts Rust 2024,
compiler/rustc_interface/src/passes.rs-2
......@@ -101,7 +101,6 @@ fn pre_expansion_lint<'a>(
101101 lint_store,
102102 registered_tools,
103103 None,
104 rustc_lint::BuiltinCombinedPreExpansionLintPass::new(),
105104 check_node,
106105 );
107106 },
......@@ -479,7 +478,6 @@ fn early_lint_checks(tcx: TyCtxt<'_>, (): ()) {
479478 lint_store,
480479 tcx.registered_tools(()),
481480 Some(lint_buffer),
482 rustc_lint::BuiltinCombinedEarlyLintPass::new(),
483481 EarlyCheckNode::CrateRoot(&*krate, &*krate.attrs),
484482 )
485483}
compiler/rustc_lint/src/context.rs+2-1
......@@ -40,7 +40,8 @@ use self::TargetLint::*;
4040use crate::levels::LintLevelsBuilder;
4141use crate::passes::{EarlyLintPassObject, LateLintPassObject};
4242
43type EarlyLintPassFactory = Box<dyn Fn() -> EarlyLintPassObject + sync::DynSend + sync::DynSync>;
43pub(crate) type EarlyLintPassFactory =
44 Box<dyn Fn() -> EarlyLintPassObject + sync::DynSend + sync::DynSync>;
4445type LateLintPassFactory =
4546 Box<dyn for<'tcx> Fn(TyCtxt<'tcx>) -> LateLintPassObject<'tcx> + sync::DynSend + sync::DynSync>;
4647
compiler/rustc_lint/src/early.rs+39-26
......@@ -16,7 +16,7 @@ use rustc_span::{Ident, Span};
1616use tracing::debug;
1717
1818use crate::DiagAndSess;
19use crate::context::{EarlyContext, LintContext, LintStore};
19use crate::context::{EarlyContext, EarlyLintPassFactory, LintContext, LintStore};
2020use crate::passes::{EarlyLintPass, EarlyLintPassObject};
2121
2222pub(super) mod diagnostics;
......@@ -316,7 +316,6 @@ pub fn check_ast_node<'a>(
316316 lint_store: &LintStore,
317317 registered_tools: &RegisteredTools,
318318 lint_buffer: Option<LintBuffer>,
319 builtin_lints: impl EarlyLintPass + 'static,
320319 check_node: EarlyCheckNode<'a>,
321320) {
322321 let context = EarlyContext::new(
......@@ -328,35 +327,20 @@ pub fn check_ast_node<'a>(
328327 lint_buffer.unwrap_or_default(),
329328 );
330329
331 // Note: `passes` is often empty. In that case, it's faster to run
332 // `builtin_lints` directly rather than bundling it up into the
333 // `RuntimeCombinedEarlyLintPass`.
334 let passes =
335 if pre_expansion { &lint_store.pre_expansion_passes } else { &lint_store.early_passes };
336 if passes.is_empty() {
337 check_ast_node_inner(sess, check_node, context, builtin_lints);
330 let context = if pre_expansion {
331 let builtin_lints = crate::BuiltinCombinedPreExpansionLintPass::new();
332 let passes = &lint_store.pre_expansion_passes;
333 run_passes(check_node, context, builtin_lints, passes)
338334 } else {
339 let mut passes: Vec<_> = passes.iter().map(|mk_pass| (mk_pass)()).collect();
340 passes.push(Box::new(builtin_lints));
341 let pass = RuntimeCombinedEarlyLintPass { passes };
342 check_ast_node_inner(sess, check_node, context, pass);
343 }
344}
345
346fn check_ast_node_inner<'a, T: EarlyLintPass>(
347 sess: &Session,
348 check_node: EarlyCheckNode<'a>,
349 context: EarlyContext<'_>,
350 pass: T,
351) {
352 let mut cx = EarlyContextAndPass { context, pass };
353
354 cx.with_lint_attrs(check_node.id(), check_node.attrs(), |cx| check_node.check(cx));
335 let builtin_lints = crate::BuiltinCombinedEarlyLintPass::new();
336 let passes = &lint_store.early_passes;
337 run_passes(check_node, context, builtin_lints, passes)
338 };
355339
356340 // All of the buffered lints should have been emitted at this point.
357341 // If not, that means that we somehow buffered a lint for a node id
358342 // that was not lint-checked (perhaps it doesn't exist?). This is a bug.
359 for (id, lints) in cx.context.buffered.map {
343 for (id, lints) in context.buffered.map {
360344 if !lints.is_empty() {
361345 assert!(
362346 sess.dcx().has_errors().is_some(),
......@@ -367,3 +351,32 @@ fn check_ast_node_inner<'a, T: EarlyLintPass>(
367351 }
368352 }
369353}
354
355fn run_passes<'a, 'ecx, T: EarlyLintPass + 'static>(
356 check_node: EarlyCheckNode<'a>,
357 context: EarlyContext<'ecx>,
358 builtin_lints: T,
359 passes: &[EarlyLintPassFactory],
360) -> EarlyContext<'ecx> {
361 // Note: `passes` is often empty. In that case, it's faster to run
362 // `builtin_lints` directly rather than bundling it up into the
363 // `RuntimeCombinedEarlyLintPass`.
364 if passes.is_empty() {
365 run_pass(check_node, context, builtin_lints)
366 } else {
367 let mut passes: Vec<_> = passes.iter().map(|mk_pass| mk_pass()).collect();
368 passes.push(Box::new(builtin_lints));
369 let pass = RuntimeCombinedEarlyLintPass { passes };
370 run_pass(check_node, context, pass)
371 }
372}
373
374fn run_pass<'a, 'ecx, T: EarlyLintPass>(
375 check_node: EarlyCheckNode<'a>,
376 context: EarlyContext<'ecx>,
377 pass: T,
378) -> EarlyContext<'ecx> {
379 let mut cx = EarlyContextAndPass { context, pass };
380 cx.with_lint_attrs(check_node.id(), check_node.attrs(), |cx| check_node.check(cx));
381 cx.context
382}
compiler/rustc_lint/src/if_let_rescope.rs+1-1
......@@ -269,7 +269,7 @@ impl_lint_pass!(
269269impl<'tcx> LateLintPass<'tcx> for IfLetRescope {
270270 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
271271 if expr.span.edition().at_least_rust_2024()
272 || cx.tcx.lints_that_dont_need_to_run(()).contains(&LintId::of(IF_LET_RESCOPE))
272 || cx.tcx.skippable_lints(()).contains(&LintId::of(IF_LET_RESCOPE))
273273 {
274274 return;
275275 }
compiler/rustc_lint/src/late.rs+22-30
......@@ -18,7 +18,7 @@ use rustc_span::Span;
1818use tracing::debug;
1919
2020use crate::passes::LateLintPassObject;
21use crate::{LateContext, LateLintPass, LintId, LintStore};
21use crate::{LateContext, LateLintPass, LintStore, is_lint_pass_required};
2222
2323/// Extract the [`LintStore`] from [`Session`].
2424///
......@@ -349,31 +349,26 @@ pub fn late_lint_mod<'tcx, T: LateLintPass<'tcx> + 'tcx>(
349349 only_module: true,
350350 };
351351
352 let skippable_lints = tcx.skippable_lints(());
353
352354 // Note: `passes` is often empty. In that case, it's faster to run
353355 // `builtin_lints` directly rather than bundling it up into the
354356 // `RuntimeCombinedLateLintPass`.
355 let store = unerased_lint_store(tcx.sess);
356
357 if store.late_module_passes.is_empty() {
358 // If all builtin lints can be skipped, there is no point in running `late_lint_mod_inner`
359 // at all. This happens often for dependencies built with `--cap-lints=allow`.
360 let dont_need_to_run = tcx.lints_that_dont_need_to_run(());
361 let can_skip_lints = builtin_lints
362 .get_lints()
363 .iter()
364 .all(|lint| dont_need_to_run.contains(&LintId::of(lint)));
365 if !can_skip_lints {
357 let mut passes: Vec<_> = unerased_lint_store(tcx.sess)
358 .late_module_passes
359 .iter()
360 .map(|mk_pass| mk_pass(tcx))
361 .filter(|pass| is_lint_pass_required(skippable_lints, &pass.get_lints()))
362 .collect();
363 let builtin_lints_must_run = is_lint_pass_required(skippable_lints, &builtin_lints.get_lints());
364 if passes.is_empty() {
365 if builtin_lints_must_run {
366366 late_lint_mod_inner(tcx, module_def_id, context, builtin_lints);
367367 }
368368 } else {
369 let builtin_lints = Box::new(builtin_lints) as Box<dyn LateLintPass<'tcx>>;
370 let passes = store
371 .late_module_passes
372 .iter()
373 .map(|mk_pass| (mk_pass)(tcx))
374 .chain(std::iter::once(builtin_lints))
375 .collect::<Vec<_>>();
376
369 if builtin_lints_must_run {
370 passes.push(Box::new(builtin_lints) as Box<dyn LateLintPass<'tcx>>);
371 }
377372 let pass = RuntimeCombinedLateLintPass { passes };
378373 late_lint_mod_inner(tcx, module_def_id, context, pass);
379374 }
......@@ -404,18 +399,15 @@ fn late_lint_mod_inner<'tcx, T: LateLintPass<'tcx>>(
404399}
405400
406401fn late_lint_crate<'tcx>(tcx: TyCtxt<'tcx>) {
407 let lints_that_dont_need_to_run = tcx.lints_that_dont_need_to_run(());
402 let skippable_lints = tcx.skippable_lints(());
408403
409404 // Note: `passes` is often empty after filtering.
410 let mut passes: Vec<_> =
411 unerased_lint_store(tcx.sess).late_passes.iter().map(|mk_pass| (mk_pass)(tcx)).collect();
412 passes.retain(|pass| {
413 let lints = pass.get_lints();
414 // Lintless passes are always in
415 lints.is_empty() ||
416 // If the pass doesn't have a single needed lint, omit it
417 !lints.iter().all(|lint| lints_that_dont_need_to_run.contains(&LintId::of(lint)))
418 });
405 let passes: Vec<_> = unerased_lint_store(tcx.sess)
406 .late_passes
407 .iter()
408 .map(|mk_pass| mk_pass(tcx))
409 .filter(|pass| is_lint_pass_required(skippable_lints, &pass.get_lints()))
410 .collect();
419411 if passes.is_empty() {
420412 return;
421413 }
compiler/rustc_lint/src/levels.rs+5-5
......@@ -114,11 +114,11 @@ impl LintLevelSets {
114114 }
115115}
116116
117fn lints_that_dont_need_to_run(tcx: TyCtxt<'_>, (): ()) -> UnordSet<LintId> {
117fn skippable_lints(tcx: TyCtxt<'_>, (): ()) -> UnordSet<LintId> {
118118 let store = unerased_lint_store(&tcx.sess);
119119 let root_map = tcx.shallow_lint_levels_on(hir::CRATE_OWNER_ID);
120120
121 let mut dont_need_to_run: FxHashSet<LintId> = store
121 let mut skippable: FxHashSet<LintId> = store
122122 .get_lints()
123123 .into_iter()
124124 .filter(|lint| {
......@@ -145,13 +145,13 @@ fn lints_that_dont_need_to_run(tcx: TyCtxt<'_>, (): ()) -> UnordSet<LintId> {
145145 for (_, specs) in map.specs.iter() {
146146 for (lint, level_spec) in specs.iter() {
147147 if !level_spec.is_allow() {
148 dont_need_to_run.remove(lint);
148 skippable.remove(lint);
149149 }
150150 }
151151 }
152152 }
153153
154 dont_need_to_run.into()
154 skippable.into()
155155}
156156
157157#[instrument(level = "trace", skip(tcx), ret)]
......@@ -1035,7 +1035,7 @@ where
10351035}
10361036
10371037pub(crate) fn provide(providers: &mut Providers) {
1038 *providers = Providers { shallow_lint_levels_on, lints_that_dont_need_to_run, ..*providers };
1038 *providers = Providers { shallow_lint_levels_on, skippable_lints, ..*providers };
10391039}
10401040
10411041pub(crate) fn parse_lint_and_tool_name(lint_name: &str) -> (Option<Symbol>, &str) {
compiler/rustc_lint/src/lib.rs+17
......@@ -117,6 +117,7 @@ use precedence::*;
117117use ptr_nulls::*;
118118use redundant_semicolon::*;
119119use reference_casting::*;
120use rustc_data_structures::unord::UnordSet;
120121use rustc_hir::def_id::LocalModDefId;
121122use rustc_middle::query::Providers;
122123use rustc_middle::ty::TyCtxt;
......@@ -742,5 +743,21 @@ fn register_internals(store: &mut LintStore) {
742743 );
743744}
744745
746/// Is a pass (which contains `lints`) required to run? Maybe not, e.g. for dependencies built with
747/// `--cap-lints=allow`.
748///
749/// Note: this is a conservative estimate intended for optimization purposes. It might return
750/// `true` for a pass that need not run, but it will never return `false` for a pass that must run.
751pub fn is_lint_pass_required(skippable: &UnordSet<LintId>, lints: &LintVec) -> bool {
752 // A pass without any lints? Clippy sometimes does this, to collect things while traversing.
753 // Such a pass must always run.
754 if lints.is_empty() {
755 return true;
756 }
757
758 // Otherwise, the pass must run unless all lints within are skippable.
759 !lints.iter().all(|lint| skippable.contains(&LintId::of(lint)))
760}
761
745762#[cfg(test)]
746763mod tests;
compiler/rustc_middle/src/queries.rs+1-1
......@@ -541,7 +541,7 @@ rustc_queries! {
541541 desc { "computing `#[expect]`ed lints in this crate" }
542542 }
543543
544 query lints_that_dont_need_to_run(_: ()) -> &'tcx UnordSet<LintId> {
544 query skippable_lints(_: ()) -> &'tcx UnordSet<LintId> {
545545 arena_cache
546546 // This depends on the lint store, which includes internal lints when the
547547 // untracked `-Zunstable-options` flag is set.
compiler/rustc_mir_transform/src/lint_tail_expr_drop_order.rs+1-1
......@@ -187,7 +187,7 @@ pub(crate) fn run_lint<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId, body: &Body<
187187 return;
188188 }
189189 if body.span.edition().at_least_rust_2024()
190 || tcx.lints_that_dont_need_to_run(()).contains(&lint::LintId::of(TAIL_EXPR_DROP_ORDER))
190 || tcx.skippable_lints(()).contains(&lint::LintId::of(TAIL_EXPR_DROP_ORDER))
191191 {
192192 return;
193193 }
src/tools/clippy/clippy_lints/src/combined_early_pass.rs+1-1
......@@ -11,7 +11,7 @@
1111//! inlined calls. No vtable, no per-node dynamic dispatch.
1212//!
1313//! Unlike the late combine there is no `active` gate. rustc drops fully-disabled
14//! late passes via `lints_that_dont_need_to_run`, but the early pass runner has
14//! late passes via `skippable_lints`, but the early pass runner has
1515//! no such filtering, so a plain forward is equivalent and loses nothing.
1616//!
1717//! [`combined_late_pass`]: crate::combined_late_pass
src/tools/clippy/clippy_lints/src/lib.rs+3-8
......@@ -414,7 +414,7 @@ mod zombie_processes;
414414use clippy_config::{Conf, get_configuration_metadata, sanitize_explanation};
415415use clippy_utils::macros::FormatArgsStorage;
416416use rustc_data_structures::fx::FxHashSet;
417use rustc_lint::Lint;
417use rustc_lint::{Lint, is_lint_pass_required};
418418use rustc_middle::ty::TyCtxt;
419419use utils::attr_collector::AttrStorage;
420420
......@@ -470,13 +470,8 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
470470 }
471471
472472 store.late_passes.push(Box::new(move |tcx: TyCtxt<'_>| {
473 let dont_need = tcx.lints_that_dont_need_to_run(());
474 let is_active = |lints: &rustc_lint::LintVec| {
475 lints.is_empty()
476 || !lints
477 .iter()
478 .all(|lint| dont_need.contains(&rustc_lint::LintId::of(lint)))
479 };
473 let skippable_lints = tcx.skippable_lints(());
474 let is_active = |lints: &rustc_lint::LintVec| is_lint_pass_required(skippable_lints, lints);
480475 Box::new(CombinedLateLintPass::new(
481476 tcx,
482477 conf,