| author | bors <bors@rust-lang.org> 2026-06-25 08:22:49 UTC |
| committer | bors <bors@rust-lang.org> 2026-06-25 08:22:49 UTC |
| log | 973ad0d0ab149bde2e96422833c1265c7a5be217 |
| tree | acaa8bb97cc8b9695f7c0f66a378da0c65c6ff19 |
| parent | 73100eefe2afb831c4964f579c95beeb27b86e28 |
| parent | 2724e23a54c66ce397c6028c0645ce51ad146e36 |
Rework lint pass running
Some cleanups relating to the running of lint passes.
r? @Urgau12 files changed, 93 insertions(+), 77 deletions(-)
compiler/rustc_hir_analysis/src/check/region.rs+1-1| ... | ... | @@ -148,7 +148,7 @@ fn resolve_block<'tcx>( |
| 148 | 148 | if !terminating |
| 149 | 149 | && !visitor |
| 150 | 150 | .tcx |
| 151 | .lints_that_dont_need_to_run(()) | |
| 151 | .skippable_lints(()) | |
| 152 | 152 | .contains(&lint::LintId::of(lint::builtin::TAIL_EXPR_DROP_ORDER)) |
| 153 | 153 | { |
| 154 | 154 | // 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>( |
| 101 | 101 | lint_store, |
| 102 | 102 | registered_tools, |
| 103 | 103 | None, |
| 104 | rustc_lint::BuiltinCombinedPreExpansionLintPass::new(), | |
| 105 | 104 | check_node, |
| 106 | 105 | ); |
| 107 | 106 | }, |
| ... | ... | @@ -479,7 +478,6 @@ fn early_lint_checks(tcx: TyCtxt<'_>, (): ()) { |
| 479 | 478 | lint_store, |
| 480 | 479 | tcx.registered_tools(()), |
| 481 | 480 | Some(lint_buffer), |
| 482 | rustc_lint::BuiltinCombinedEarlyLintPass::new(), | |
| 483 | 481 | EarlyCheckNode::CrateRoot(&*krate, &*krate.attrs), |
| 484 | 482 | ) |
| 485 | 483 | } |
compiler/rustc_lint/src/context.rs+2-1| ... | ... | @@ -40,7 +40,8 @@ use self::TargetLint::*; |
| 40 | 40 | use crate::levels::LintLevelsBuilder; |
| 41 | 41 | use crate::passes::{EarlyLintPassObject, LateLintPassObject}; |
| 42 | 42 | |
| 43 | type EarlyLintPassFactory = Box<dyn Fn() -> EarlyLintPassObject + sync::DynSend + sync::DynSync>; | |
| 43 | pub(crate) type EarlyLintPassFactory = | |
| 44 | Box<dyn Fn() -> EarlyLintPassObject + sync::DynSend + sync::DynSync>; | |
| 44 | 45 | type LateLintPassFactory = |
| 45 | 46 | Box<dyn for<'tcx> Fn(TyCtxt<'tcx>) -> LateLintPassObject<'tcx> + sync::DynSend + sync::DynSync>; |
| 46 | 47 |
compiler/rustc_lint/src/early.rs+39-26| ... | ... | @@ -16,7 +16,7 @@ use rustc_span::{Ident, Span}; |
| 16 | 16 | use tracing::debug; |
| 17 | 17 | |
| 18 | 18 | use crate::DiagAndSess; |
| 19 | use crate::context::{EarlyContext, LintContext, LintStore}; | |
| 19 | use crate::context::{EarlyContext, EarlyLintPassFactory, LintContext, LintStore}; | |
| 20 | 20 | use crate::passes::{EarlyLintPass, EarlyLintPassObject}; |
| 21 | 21 | |
| 22 | 22 | pub(super) mod diagnostics; |
| ... | ... | @@ -316,7 +316,6 @@ pub fn check_ast_node<'a>( |
| 316 | 316 | lint_store: &LintStore, |
| 317 | 317 | registered_tools: &RegisteredTools, |
| 318 | 318 | lint_buffer: Option<LintBuffer>, |
| 319 | builtin_lints: impl EarlyLintPass + 'static, | |
| 320 | 319 | check_node: EarlyCheckNode<'a>, |
| 321 | 320 | ) { |
| 322 | 321 | let context = EarlyContext::new( |
| ... | ... | @@ -328,35 +327,20 @@ pub fn check_ast_node<'a>( |
| 328 | 327 | lint_buffer.unwrap_or_default(), |
| 329 | 328 | ); |
| 330 | 329 | |
| 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) | |
| 338 | 334 | } 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 | ||
| 346 | fn 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 | }; | |
| 355 | 339 | |
| 356 | 340 | // All of the buffered lints should have been emitted at this point. |
| 357 | 341 | // If not, that means that we somehow buffered a lint for a node id |
| 358 | 342 | // 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 { | |
| 360 | 344 | if !lints.is_empty() { |
| 361 | 345 | assert!( |
| 362 | 346 | sess.dcx().has_errors().is_some(), |
| ... | ... | @@ -367,3 +351,32 @@ fn check_ast_node_inner<'a, T: EarlyLintPass>( |
| 367 | 351 | } |
| 368 | 352 | } |
| 369 | 353 | } |
| 354 | ||
| 355 | fn 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 | ||
| 374 | fn 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!( |
| 269 | 269 | impl<'tcx> LateLintPass<'tcx> for IfLetRescope { |
| 270 | 270 | fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) { |
| 271 | 271 | 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)) | |
| 273 | 273 | { |
| 274 | 274 | return; |
| 275 | 275 | } |
compiler/rustc_lint/src/late.rs+22-30| ... | ... | @@ -18,7 +18,7 @@ use rustc_span::Span; |
| 18 | 18 | use tracing::debug; |
| 19 | 19 | |
| 20 | 20 | use crate::passes::LateLintPassObject; |
| 21 | use crate::{LateContext, LateLintPass, LintId, LintStore}; | |
| 21 | use crate::{LateContext, LateLintPass, LintStore, is_lint_pass_required}; | |
| 22 | 22 | |
| 23 | 23 | /// Extract the [`LintStore`] from [`Session`]. |
| 24 | 24 | /// |
| ... | ... | @@ -349,31 +349,26 @@ pub fn late_lint_mod<'tcx, T: LateLintPass<'tcx> + 'tcx>( |
| 349 | 349 | only_module: true, |
| 350 | 350 | }; |
| 351 | 351 | |
| 352 | let skippable_lints = tcx.skippable_lints(()); | |
| 353 | ||
| 352 | 354 | // Note: `passes` is often empty. In that case, it's faster to run |
| 353 | 355 | // `builtin_lints` directly rather than bundling it up into the |
| 354 | 356 | // `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 { | |
| 366 | 366 | late_lint_mod_inner(tcx, module_def_id, context, builtin_lints); |
| 367 | 367 | } |
| 368 | 368 | } 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 | } | |
| 377 | 372 | let pass = RuntimeCombinedLateLintPass { passes }; |
| 378 | 373 | late_lint_mod_inner(tcx, module_def_id, context, pass); |
| 379 | 374 | } |
| ... | ... | @@ -404,18 +399,15 @@ fn late_lint_mod_inner<'tcx, T: LateLintPass<'tcx>>( |
| 404 | 399 | } |
| 405 | 400 | |
| 406 | 401 | fn 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(()); | |
| 408 | 403 | |
| 409 | 404 | // 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(); | |
| 419 | 411 | if passes.is_empty() { |
| 420 | 412 | return; |
| 421 | 413 | } |
compiler/rustc_lint/src/levels.rs+5-5| ... | ... | @@ -114,11 +114,11 @@ impl LintLevelSets { |
| 114 | 114 | } |
| 115 | 115 | } |
| 116 | 116 | |
| 117 | fn lints_that_dont_need_to_run(tcx: TyCtxt<'_>, (): ()) -> UnordSet<LintId> { | |
| 117 | fn skippable_lints(tcx: TyCtxt<'_>, (): ()) -> UnordSet<LintId> { | |
| 118 | 118 | let store = unerased_lint_store(&tcx.sess); |
| 119 | 119 | let root_map = tcx.shallow_lint_levels_on(hir::CRATE_OWNER_ID); |
| 120 | 120 | |
| 121 | let mut dont_need_to_run: FxHashSet<LintId> = store | |
| 121 | let mut skippable: FxHashSet<LintId> = store | |
| 122 | 122 | .get_lints() |
| 123 | 123 | .into_iter() |
| 124 | 124 | .filter(|lint| { |
| ... | ... | @@ -145,13 +145,13 @@ fn lints_that_dont_need_to_run(tcx: TyCtxt<'_>, (): ()) -> UnordSet<LintId> { |
| 145 | 145 | for (_, specs) in map.specs.iter() { |
| 146 | 146 | for (lint, level_spec) in specs.iter() { |
| 147 | 147 | if !level_spec.is_allow() { |
| 148 | dont_need_to_run.remove(lint); | |
| 148 | skippable.remove(lint); | |
| 149 | 149 | } |
| 150 | 150 | } |
| 151 | 151 | } |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | dont_need_to_run.into() | |
| 154 | skippable.into() | |
| 155 | 155 | } |
| 156 | 156 | |
| 157 | 157 | #[instrument(level = "trace", skip(tcx), ret)] |
| ... | ... | @@ -1035,7 +1035,7 @@ where |
| 1035 | 1035 | } |
| 1036 | 1036 | |
| 1037 | 1037 | pub(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 }; | |
| 1039 | 1039 | } |
| 1040 | 1040 | |
| 1041 | 1041 | pub(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::*; |
| 117 | 117 | use ptr_nulls::*; |
| 118 | 118 | use redundant_semicolon::*; |
| 119 | 119 | use reference_casting::*; |
| 120 | use rustc_data_structures::unord::UnordSet; | |
| 120 | 121 | use rustc_hir::def_id::LocalModDefId; |
| 121 | 122 | use rustc_middle::query::Providers; |
| 122 | 123 | use rustc_middle::ty::TyCtxt; |
| ... | ... | @@ -742,5 +743,21 @@ fn register_internals(store: &mut LintStore) { |
| 742 | 743 | ); |
| 743 | 744 | } |
| 744 | 745 | |
| 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. | |
| 751 | pub 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 | ||
| 745 | 762 | #[cfg(test)] |
| 746 | 763 | mod tests; |
compiler/rustc_middle/src/queries.rs+1-1| ... | ... | @@ -541,7 +541,7 @@ rustc_queries! { |
| 541 | 541 | desc { "computing `#[expect]`ed lints in this crate" } |
| 542 | 542 | } |
| 543 | 543 | |
| 544 | query lints_that_dont_need_to_run(_: ()) -> &'tcx UnordSet<LintId> { | |
| 544 | query skippable_lints(_: ()) -> &'tcx UnordSet<LintId> { | |
| 545 | 545 | arena_cache |
| 546 | 546 | // This depends on the lint store, which includes internal lints when the |
| 547 | 547 | // 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< |
| 187 | 187 | return; |
| 188 | 188 | } |
| 189 | 189 | 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)) | |
| 191 | 191 | { |
| 192 | 192 | return; |
| 193 | 193 | } |
src/tools/clippy/clippy_lints/src/combined_early_pass.rs+1-1| ... | ... | @@ -11,7 +11,7 @@ |
| 11 | 11 | //! inlined calls. No vtable, no per-node dynamic dispatch. |
| 12 | 12 | //! |
| 13 | 13 | //! 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 | |
| 15 | 15 | //! no such filtering, so a plain forward is equivalent and loses nothing. |
| 16 | 16 | //! |
| 17 | 17 | //! [`combined_late_pass`]: crate::combined_late_pass |
src/tools/clippy/clippy_lints/src/lib.rs+3-8| ... | ... | @@ -414,7 +414,7 @@ mod zombie_processes; |
| 414 | 414 | use clippy_config::{Conf, get_configuration_metadata, sanitize_explanation}; |
| 415 | 415 | use clippy_utils::macros::FormatArgsStorage; |
| 416 | 416 | use rustc_data_structures::fx::FxHashSet; |
| 417 | use rustc_lint::Lint; | |
| 417 | use rustc_lint::{Lint, is_lint_pass_required}; | |
| 418 | 418 | use rustc_middle::ty::TyCtxt; |
| 419 | 419 | use utils::attr_collector::AttrStorage; |
| 420 | 420 | |
| ... | ... | @@ -470,13 +470,8 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co |
| 470 | 470 | } |
| 471 | 471 | |
| 472 | 472 | 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); | |
| 480 | 475 | Box::new(CombinedLateLintPass::new( |
| 481 | 476 | tcx, |
| 482 | 477 | conf, |