From faa7966a5bafb7e694e56126dd39ed8faf13d09d Mon Sep 17 00:00:00 2001 From: mu001999 Date: Thu, 26 Mar 2026 00:08:02 +0800 Subject: [PATCH 01/12] fix #[expect(dead_code)] liveness propagation --- compiler/rustc_passes/src/dead.rs | 162 ++++++++++++++---- .../lint/dead-code/expect-dead-code-152370.rs | 13 ++ .../lint/dead-code/expect-dead-code-154324.rs | 9 + .../expect-dead-code-field-read-in-dead-fn.rs | 19 ++ ...xpect-dead-code-through-non-expect-item.rs | 18 ++ 5 files changed, 186 insertions(+), 35 deletions(-) create mode 100644 tests/ui/lint/dead-code/expect-dead-code-152370.rs create mode 100644 tests/ui/lint/dead-code/expect-dead-code-154324.rs create mode 100644 tests/ui/lint/dead-code/expect-dead-code-field-read-in-dead-fn.rs create mode 100644 tests/ui/lint/dead-code/expect-dead-code-through-non-expect-item.rs diff --git a/compiler/rustc_passes/src/dead.rs b/compiler/rustc_passes/src/dead.rs index 30634c800e819..e53f9c6fe4f36 100644 --- a/compiler/rustc_passes/src/dead.rs +++ b/compiler/rustc_passes/src/dead.rs @@ -8,7 +8,7 @@ use std::ops::ControlFlow; use hir::def_id::{LocalDefIdMap, LocalDefIdSet}; use rustc_abi::FieldIdx; -use rustc_data_structures::fx::FxIndexSet; +use rustc_data_structures::fx::{FxHashSet, FxIndexSet}; use rustc_errors::{ErrorGuaranteed, MultiSpan}; use rustc_hir::def::{CtorOf, DefKind, Res}; use rustc_hir::def_id::{DefId, LocalDefId, LocalModDefId}; @@ -75,11 +75,46 @@ enum ComesFromAllowExpect { No, } +/// Carries both the propagated `allow/expect` context and the current item's +/// own `allow/expect` status. +/// +/// For example: +/// +/// ```rust +/// #[expect(dead_code)] +/// fn root() { middle() } +/// +/// fn middle() { leaf() } +/// +/// #[expect(dead_code)] +/// fn leaf() {} +/// ``` +/// +/// The seed for `root` starts as `propagated = Yes, own = Yes`. +/// +/// When `root` reaches `middle`, the propagated context stays `Yes`, but +/// `middle` itself does not have `#[allow(dead_code)]` or `#[expect(dead_code)]`, +/// so its work item becomes `propagated = Yes, own = No`. +/// +/// When `middle` reaches `leaf`, that same propagated `Yes` context is preserved, +/// and since `leaf` itself has `#[expect(dead_code)]`, its work item becomes +/// `propagated = Yes, own = Yes`. +/// +/// In general, `propagated` controls whether descendants are still explored +/// under an `allow/expect` context, while `own` controls whether the current +/// item itself should be excluded from `live_symbols`. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +struct WorkItem { + id: LocalDefId, + propagated: ComesFromAllowExpect, + own: ComesFromAllowExpect, +} + struct MarkSymbolVisitor<'tcx> { - worklist: Vec<(LocalDefId, ComesFromAllowExpect)>, + worklist: Vec, tcx: TyCtxt<'tcx>, maybe_typeck_results: Option<&'tcx ty::TypeckResults<'tcx>>, - scanned: LocalDefIdSet, + scanned: FxHashSet<(LocalDefId, ComesFromAllowExpect)>, live_symbols: LocalDefIdSet, repr_unconditionally_treats_fields_as_live: bool, repr_has_repr_simd: bool, @@ -89,6 +124,7 @@ struct MarkSymbolVisitor<'tcx> { // and the span of their respective impl (i.e., part of the derive // macro) ignored_derived_traits: LocalDefIdMap>, + propagated_comes_from_allow_expect: ComesFromAllowExpect, } impl<'tcx> MarkSymbolVisitor<'tcx> { @@ -101,19 +137,45 @@ impl<'tcx> MarkSymbolVisitor<'tcx> { .expect("`MarkSymbolVisitor::typeck_results` called outside of body") } + /// Returns whether `def_id` itself should be treated as coming from + /// `#[allow(dead_code)]` or `#[expect(dead_code)]` in the current + /// propagated work-item context. + fn own_comes_from_allow_expect(&self, def_id: LocalDefId) -> ComesFromAllowExpect { + if self.propagated_comes_from_allow_expect == ComesFromAllowExpect::Yes + && let Some(ComesFromAllowExpect::Yes) = + has_allow_dead_code_or_lang_attr(self.tcx, def_id) + { + ComesFromAllowExpect::Yes + } else { + ComesFromAllowExpect::No + } + } + fn check_def_id(&mut self, def_id: DefId) { if let Some(def_id) = def_id.as_local() { + let own_comes_from_allow_expect = self.own_comes_from_allow_expect(def_id); + if should_explore(self.tcx, def_id) { - self.worklist.push((def_id, ComesFromAllowExpect::No)); + self.worklist.push(WorkItem { + id: def_id, + propagated: self.propagated_comes_from_allow_expect, + own: own_comes_from_allow_expect, + }); + } + + if own_comes_from_allow_expect == ComesFromAllowExpect::No { + self.live_symbols.insert(def_id); } - self.live_symbols.insert(def_id); } } fn insert_def_id(&mut self, def_id: DefId) { if let Some(def_id) = def_id.as_local() { debug_assert!(!should_explore(self.tcx, def_id)); - self.live_symbols.insert(def_id); + + if self.own_comes_from_allow_expect(def_id) == ComesFromAllowExpect::No { + self.live_symbols.insert(def_id); + } } } @@ -323,7 +385,8 @@ impl<'tcx> MarkSymbolVisitor<'tcx> { fn mark_live_symbols(&mut self) -> as Visitor<'tcx>>::Result { while let Some(work) = self.worklist.pop() { - let (mut id, comes_from_allow_expect) = work; + let WorkItem { mut id, propagated, own } = work; + self.propagated_comes_from_allow_expect = propagated; // in the case of tuple struct constructors we want to check the item, // not the generated tuple struct constructor function @@ -352,14 +415,14 @@ impl<'tcx> MarkSymbolVisitor<'tcx> { // this "duplication" is essential as otherwise a function with `#[expect]` // called from a `pub fn` may be falsely reported as not live, falsely // triggering the `unfulfilled_lint_expectations` lint. - match comes_from_allow_expect { + match own { ComesFromAllowExpect::Yes => {} ComesFromAllowExpect::No => { self.live_symbols.insert(id); } } - if !self.scanned.insert(id) { + if !self.scanned.insert((id, propagated)) { continue; } @@ -692,7 +755,11 @@ impl<'tcx> Visitor<'tcx> for MarkSymbolVisitor<'tcx> { ) .and_then(|item| item.def_id.as_local()) { - self.worklist.push((local_def_id, ComesFromAllowExpect::No)); + self.worklist.push(WorkItem { + id: local_def_id, + propagated: ComesFromAllowExpect::No, + own: ComesFromAllowExpect::No, + }); } } } @@ -750,23 +817,27 @@ fn has_allow_dead_code_or_lang_attr( fn maybe_record_as_seed<'tcx>( tcx: TyCtxt<'tcx>, owner_id: hir::OwnerId, - worklist: &mut Vec<(LocalDefId, ComesFromAllowExpect)>, + worklist: &mut Vec, unsolved_items: &mut Vec, ) { let allow_dead_code = has_allow_dead_code_or_lang_attr(tcx, owner_id.def_id); if let Some(comes_from_allow) = allow_dead_code { - worklist.push((owner_id.def_id, comes_from_allow)); + worklist.push(WorkItem { + id: owner_id.def_id, + propagated: comes_from_allow, + own: comes_from_allow, + }); } match tcx.def_kind(owner_id) { DefKind::Enum => { if let Some(comes_from_allow) = allow_dead_code { let adt = tcx.adt_def(owner_id); - worklist.extend( - adt.variants() - .iter() - .map(|variant| (variant.def_id.expect_local(), comes_from_allow)), - ); + worklist.extend(adt.variants().iter().map(|variant| WorkItem { + id: variant.def_id.expect_local(), + propagated: comes_from_allow, + own: comes_from_allow, + })); } } DefKind::AssocFn | DefKind::AssocConst { .. } | DefKind::AssocTy => { @@ -781,7 +852,11 @@ fn maybe_record_as_seed<'tcx>( && let Some(comes_from_allow) = has_allow_dead_code_or_lang_attr(tcx, trait_item_local_def_id) { - worklist.push((owner_id.def_id, comes_from_allow)); + worklist.push(WorkItem { + id: owner_id.def_id, + propagated: comes_from_allow, + own: comes_from_allow, + }); } // We only care about associated items of traits, @@ -802,7 +877,11 @@ fn maybe_record_as_seed<'tcx>( && let Some(comes_from_allow) = has_allow_dead_code_or_lang_attr(tcx, trait_def_id) { - worklist.push((owner_id.def_id, comes_from_allow)); + worklist.push(WorkItem { + id: owner_id.def_id, + propagated: comes_from_allow, + own: comes_from_allow, + }); } unsolved_items.push(owner_id.def_id); @@ -810,38 +889,48 @@ fn maybe_record_as_seed<'tcx>( } DefKind::GlobalAsm => { // global_asm! is always live. - worklist.push((owner_id.def_id, ComesFromAllowExpect::No)); + worklist.push(WorkItem { + id: owner_id.def_id, + propagated: ComesFromAllowExpect::No, + own: ComesFromAllowExpect::No, + }); } DefKind::Const { .. } => { if tcx.item_name(owner_id.def_id) == kw::Underscore { // `const _` is always live, as that syntax only exists for the side effects // of type checking and evaluating the constant expression, and marking them // as dead code would defeat that purpose. - worklist.push((owner_id.def_id, ComesFromAllowExpect::No)); + worklist.push(WorkItem { + id: owner_id.def_id, + propagated: ComesFromAllowExpect::No, + own: ComesFromAllowExpect::No, + }); } } _ => {} } } -fn create_and_seed_worklist( - tcx: TyCtxt<'_>, -) -> (Vec<(LocalDefId, ComesFromAllowExpect)>, Vec) { +fn create_and_seed_worklist(tcx: TyCtxt<'_>) -> (Vec, Vec) { let effective_visibilities = &tcx.effective_visibilities(()); let mut unsolved_impl_item = Vec::new(); let mut worklist = effective_visibilities .iter() .filter_map(|(&id, effective_vis)| { - effective_vis - .is_public_at_level(Level::Reachable) - .then_some(id) - .map(|id| (id, ComesFromAllowExpect::No)) + effective_vis.is_public_at_level(Level::Reachable).then_some(id).map(|id| WorkItem { + id, + propagated: ComesFromAllowExpect::No, + own: ComesFromAllowExpect::No, + }) }) // Seed entry point - .chain( - tcx.entry_fn(()) - .and_then(|(def_id, _)| def_id.as_local().map(|id| (id, ComesFromAllowExpect::No))), - ) + .chain(tcx.entry_fn(()).and_then(|(def_id, _)| { + def_id.as_local().map(|id| WorkItem { + id, + propagated: ComesFromAllowExpect::No, + own: ComesFromAllowExpect::No, + }) + })) .collect::>(); let crate_items = tcx.hir_crate_items(()); @@ -868,6 +957,7 @@ fn live_symbols_and_ignored_derived_traits( in_pat: false, ignore_variant_stack: vec![], ignored_derived_traits: Default::default(), + propagated_comes_from_allow_expect: ComesFromAllowExpect::No, }; if let ControlFlow::Break(guar) = symbol_visitor.mark_live_symbols() { return Err(guar); @@ -882,9 +972,11 @@ fn live_symbols_and_ignored_derived_traits( .collect(); while !items_to_check.is_empty() { - symbol_visitor - .worklist - .extend(items_to_check.drain(..).map(|id| (id, ComesFromAllowExpect::No))); + symbol_visitor.worklist.extend(items_to_check.drain(..).map(|id| WorkItem { + id, + propagated: ComesFromAllowExpect::No, + own: ComesFromAllowExpect::No, + })); if let ControlFlow::Break(guar) = symbol_visitor.mark_live_symbols() { return Err(guar); } diff --git a/tests/ui/lint/dead-code/expect-dead-code-152370.rs b/tests/ui/lint/dead-code/expect-dead-code-152370.rs new file mode 100644 index 0000000000000..d33d9fb713b00 --- /dev/null +++ b/tests/ui/lint/dead-code/expect-dead-code-152370.rs @@ -0,0 +1,13 @@ +//@ check-pass + +#[expect(unused)] +trait UnusedTrait {} + +struct UsedStruct(u32); + +impl UnusedTrait for UsedStruct {} + +fn main() { + let x = UsedStruct(12); + println!("Hello World! {}", x.0); +} diff --git a/tests/ui/lint/dead-code/expect-dead-code-154324.rs b/tests/ui/lint/dead-code/expect-dead-code-154324.rs new file mode 100644 index 0000000000000..1ee2965fc3b07 --- /dev/null +++ b/tests/ui/lint/dead-code/expect-dead-code-154324.rs @@ -0,0 +1,9 @@ +//@ check-pass + +#![deny(dead_code, unfulfilled_lint_expectations, reason = "example")] +#![expect(dead_code, reason = "example")] + +struct Foo; +impl Foo {} + +fn main() {} diff --git a/tests/ui/lint/dead-code/expect-dead-code-field-read-in-dead-fn.rs b/tests/ui/lint/dead-code/expect-dead-code-field-read-in-dead-fn.rs new file mode 100644 index 0000000000000..1af0bf1645e2b --- /dev/null +++ b/tests/ui/lint/dead-code/expect-dead-code-field-read-in-dead-fn.rs @@ -0,0 +1,19 @@ +//@ check-pass + +#![deny(unfulfilled_lint_expectations)] +#![warn(dead_code)] + +struct Foo { + #[expect(dead_code)] + value: usize, +} + +#[expect(dead_code)] +fn dead_reads_field() { + let foo = Foo { value: 0 }; + let _ = foo.value; +} + +fn main() { + let _ = Foo { value: 0 }; +} diff --git a/tests/ui/lint/dead-code/expect-dead-code-through-non-expect-item.rs b/tests/ui/lint/dead-code/expect-dead-code-through-non-expect-item.rs new file mode 100644 index 0000000000000..84d805647d1b5 --- /dev/null +++ b/tests/ui/lint/dead-code/expect-dead-code-through-non-expect-item.rs @@ -0,0 +1,18 @@ +//@ check-pass + +#![deny(unfulfilled_lint_expectations)] +#![warn(dead_code)] + +#[expect(dead_code)] +fn root() { + middle(); +} + +fn middle() { + leaf(); +} + +#[expect(dead_code)] +fn leaf() {} + +fn main() {} From ae05a49f330abfa15de917792e4c112a84ef026d Mon Sep 17 00:00:00 2001 From: yukang Date: Tue, 13 Jan 2026 20:41:41 +0800 Subject: [PATCH 02/12] Fix wrong suggestion for returning async closure --- .../src/error_reporting/traits/suggestions.rs | 92 ++++++++++++++----- .../suggest-async-block-issue-140265.stderr | 7 +- .../const_param_ty_bad.stderr | 5 +- .../suggest-create-closure-issue-150701.fixed | 15 +++ .../suggest-create-closure-issue-150701.rs | 15 +++ ...suggest-create-closure-issue-150701.stderr | 52 +++++++++++ ...theses-to-call-closure-issue-145404.stderr | 5 +- 7 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 tests/ui/suggestions/suggest-create-closure-issue-150701.fixed create mode 100644 tests/ui/suggestions/suggest-create-closure-issue-150701.rs create mode 100644 tests/ui/suggestions/suggest-create-closure-issue-150701.stderr diff --git a/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs b/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs index 5a5d8af4ce144..03a46d73f7f7a 100644 --- a/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs +++ b/compiler/rustc_trait_selection/src/error_reporting/traits/suggestions.rs @@ -1034,17 +1034,25 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { && let ty::Tuple(inputs) = *sig.tupled_inputs_ty.kind() && inputs.is_empty() && self.tcx.is_lang_item(trait_pred.def_id(), LangItem::Future) + && let ObligationCauseCode::FunctionArg { arg_hir_id, .. } = obligation.cause.code() + && let hir::Node::Expr(hir::Expr { kind: hir::ExprKind::Closure(..), .. }) = + self.tcx.hir_node(*arg_hir_id) && let Some(hir::Node::Expr(hir::Expr { - kind: - hir::ExprKind::Closure(hir::Closure { - kind: hir::ClosureKind::CoroutineClosure(CoroutineDesugaring::Async), - fn_arg_span: Some(arg_span), - .. - }), - .. + kind: hir::ExprKind::Closure(closure), .. })) = self.tcx.hir_get_if_local(def_id) - && obligation.cause.span.contains(*arg_span) + && let hir::ClosureKind::CoroutineClosure(CoroutineDesugaring::Async) = closure.kind + && let Some(arg_span) = closure.fn_arg_span + && obligation.cause.span.contains(arg_span) { + let mut body = self.tcx.hir_body(closure.body).value; + let peeled = body.peel_blocks().peel_drop_temps(); + if let hir::ExprKind::Closure(inner) = peeled.kind { + body = self.tcx.hir_body(inner.body).value; + } + if !matches!(body.peel_blocks().peel_drop_temps().kind, hir::ExprKind::Block(..)) { + return false; + } + let sm = self.tcx.sess.source_map(); let removal_span = if let Ok(snippet) = sm.span_to_snippet(arg_span.with_hi(arg_span.hi() + rustc_span::BytePos(1))) @@ -1053,7 +1061,7 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { // There's a space after `||`, include it in the removal arg_span.with_hi(arg_span.hi() + rustc_span::BytePos(1)) } else { - *arg_span + arg_span }; err.span_suggestion_verbose( removal_span, @@ -1093,23 +1101,63 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> { .collect::>() .join(", "); - if matches!(obligation.cause.code(), ObligationCauseCode::FunctionArg { .. }) + if let ObligationCauseCode::FunctionArg { arg_hir_id, .. } = obligation.cause.code() && obligation.cause.span.can_be_used_for_suggestions() { - let (span, sugg) = if let Some(snippet) = - self.tcx.sess.source_map().span_to_snippet(obligation.cause.span).ok() - && snippet.starts_with("|") - { - (obligation.cause.span, format!("({snippet})({args})")) - } else { - (obligation.cause.span.shrink_to_hi(), format!("({args})")) + let span = obligation.cause.span; + + let arg_expr = match self.tcx.hir_node(*arg_hir_id) { + hir::Node::Expr(expr) => Some(expr), + _ => None, }; - // When the obligation error has been ensured to have been caused by - // an argument, the `obligation.cause.span` points at the expression - // of the argument, so we can provide a suggestion. Otherwise, we give - // a more general note. - err.span_suggestion_verbose(span, msg, sugg, Applicability::HasPlaceholders); + let is_closure_expr = + arg_expr.is_some_and(|expr| matches!(expr.kind, hir::ExprKind::Closure(..))); + + // If the user wrote `|| {}()`, suggesting to call the closure would produce `(|| {}())()`, + // which doesn't help and is often outright wrong. + if args.is_empty() + && let Some(expr) = arg_expr + && let hir::ExprKind::Closure(closure) = expr.kind + { + let mut body = self.tcx.hir_body(closure.body).value; + + // Async closures desugar to a closure returning a coroutine + if let hir::ClosureKind::CoroutineClosure(hir::CoroutineDesugaring::Async) = + closure.kind + { + let peeled = body.peel_blocks().peel_drop_temps(); + if let hir::ExprKind::Closure(inner) = peeled.kind { + body = self.tcx.hir_body(inner.body).value; + } + } + + let peeled_body = body.peel_blocks().peel_drop_temps(); + if let hir::ExprKind::Call(callee, call_args) = peeled_body.kind + && call_args.is_empty() + && let hir::ExprKind::Block(..) = callee.peel_blocks().peel_drop_temps().kind + { + return false; + } + } + + if is_closure_expr { + err.multipart_suggestions( + msg, + vec![vec![ + (span.shrink_to_lo(), "(".to_string()), + (span.shrink_to_hi(), format!(")({args})")), + ]], + Applicability::HasPlaceholders, + ); + } else { + err.span_suggestion_verbose( + span.shrink_to_hi(), + msg, + format!("({args})"), + Applicability::HasPlaceholders, + ); + } } else if let DefIdOrName::DefId(def_id) = def_id_or_name { let name = match self.tcx.hir_get_if_local(def_id) { Some(hir::Node::Expr(hir::Expr { diff --git a/tests/ui/async-await/async-closures/suggest-async-block-issue-140265.stderr b/tests/ui/async-await/async-closures/suggest-async-block-issue-140265.stderr index d81cfaac7f7d1..8e9e8ce1e7303 100644 --- a/tests/ui/async-await/async-closures/suggest-async-block-issue-140265.stderr +++ b/tests/ui/async-await/async-closures/suggest-async-block-issue-140265.stderr @@ -66,8 +66,11 @@ LL | fn takes_future(_fut: impl Future) {} | ^^^^^^^^^^^^^^^^^^^ required by this bound in `takes_future` help: use parentheses to call this closure | -LL | }(/* i32 */)); - | +++++++++++ +LL ~ takes_future((async |x: i32| { +LL | +LL | println!("{x}"); +LL ~ })(/* i32 */)); + | error: aborting due to 3 previous errors diff --git a/tests/ui/const-generics/adt_const_params/const_param_ty_bad.stderr b/tests/ui/const-generics/adt_const_params/const_param_ty_bad.stderr index be63c9e5c046b..0237e7bb5eeab 100644 --- a/tests/ui/const-generics/adt_const_params/const_param_ty_bad.stderr +++ b/tests/ui/const-generics/adt_const_params/const_param_ty_bad.stderr @@ -32,9 +32,8 @@ LL | fn check(_: impl std::marker::ConstParamTy_) {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `check` help: use parentheses to call this closure | -LL - check(|| {}); -LL + check((|| {})()); - | +LL | check((|| {})()); + | + +++ error[E0277]: `fn()` can't be used as a const parameter type --> $DIR/const_param_ty_bad.rs:9:11 diff --git a/tests/ui/suggestions/suggest-create-closure-issue-150701.fixed b/tests/ui/suggestions/suggest-create-closure-issue-150701.fixed new file mode 100644 index 0000000000000..f3e226c9e5681 --- /dev/null +++ b/tests/ui/suggestions/suggest-create-closure-issue-150701.fixed @@ -0,0 +1,15 @@ +// Regression test for #150701 + +//@ run-rustfix +//@ edition: 2024 + +use std::future::Future; + +fn f(_c: impl Future) {} + +fn main() { + f((async || {})()); //~ ERROR: expected function, found `()` + //~^ ERROR: is not a future + f(async {}); + //~^ ERROR: is not a future +} diff --git a/tests/ui/suggestions/suggest-create-closure-issue-150701.rs b/tests/ui/suggestions/suggest-create-closure-issue-150701.rs new file mode 100644 index 0000000000000..e7a5076d8f1d9 --- /dev/null +++ b/tests/ui/suggestions/suggest-create-closure-issue-150701.rs @@ -0,0 +1,15 @@ +// Regression test for #150701 + +//@ run-rustfix +//@ edition: 2024 + +use std::future::Future; + +fn f(_c: impl Future) {} + +fn main() { + f(async || {}()); //~ ERROR: expected function, found `()` + //~^ ERROR: is not a future + f(async || {}); + //~^ ERROR: is not a future +} diff --git a/tests/ui/suggestions/suggest-create-closure-issue-150701.stderr b/tests/ui/suggestions/suggest-create-closure-issue-150701.stderr new file mode 100644 index 0000000000000..0f991169d6772 --- /dev/null +++ b/tests/ui/suggestions/suggest-create-closure-issue-150701.stderr @@ -0,0 +1,52 @@ +error[E0618]: expected function, found `()` + --> $DIR/suggest-create-closure-issue-150701.rs:11:16 + | +LL | f(async || {}()); + | ^^-- + | | + | call expression requires function + | +help: if you meant to create this closure and immediately call it, surround the closure with parentheses + | +LL | f((async || {})()); + | + + + +error[E0277]: `{async closure@$DIR/suggest-create-closure-issue-150701.rs:11:7: 11:15}` is not a future + --> $DIR/suggest-create-closure-issue-150701.rs:11:7 + | +LL | f(async || {}()); + | - ^^^^^^^^^^^^^ `{async closure@$DIR/suggest-create-closure-issue-150701.rs:11:7: 11:15}` is not a future + | | + | required by a bound introduced by this call + | + = help: the trait `Future` is not implemented for `{async closure@$DIR/suggest-create-closure-issue-150701.rs:11:7: 11:15}` +note: required by a bound in `f` + --> $DIR/suggest-create-closure-issue-150701.rs:8:15 + | +LL | fn f(_c: impl Future) {} + | ^^^^^^^^^^^^^^^^^^^ required by this bound in `f` + +error[E0277]: `{async closure@$DIR/suggest-create-closure-issue-150701.rs:13:7: 13:15}` is not a future + --> $DIR/suggest-create-closure-issue-150701.rs:13:7 + | +LL | f(async || {}); + | - ^^^^^^^^^^^ `{async closure@$DIR/suggest-create-closure-issue-150701.rs:13:7: 13:15}` is not a future + | | + | required by a bound introduced by this call + | + = help: the trait `Future` is not implemented for `{async closure@$DIR/suggest-create-closure-issue-150701.rs:13:7: 13:15}` +note: required by a bound in `f` + --> $DIR/suggest-create-closure-issue-150701.rs:8:15 + | +LL | fn f(_c: impl Future) {} + | ^^^^^^^^^^^^^^^^^^^ required by this bound in `f` +help: use `async {}` instead of `async || {}` to introduce an async block + | +LL - f(async || {}); +LL + f(async {}); + | + +error: aborting due to 3 previous errors + +Some errors have detailed explanations: E0277, E0618. +For more information about an error, try `rustc --explain E0277`. diff --git a/tests/ui/suggestions/use-parentheses-to-call-closure-issue-145404.stderr b/tests/ui/suggestions/use-parentheses-to-call-closure-issue-145404.stderr index cb6df5af7fb16..e32b2d4a30c80 100644 --- a/tests/ui/suggestions/use-parentheses-to-call-closure-issue-145404.stderr +++ b/tests/ui/suggestions/use-parentheses-to-call-closure-issue-145404.stderr @@ -14,9 +14,8 @@ LL | fn call(&self, _: impl Display) {} | ^^^^^^^ required by this bound in `S::call` help: use parentheses to call this closure | -LL - S.call(|| "hello"); -LL + S.call((|| "hello")()); - | +LL | S.call((|| "hello")()); + | + +++ error: aborting due to 1 previous error From 21cd762cd16ff9806209c605a750491e223a75b7 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Tue, 21 Apr 2026 15:06:58 +1000 Subject: [PATCH 03/12] Rewrite `FlatMapInPlace`. Replace the hacky macro with a generic function and a new `FlatMapInPlaceVec` trait. More verbose but more readable and typical. LLM disclosure: I asked Claude Code to critique this file and it suggested the generic function + trait idea. I implemented the idea entirely by hand. --- .../src/flat_map_in_place.rs | 188 ++++++++++++------ 1 file changed, 132 insertions(+), 56 deletions(-) diff --git a/compiler/rustc_data_structures/src/flat_map_in_place.rs b/compiler/rustc_data_structures/src/flat_map_in_place.rs index 6d718059f9c81..d1fdd999d36ae 100644 --- a/compiler/rustc_data_structures/src/flat_map_in_place.rs +++ b/compiler/rustc_data_structures/src/flat_map_in_place.rs @@ -1,81 +1,157 @@ use std::{mem, ptr}; -use smallvec::{Array, SmallVec}; +use smallvec::SmallVec; use thin_vec::ThinVec; -pub trait FlatMapInPlace: Sized { +pub trait FlatMapInPlace { + /// `f` turns each element into 0..many elements. This function will consume the existing + /// elements in a vec-like structure and replace them with any number of new elements — fewer, + /// more, or the same number — as efficiently as possible. fn flat_map_in_place(&mut self, f: F) where F: FnMut(T) -> I, I: IntoIterator; } -// The implementation of this method is syntactically identical for all the -// different vector types. -macro_rules! flat_map_in_place { - ($vec:ident $( where T: $bound:path)?) => { - fn flat_map_in_place(&mut self, mut f: F) - where - F: FnMut(T) -> I, - I: IntoIterator, - { - struct LeakGuard<'a, T $(: $bound)?>(&'a mut $vec); - - impl<'a, T $(: $bound)?> Drop for LeakGuard<'a, T> { - fn drop(&mut self) { - unsafe { - self.0.set_len(0); // make sure we just leak elements in case of panic - } +// Blanket impl for all vec-like types that impl `FlatMapInPlaceVec`. +impl FlatMapInPlace for V { + fn flat_map_in_place(&mut self, mut f: F) + where + F: FnMut(V::Elem) -> I, + I: IntoIterator, + { + struct LeakGuard<'a, V: FlatMapInPlaceVec>(&'a mut V); + + impl<'a, V: FlatMapInPlaceVec> Drop for LeakGuard<'a, V> { + fn drop(&mut self) { + unsafe { + // Leak all elements in case of panic. + self.0.set_len(0); } } + } - let this = LeakGuard(self); - - let mut read_i = 0; - let mut write_i = 0; - unsafe { - while read_i < this.0.len() { - // move the read_i'th item out of the vector and map it - // to an iterator - let e = ptr::read(this.0.as_ptr().add(read_i)); - let iter = f(e).into_iter(); - read_i += 1; - - for e in iter { - if write_i < read_i { - ptr::write(this.0.as_mut_ptr().add(write_i), e); - write_i += 1; - } else { - // If this is reached we ran out of space - // in the middle of the vector. - // However, the vector is in a valid state here, - // so we just do a somewhat inefficient insert. - this.0.insert(write_i, e); - - read_i += 1; - write_i += 1; - } + let guard = LeakGuard(self); + + let mut read_i = 0; + let mut write_i = 0; + unsafe { + while read_i < guard.0.len() { + // Move the read_i'th item out of the vector and map it to an iterator. + let e = ptr::read(guard.0.as_ptr().add(read_i)); + let iter = f(e).into_iter(); + read_i += 1; + + for e in iter { + if write_i < read_i { + ptr::write(guard.0.as_mut_ptr().add(write_i), e); + write_i += 1; + } else { + // If this is reached we ran out of space in the middle of the vector. + // However, the vector is in a valid state here, so we just do a somewhat + // inefficient insert. + guard.0.insert(write_i, e); + + read_i += 1; + write_i += 1; } } + } - // write_i tracks the number of actually written new items. - this.0.set_len(write_i); + // `write_i` tracks the number of actually written new items. + guard.0.set_len(write_i); - // The ThinVec is in a sane state again. Prevent the LeakGuard from leaking the data. - mem::forget(this); - } + // `vec` is in a sane state again. Prevent the LeakGuard from leaking the data. + mem::forget(guard); } - }; + } +} + +// A vec-like type must implement these operations to support `flat_map_in_place`. +pub trait FlatMapInPlaceVec { + type Elem; + + fn len(&self) -> usize; + unsafe fn set_len(&mut self, len: usize); + fn as_ptr(&self) -> *const Self::Elem; + fn as_mut_ptr(&mut self) -> *mut Self::Elem; + fn insert(&mut self, idx: usize, elem: Self::Elem); } -impl FlatMapInPlace for Vec { - flat_map_in_place!(Vec); +impl FlatMapInPlaceVec for Vec { + type Elem = T; + + fn len(&self) -> usize { + self.len() + } + + unsafe fn set_len(&mut self, len: usize) { + unsafe { + self.set_len(len); + } + } + + fn as_ptr(&self) -> *const Self::Elem { + self.as_ptr() + } + + fn as_mut_ptr(&mut self) -> *mut Self::Elem { + self.as_mut_ptr() + } + + fn insert(&mut self, idx: usize, elem: Self::Elem) { + self.insert(idx, elem); + } } -impl> FlatMapInPlace for SmallVec { - flat_map_in_place!(SmallVec where T: Array); +impl FlatMapInPlaceVec for ThinVec { + type Elem = T; + + fn len(&self) -> usize { + self.len() + } + + unsafe fn set_len(&mut self, len: usize) { + unsafe { + self.set_len(len); + } + } + + fn as_ptr(&self) -> *const Self::Elem { + self.as_slice().as_ptr() + } + + fn as_mut_ptr(&mut self) -> *mut Self::Elem { + self.as_mut_slice().as_mut_ptr() + } + + fn insert(&mut self, idx: usize, elem: Self::Elem) { + self.insert(idx, elem); + } } -impl FlatMapInPlace for ThinVec { - flat_map_in_place!(ThinVec); +impl FlatMapInPlaceVec for SmallVec<[T; N]> { + type Elem = T; + + fn len(&self) -> usize { + self.len() + } + + unsafe fn set_len(&mut self, len: usize) { + unsafe { + self.set_len(len); + } + } + + fn as_ptr(&self) -> *const Self::Elem { + self.as_ptr() + } + + fn as_mut_ptr(&mut self) -> *mut Self::Elem { + self.as_mut_ptr() + } + + fn insert(&mut self, idx: usize, elem: Self::Elem) { + self.insert(idx, elem); + } } From b1c7595965ea1595b56fda5e12e5a8a45eb38136 Mon Sep 17 00:00:00 2001 From: Oli Scherer Date: Mon, 20 Apr 2026 19:44:10 +0200 Subject: [PATCH 04/12] Ensure we don't feed owners from ast lowering if we ever make that query tracked --- compiler/rustc_middle/src/ty/context.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs index 44cd6499fb088..71b0077b62131 100644 --- a/compiler/rustc_middle/src/ty/context.rs +++ b/compiler/rustc_middle/src/ty/context.rs @@ -700,6 +700,7 @@ impl<'tcx> TyCtxt<'tcx> { /// Feeds the HIR delayed owner during AST -> HIR delayed lowering. pub fn feed_delayed_owner(self, key: LocalDefId, owner: MaybeOwner<'tcx>) { + self.dep_graph.assert_ignored(); TyCtxtFeed { tcx: self, key }.delayed_owner(owner); } } From 39f517fd18eb0d20741ab9ab16dac8b82b4d4a39 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:54:50 +0200 Subject: [PATCH 05/12] Move diagnostic::on_const target check --- .../src/attributes/diagnostic/on_const.rs | 22 ++++++++++- compiler/rustc_attr_parsing/src/errors.rs | 7 ++++ compiler/rustc_passes/src/check_attr.rs | 24 +++--------- .../on_const/misplaced_attr.stderr | 38 +++++++++++-------- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_const.rs b/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_const.rs index 1e62c03fc3b63..349b54706623b 100644 --- a/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_const.rs +++ b/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_const.rs @@ -1,8 +1,10 @@ +use rustc_errors::Diagnostic; use rustc_hir::attrs::diagnostic::Directive; +use rustc_session::lint::builtin::MISPLACED_DIAGNOSTIC_ATTRIBUTES; use crate::attributes::diagnostic::*; use crate::attributes::prelude::*; - +use crate::errors::DiagnosticOnConstOnlyForTraitImpls; #[derive(Default)] pub(crate) struct OnConstParser { span: Option, @@ -21,6 +23,21 @@ impl AttributeParser for OnConstParser { let span = cx.attr_span; this.span = Some(span); + + // FIXME(mejrs) no constness field on `Target`, + // so non-constness is still checked in check_attr.rs + if !matches!(cx.target, Target::Impl { of_trait: true }) { + let target_span = cx.target_span; + cx.emit_dyn_lint( + MISPLACED_DIAGNOSTIC_ATTRIBUTES, + move |dcx, level| { + DiagnosticOnConstOnlyForTraitImpls { target_span }.into_diag(dcx, level) + }, + span, + ); + return; + } + let mode = Mode::DiagnosticOnConst; let Some(items) = parse_list(cx, args, mode) else { return }; @@ -32,7 +49,8 @@ impl AttributeParser for OnConstParser { }, )]; - //FIXME Still checked in `check_attr.rs` + // "Allowed" on all targets; noop on anything but non-const trait impls; + // this linted on in parser. const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(ALL_TARGETS); fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option { diff --git a/compiler/rustc_attr_parsing/src/errors.rs b/compiler/rustc_attr_parsing/src/errors.rs index ca57c25f25a0e..cff8feb933ef2 100644 --- a/compiler/rustc_attr_parsing/src/errors.rs +++ b/compiler/rustc_attr_parsing/src/errors.rs @@ -252,3 +252,10 @@ pub(crate) struct DocUnknownAny { #[derive(Diagnostic)] #[diag("expected boolean for `#[doc(auto_cfg = ...)]`")] pub(crate) struct DocAutoCfgWrongLiteral; + +#[derive(Diagnostic)] +#[diag("`#[diagnostic::on_const]` can only be applied to trait impls")] +pub(crate) struct DiagnosticOnConstOnlyForTraitImpls { + #[label("not a trait impl")] + pub target_span: Span, +} diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index 10a805a159b06..ecc83628971c2 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -54,13 +54,6 @@ use crate::errors; #[diag("`#[diagnostic::on_unimplemented]` can only be applied to trait definitions")] struct DiagnosticOnUnimplementedOnlyForTraits; -#[derive(Diagnostic)] -#[diag("`#[diagnostic::on_const]` can only be applied to trait impls")] -struct DiagnosticOnConstOnlyForTraitImpls { - #[label("not a trait impl")] - item_span: Span, -} - #[derive(Diagnostic)] #[diag("`#[diagnostic::on_const]` can only be applied to non-const trait impls")] struct DiagnosticOnConstOnlyForNonConstTraitImpls { @@ -587,7 +580,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> { } } - /// Checks if `#[diagnostic::on_const]` is applied to a trait impl + /// Checks if `#[diagnostic::on_const]` is applied to a on-const trait impl fn check_diagnostic_on_const( &self, attr_span: Span, @@ -595,6 +588,8 @@ impl<'tcx> CheckAttrVisitor<'tcx> { target: Target, item: Option>, ) { + // We only check the non-constness here. A diagnostic for use + // on not-trait impl items is issued during attribute parsing. if target == (Target::Impl { of_trait: true }) { match item.unwrap() { ItemLike::Item(it) => match it.expect_impl().constness { @@ -613,16 +608,9 @@ impl<'tcx> CheckAttrVisitor<'tcx> { ItemLike::ForeignItem => {} } } - let item_span = self.tcx.hir_span(hir_id); - self.tcx.emit_node_span_lint( - MISPLACED_DIAGNOSTIC_ATTRIBUTES, - hir_id, - attr_span, - DiagnosticOnConstOnlyForTraitImpls { item_span }, - ); - - // We don't check the validity of generic args here...whose generics would that be, anyway? - // The traits' or the impls'? + // FIXME(#155570) Can we do something with generic args here? + // regardless, we don't check the validity of generic args here + // ...whose generics would that be, anyway? The traits' or the impls'? } /// Checks if `#[diagnostic::on_move]` is applied to an ADT definition diff --git a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr index f92ea501696e5..609bc540ef8e3 100644 --- a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr +++ b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr @@ -1,11 +1,11 @@ -error: `#[diagnostic::on_const]` can only be applied to trait impls - --> $DIR/misplaced_attr.rs:4:1 +error: `#[diagnostic::on_const]` can only be applied to non-const trait impls + --> $DIR/misplaced_attr.rs:8:1 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | -LL | pub struct Foo; - | -------------- not a trait impl +LL | impl const PartialEq for Foo { + | ---------------------------- this is a const trait impl | note: the lint level is defined here --> $DIR/misplaced_attr.rs:2:9 @@ -13,32 +13,38 @@ note: the lint level is defined here LL | #![deny(misplaced_diagnostic_attributes)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: `#[diagnostic::on_const]` can only be applied to non-const trait impls - --> $DIR/misplaced_attr.rs:8:1 +error: `#[diagnostic::on_const]` can only be applied to trait impls + --> $DIR/misplaced_attr.rs:4:1 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | -LL | impl const PartialEq for Foo { - | ---------------------------- this is a const trait impl +LL | pub struct Foo; + | --------------- not a trait impl error: `#[diagnostic::on_const]` can only be applied to trait impls --> $DIR/misplaced_attr.rs:16:1 | -LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | -LL | impl Foo { - | -------- not a trait impl +LL | / impl Foo { +LL | | fn eq(&self, _other: &Foo) -> bool { +LL | | true +LL | | } +LL | | } + | |_- not a trait impl error: `#[diagnostic::on_const]` can only be applied to trait impls --> $DIR/misplaced_attr.rs:25:5 | -LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | -LL | fn partial_cmp(&self, other: &Foo) -> Option { - | ---------------------------------------------------------------- not a trait impl +LL | / fn partial_cmp(&self, other: &Foo) -> Option { +LL | | None +LL | | } + | |_____- not a trait impl error: aborting due to 4 previous errors From 5ba06cccd01f869938e27b4b4090a06db89a5e02 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:08:03 +0200 Subject: [PATCH 06/12] Always refer to non-const trait impls --- compiler/rustc_attr_parsing/src/errors.rs | 2 +- tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs | 6 +++--- .../ui/diagnostic_namespace/on_const/misplaced_attr.stderr | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/errors.rs b/compiler/rustc_attr_parsing/src/errors.rs index cff8feb933ef2..8e1050852181b 100644 --- a/compiler/rustc_attr_parsing/src/errors.rs +++ b/compiler/rustc_attr_parsing/src/errors.rs @@ -254,7 +254,7 @@ pub(crate) struct DocUnknownAny { pub(crate) struct DocAutoCfgWrongLiteral; #[derive(Diagnostic)] -#[diag("`#[diagnostic::on_const]` can only be applied to trait impls")] +#[diag("`#[diagnostic::on_const]` can only be applied to non-const trait impls")] pub(crate) struct DiagnosticOnConstOnlyForTraitImpls { #[label("not a trait impl")] pub target_span: Span, diff --git a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs index 48654ed47ec4d..815d2168bc829 100644 --- a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs +++ b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs @@ -2,7 +2,7 @@ #![deny(misplaced_diagnostic_attributes)] #[diagnostic::on_const(message = "tadaa", note = "boing")] -//~^ ERROR: `#[diagnostic::on_const]` can only be applied to trait impls +//~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait impls pub struct Foo; #[diagnostic::on_const(message = "tadaa", note = "boing")] @@ -14,7 +14,7 @@ impl const PartialEq for Foo { } #[diagnostic::on_const(message = "tadaa", note = "boing")] -//~^ ERROR: `#[diagnostic::on_const]` can only be applied to trait impls +//~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait impls impl Foo { fn eq(&self, _other: &Foo) -> bool { true @@ -23,7 +23,7 @@ impl Foo { impl PartialOrd for Foo { #[diagnostic::on_const(message = "tadaa", note = "boing")] - //~^ ERROR: `#[diagnostic::on_const]` can only be applied to trait impls + //~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait impls fn partial_cmp(&self, other: &Foo) -> Option { None } diff --git a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr index 609bc540ef8e3..18246b806ae8e 100644 --- a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr +++ b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr @@ -13,7 +13,7 @@ note: the lint level is defined here LL | #![deny(misplaced_diagnostic_attributes)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: `#[diagnostic::on_const]` can only be applied to trait impls +error: `#[diagnostic::on_const]` can only be applied to non-const trait impls --> $DIR/misplaced_attr.rs:4:1 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] @@ -22,7 +22,7 @@ LL | LL | pub struct Foo; | --------------- not a trait impl -error: `#[diagnostic::on_const]` can only be applied to trait impls +error: `#[diagnostic::on_const]` can only be applied to non-const trait impls --> $DIR/misplaced_attr.rs:16:1 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] @@ -35,7 +35,7 @@ LL | | } LL | | } | |_- not a trait impl -error: `#[diagnostic::on_const]` can only be applied to trait impls +error: `#[diagnostic::on_const]` can only be applied to non-const trait impls --> $DIR/misplaced_attr.rs:25:5 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] From 26d2c3068254f2ed31eca7eb36a1524aea4ad93e Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:00:45 +0200 Subject: [PATCH 07/12] Move diagnostic::on_move target check --- .../src/attributes/diagnostic/on_move.rs | 14 ++++++++++ compiler/rustc_attr_parsing/src/errors.rs | 4 +++ compiler/rustc_passes/src/check_attr.rs | 27 +++---------------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_move.rs b/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_move.rs index 2b62ab7f3d113..feb48fa868d49 100644 --- a/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_move.rs +++ b/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_move.rs @@ -1,10 +1,13 @@ +use rustc_errors::Diagnostic; use rustc_feature::template; use rustc_hir::attrs::AttributeKind; +use rustc_session::lint::builtin::MISPLACED_DIAGNOSTIC_ATTRIBUTES; use rustc_span::sym; use crate::attributes::diagnostic::*; use crate::attributes::prelude::*; use crate::context::{AcceptContext, Stage}; +use crate::errors::DiagnosticOnMoveOnlyForAdt; use crate::parser::ArgParser; use crate::target_checking::{ALL_TARGETS, AllowedTargets}; @@ -29,6 +32,15 @@ impl OnMoveParser { let span = cx.attr_span; self.span = Some(span); + if !matches!(cx.target, Target::Enum | Target::Struct | Target::Union) { + cx.emit_dyn_lint( + MISPLACED_DIAGNOSTIC_ATTRIBUTES, + move |dcx, level| DiagnosticOnMoveOnlyForAdt.into_diag(dcx, level), + span, + ); + return; + } + let Some(items) = parse_list(cx, args, mode) else { return }; if let Some(directive) = parse_directive_items(cx, mode, items.mixed(), true) { @@ -44,6 +56,8 @@ impl AttributeParser for OnMoveParser { this.parse(cx, args, Mode::DiagnosticOnMove); }, )]; + + // "Allowed" for all targets but noop if used on not-adt. const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(ALL_TARGETS); fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option { diff --git a/compiler/rustc_attr_parsing/src/errors.rs b/compiler/rustc_attr_parsing/src/errors.rs index 8e1050852181b..89ebbcae3507f 100644 --- a/compiler/rustc_attr_parsing/src/errors.rs +++ b/compiler/rustc_attr_parsing/src/errors.rs @@ -259,3 +259,7 @@ pub(crate) struct DiagnosticOnConstOnlyForTraitImpls { #[label("not a trait impl")] pub target_span: Span, } + +#[derive(Diagnostic)] +#[diag("`#[diagnostic::on_move]` can only be applied to enums, structs or unions")] +pub(crate) struct DiagnosticOnMoveOnlyForAdt; diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index ecc83628971c2..bd794d2d00f78 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -61,10 +61,6 @@ struct DiagnosticOnConstOnlyForNonConstTraitImpls { item_span: Span, } -#[derive(Diagnostic)] -#[diag("`#[diagnostic::on_move]` can only be applied to enums, structs or unions")] -struct DiagnosticOnMoveOnlyForAdt; - #[derive(Diagnostic)] #[diag("`#[diagnostic::on_unknown]` can only be applied to `use` statements")] struct DiagnosticOnUnknownOnlyForImports { @@ -218,8 +214,8 @@ impl<'tcx> CheckAttrVisitor<'tcx> { Attribute::Parsed(AttributeKind::OnUnimplemented{span, directive}) => {self.check_diagnostic_on_unimplemented(*span, hir_id, target,directive.as_deref())}, Attribute::Parsed(AttributeKind::OnUnknown { span, .. }) => { self.check_diagnostic_on_unknown(*span, hir_id, target) }, Attribute::Parsed(AttributeKind::OnConst{span, ..}) => {self.check_diagnostic_on_const(*span, hir_id, target, item)} - Attribute::Parsed(AttributeKind::OnMove { span, directive }) => { - self.check_diagnostic_on_move(*span, hir_id, target, directive.as_deref()) + Attribute::Parsed(AttributeKind::OnMove { directive , .. }) => { + self.check_diagnostic_on_move(hir_id, directive.as_deref()) }, Attribute::Parsed( // tidy-alphabetical-start @@ -613,23 +609,8 @@ impl<'tcx> CheckAttrVisitor<'tcx> { // ...whose generics would that be, anyway? The traits' or the impls'? } - /// Checks if `#[diagnostic::on_move]` is applied to an ADT definition - fn check_diagnostic_on_move( - &self, - attr_span: Span, - hir_id: HirId, - target: Target, - directive: Option<&Directive>, - ) { - if !matches!(target, Target::Enum | Target::Struct | Target::Union) { - self.tcx.emit_node_span_lint( - MISPLACED_DIAGNOSTIC_ATTRIBUTES, - hir_id, - attr_span, - DiagnosticOnMoveOnlyForAdt, - ); - } - + /// Checks use of generic formatting parameters in `#[diagnostic::on_move]` + fn check_diagnostic_on_move(&self, hir_id: HirId, directive: Option<&Directive>) { if let Some(directive) = directive { if let Node::Item(Item { kind: From 59503ecb5e7f06307472ffac7f9ec69467b35190 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:37:33 +0200 Subject: [PATCH 08/12] Move diagnostic::on_unimplemented target check --- .../attributes/diagnostic/on_unimplemented.rs | 12 ++++++--- compiler/rustc_attr_parsing/src/errors.rs | 4 +++ compiler/rustc_passes/src/check_attr.rs | 25 +++---------------- ...ons_of_the_internal_rustc_attribute.stderr | 16 ++++++------ ...t_fail_parsing_on_invalid_options_1.stderr | 16 ++++++------ 5 files changed, 31 insertions(+), 42 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_unimplemented.rs b/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_unimplemented.rs index dce3226670a15..069cda28582ec 100644 --- a/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_unimplemented.rs +++ b/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_unimplemented.rs @@ -1,7 +1,10 @@ +use rustc_errors::Diagnostic; use rustc_hir::attrs::diagnostic::Directive; +use rustc_session::lint::builtin::MISPLACED_DIAGNOSTIC_ATTRIBUTES; use crate::attributes::diagnostic::*; use crate::attributes::prelude::*; +use crate::errors::DiagnosticOnUnimplementedOnlyForTraits; #[derive(Default)] pub(crate) struct OnUnimplementedParser { @@ -19,11 +22,12 @@ impl OnUnimplementedParser { let span = cx.attr_span; self.span = Some(span); - // If target is not a trait, returning early will make `finalize` emit a - // `AttributeKind::OnUnimplemented {span, directive: None }`, to prevent it being - // accidentally used on non-trait items like trait aliases. if !matches!(cx.target, Target::Trait) { - // Lint later emitted in check_attr + cx.emit_dyn_lint( + MISPLACED_DIAGNOSTIC_ATTRIBUTES, + move |dcx, level| DiagnosticOnUnimplementedOnlyForTraits.into_diag(dcx, level), + span, + ); return; } diff --git a/compiler/rustc_attr_parsing/src/errors.rs b/compiler/rustc_attr_parsing/src/errors.rs index 89ebbcae3507f..d802081b12327 100644 --- a/compiler/rustc_attr_parsing/src/errors.rs +++ b/compiler/rustc_attr_parsing/src/errors.rs @@ -263,3 +263,7 @@ pub(crate) struct DiagnosticOnConstOnlyForTraitImpls { #[derive(Diagnostic)] #[diag("`#[diagnostic::on_move]` can only be applied to enums, structs or unions")] pub(crate) struct DiagnosticOnMoveOnlyForAdt; + +#[derive(Diagnostic)] +#[diag("`#[diagnostic::on_unimplemented]` can only be applied to trait definitions")] +pub(crate) struct DiagnosticOnUnimplementedOnlyForTraits; diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index bd794d2d00f78..6a95111cc495f 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -50,10 +50,6 @@ use rustc_trait_selection::traits::ObligationCtxt; use crate::errors; -#[derive(Diagnostic)] -#[diag("`#[diagnostic::on_unimplemented]` can only be applied to trait definitions")] -struct DiagnosticOnUnimplementedOnlyForTraits; - #[derive(Diagnostic)] #[diag("`#[diagnostic::on_const]` can only be applied to non-const trait impls")] struct DiagnosticOnConstOnlyForNonConstTraitImpls { @@ -211,7 +207,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> { self.check_rustc_must_implement_one_of(*attr_span, fn_names, hir_id,target) }, Attribute::Parsed(AttributeKind::DoNotRecommend{attr_span}) => {self.check_do_not_recommend(*attr_span, hir_id, target, item)}, - Attribute::Parsed(AttributeKind::OnUnimplemented{span, directive}) => {self.check_diagnostic_on_unimplemented(*span, hir_id, target,directive.as_deref())}, + Attribute::Parsed(AttributeKind::OnUnimplemented{directive,..}) => {self.check_diagnostic_on_unimplemented(hir_id, directive.as_deref())}, Attribute::Parsed(AttributeKind::OnUnknown { span, .. }) => { self.check_diagnostic_on_unknown(*span, hir_id, target) }, Attribute::Parsed(AttributeKind::OnConst{span, ..}) => {self.check_diagnostic_on_const(*span, hir_id, target, item)} Attribute::Parsed(AttributeKind::OnMove { directive , .. }) => { @@ -525,23 +521,8 @@ impl<'tcx> CheckAttrVisitor<'tcx> { } } - /// Checks if `#[diagnostic::on_unimplemented]` is applied to a trait definition - fn check_diagnostic_on_unimplemented( - &self, - attr_span: Span, - hir_id: HirId, - target: Target, - directive: Option<&Directive>, - ) { - if !matches!(target, Target::Trait) { - self.tcx.emit_node_span_lint( - MISPLACED_DIAGNOSTIC_ATTRIBUTES, - hir_id, - attr_span, - DiagnosticOnUnimplementedOnlyForTraits, - ); - } - + /// Checks use of generic formatting parameters in `#[diagnostic::on_unimplemented]` + fn check_diagnostic_on_unimplemented(&self, hir_id: HirId, directive: Option<&Directive>) { if let Some(directive) = directive { if let Node::Item(Item { kind: ItemKind::Trait(_, _, _, _, trait_name, generics, _, _), diff --git a/tests/ui/diagnostic_namespace/on_unimplemented/do_not_accept_options_of_the_internal_rustc_attribute.stderr b/tests/ui/diagnostic_namespace/on_unimplemented/do_not_accept_options_of_the_internal_rustc_attribute.stderr index 65d92e4592d01..9ed72d334b271 100644 --- a/tests/ui/diagnostic_namespace/on_unimplemented/do_not_accept_options_of_the_internal_rustc_attribute.stderr +++ b/tests/ui/diagnostic_namespace/on_unimplemented/do_not_accept_options_of_the_internal_rustc_attribute.stderr @@ -1,11 +1,3 @@ -warning: `#[diagnostic::on_unimplemented]` can only be applied to trait definitions - --> $DIR/do_not_accept_options_of_the_internal_rustc_attribute.rs:22:1 - | -LL | #[diagnostic::on_unimplemented(message = "Not allowed to apply it on a impl")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: `#[warn(misplaced_diagnostic_attributes)]` (part of `#[warn(unknown_or_malformed_diagnostic_attributes)]`) on by default - warning: there is no parameter `from_desugaring` on trait `Baz` --> $DIR/do_not_accept_options_of_the_internal_rustc_attribute.rs:29:17 | @@ -152,6 +144,14 @@ LL | #[diagnostic::on_unimplemented = "Message"] | = help: only `message`, `note` and `label` are allowed as options +warning: `#[diagnostic::on_unimplemented]` can only be applied to trait definitions + --> $DIR/do_not_accept_options_of_the_internal_rustc_attribute.rs:22:1 + | +LL | #[diagnostic::on_unimplemented(message = "Not allowed to apply it on a impl")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(misplaced_diagnostic_attributes)]` (part of `#[warn(unknown_or_malformed_diagnostic_attributes)]`) on by default + error[E0277]: trait has `()` and `i32` as params --> $DIR/do_not_accept_options_of_the_internal_rustc_attribute.rs:53:15 | diff --git a/tests/ui/diagnostic_namespace/on_unimplemented/do_not_fail_parsing_on_invalid_options_1.stderr b/tests/ui/diagnostic_namespace/on_unimplemented/do_not_fail_parsing_on_invalid_options_1.stderr index 14d3c0db1ec9f..d5c7f25efbc29 100644 --- a/tests/ui/diagnostic_namespace/on_unimplemented/do_not_fail_parsing_on_invalid_options_1.stderr +++ b/tests/ui/diagnostic_namespace/on_unimplemented/do_not_fail_parsing_on_invalid_options_1.stderr @@ -1,11 +1,3 @@ -warning: `#[diagnostic::on_unimplemented]` can only be applied to trait definitions - --> $DIR/do_not_fail_parsing_on_invalid_options_1.rs:7:1 - | -LL | #[diagnostic::on_unimplemented(message = "Baz")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: `#[warn(misplaced_diagnostic_attributes)]` (part of `#[warn(unknown_or_malformed_diagnostic_attributes)]`) on by default - warning: there is no parameter `DoesNotExist` on trait `Test` --> $DIR/do_not_fail_parsing_on_invalid_options_1.rs:31:44 | @@ -24,6 +16,14 @@ LL | #[diagnostic::on_unimplemented(unsupported = "foo")] = help: only `message`, `note` and `label` are allowed as options = note: `#[warn(malformed_diagnostic_attributes)]` (part of `#[warn(unknown_or_malformed_diagnostic_attributes)]`) on by default +warning: `#[diagnostic::on_unimplemented]` can only be applied to trait definitions + --> $DIR/do_not_fail_parsing_on_invalid_options_1.rs:7:1 + | +LL | #[diagnostic::on_unimplemented(message = "Baz")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(misplaced_diagnostic_attributes)]` (part of `#[warn(unknown_or_malformed_diagnostic_attributes)]`) on by default + warning: malformed `diagnostic::on_unimplemented` attribute --> $DIR/do_not_fail_parsing_on_invalid_options_1.rs:11:50 | From c62be6e99294b61d55a3dee1a879b72d1f2603f5 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:34:17 +0200 Subject: [PATCH 09/12] Move diagnostic::on_unknown target check --- .../src/attributes/diagnostic/on_unknown.rs | 22 ++++++++++++++++++- compiler/rustc_attr_parsing/src/errors.rs | 7 ++++++ compiler/rustc_passes/src/check_attr.rs | 22 +------------------ .../on_unknown/incorrect-locations.stderr | 20 ++++++++--------- 4 files changed, 39 insertions(+), 32 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_unknown.rs b/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_unknown.rs index a5364f968b219..d30ccfb73fe8b 100644 --- a/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_unknown.rs +++ b/compiler/rustc_attr_parsing/src/attributes/diagnostic/on_unknown.rs @@ -1,7 +1,11 @@ +use rustc_errors::Diagnostic; use rustc_hir::attrs::diagnostic::Directive; +use rustc_session::lint::builtin::MISPLACED_DIAGNOSTIC_ATTRIBUTES; +use crate::ShouldEmit; use crate::attributes::diagnostic::*; use crate::attributes::prelude::*; +use crate::errors::DiagnosticOnUnknownOnlyForImports; #[derive(Default)] pub(crate) struct OnUnknownParser { @@ -25,6 +29,22 @@ impl OnUnknownParser { let span = cx.attr_span; self.span = Some(span); + // At early parsing we get passed `Target::Crate` regardless of the item we're on. + // Only do target checking if we're late. + let early = matches!(cx.stage.should_emit(), ShouldEmit::Nothing); + + if !early && !matches!(cx.target, Target::Use) { + let target_span = cx.target_span; + cx.emit_dyn_lint( + MISPLACED_DIAGNOSTIC_ATTRIBUTES, + move |dcx, level| { + DiagnosticOnUnknownOnlyForImports { target_span }.into_diag(dcx, level) + }, + span, + ); + return; + } + let Some(items) = parse_list(cx, args, mode) else { return }; if let Some(directive) = parse_directive_items(cx, mode, items.mixed(), true) { @@ -41,7 +61,7 @@ impl AttributeParser for OnUnknownParser { this.parse(cx, args, Mode::DiagnosticOnUnknown); }, )]; - //FIXME attribute is not parsed for non-use statements but diagnostics are issued in `check_attr.rs` + // "Allowed" for all targets, but noop for all but use statements. const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(ALL_TARGETS); fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option { diff --git a/compiler/rustc_attr_parsing/src/errors.rs b/compiler/rustc_attr_parsing/src/errors.rs index d802081b12327..0dd843d34f154 100644 --- a/compiler/rustc_attr_parsing/src/errors.rs +++ b/compiler/rustc_attr_parsing/src/errors.rs @@ -267,3 +267,10 @@ pub(crate) struct DiagnosticOnMoveOnlyForAdt; #[derive(Diagnostic)] #[diag("`#[diagnostic::on_unimplemented]` can only be applied to trait definitions")] pub(crate) struct DiagnosticOnUnimplementedOnlyForTraits; + +#[derive(Diagnostic)] +#[diag("`#[diagnostic::on_unknown]` can only be applied to `use` statements")] +pub(crate) struct DiagnosticOnUnknownOnlyForImports { + #[label("not an import")] + pub target_span: Span, +} diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index 6a95111cc495f..27aafb97cc739 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -57,13 +57,6 @@ struct DiagnosticOnConstOnlyForNonConstTraitImpls { item_span: Span, } -#[derive(Diagnostic)] -#[diag("`#[diagnostic::on_unknown]` can only be applied to `use` statements")] -struct DiagnosticOnUnknownOnlyForImports { - #[label("not an import")] - item_span: Span, -} - fn target_from_impl_item<'tcx>(tcx: TyCtxt<'tcx>, impl_item: &hir::ImplItem<'_>) -> Target { match impl_item.kind { hir::ImplItemKind::Const(..) => Target::AssocConst, @@ -208,7 +201,6 @@ impl<'tcx> CheckAttrVisitor<'tcx> { }, Attribute::Parsed(AttributeKind::DoNotRecommend{attr_span}) => {self.check_do_not_recommend(*attr_span, hir_id, target, item)}, Attribute::Parsed(AttributeKind::OnUnimplemented{directive,..}) => {self.check_diagnostic_on_unimplemented(hir_id, directive.as_deref())}, - Attribute::Parsed(AttributeKind::OnUnknown { span, .. }) => { self.check_diagnostic_on_unknown(*span, hir_id, target) }, Attribute::Parsed(AttributeKind::OnConst{span, ..}) => {self.check_diagnostic_on_const(*span, hir_id, target, item)} Attribute::Parsed(AttributeKind::OnMove { directive , .. }) => { self.check_diagnostic_on_move(hir_id, directive.as_deref()) @@ -260,6 +252,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> { | AttributeKind::NoMain | AttributeKind::NoMangle(..) | AttributeKind::NoStd { .. } + | AttributeKind::OnUnknown { .. } | AttributeKind::Optimize(..) | AttributeKind::PanicRuntime | AttributeKind::PatchableFunctionEntry { .. } @@ -625,19 +618,6 @@ impl<'tcx> CheckAttrVisitor<'tcx> { } } - /// Checks if `#[diagnostic::on_unknown]` is applied to a trait impl - fn check_diagnostic_on_unknown(&self, attr_span: Span, hir_id: HirId, target: Target) { - if !matches!(target, Target::Use) { - let item_span = self.tcx.hir_span(hir_id); - self.tcx.emit_node_span_lint( - MISPLACED_DIAGNOSTIC_ATTRIBUTES, - hir_id, - attr_span, - DiagnosticOnUnknownOnlyForImports { item_span }, - ); - } - } - /// Checks if an `#[inline]` is applied to a function or a closure. fn check_inline(&self, hir_id: HirId, attr_span: Span, kind: &InlineAttr, target: Target) { match target { diff --git a/tests/ui/diagnostic_namespace/on_unknown/incorrect-locations.stderr b/tests/ui/diagnostic_namespace/on_unknown/incorrect-locations.stderr index 33636e1fcfc3f..c7636217af3ec 100644 --- a/tests/ui/diagnostic_namespace/on_unknown/incorrect-locations.stderr +++ b/tests/ui/diagnostic_namespace/on_unknown/incorrect-locations.stderr @@ -5,7 +5,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | extern crate std as other_std; - | ----------------------------- not an import + | ------------------------------ not an import | = note: `#[warn(misplaced_diagnostic_attributes)]` (part of `#[warn(unknown_or_malformed_diagnostic_attributes)]`) on by default @@ -16,7 +16,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | const CONST: () = (); - | --------------- not an import + | --------------------- not an import warning: `#[diagnostic::on_unknown]` can only be applied to `use` statements --> $DIR/incorrect-locations.rs:13:1 @@ -25,7 +25,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | static STATIC: () = (); - | ----------------- not an import + | ----------------------- not an import warning: `#[diagnostic::on_unknown]` can only be applied to `use` statements --> $DIR/incorrect-locations.rs:17:1 @@ -34,7 +34,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | type Type = (); - | --------- not an import + | --------------- not an import warning: `#[diagnostic::on_unknown]` can only be applied to `use` statements --> $DIR/incorrect-locations.rs:21:1 @@ -43,7 +43,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | enum Enum {} - | --------- not an import + | ------------ not an import warning: `#[diagnostic::on_unknown]` can only be applied to `use` statements --> $DIR/incorrect-locations.rs:25:1 @@ -52,7 +52,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | impl Enum {} - | --------- not an import + | ------------ not an import warning: `#[diagnostic::on_unknown]` can only be applied to `use` statements --> $DIR/incorrect-locations.rs:29:1 @@ -70,7 +70,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | fn fun() {} - | -------- not an import + | ----------- not an import warning: `#[diagnostic::on_unknown]` can only be applied to `use` statements --> $DIR/incorrect-locations.rs:37:1 @@ -79,7 +79,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | struct Struct {} - | ------------- not an import + | ---------------- not an import warning: `#[diagnostic::on_unknown]` can only be applied to `use` statements --> $DIR/incorrect-locations.rs:41:1 @@ -88,7 +88,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | trait Trait {} - | ----------- not an import + | -------------- not an import warning: `#[diagnostic::on_unknown]` can only be applied to `use` statements --> $DIR/incorrect-locations.rs:45:1 @@ -97,7 +97,7 @@ LL | #[diagnostic::on_unknown(message = "foo")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | impl Trait for i32 {} - | ------------------ not an import + | --------------------- not an import warning: 11 warnings emitted From 80fb0aa44e0f01b69b4dfefa3b7f0bdd42ab3828 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:58:00 +0200 Subject: [PATCH 10/12] Move diagnostic::do_not_recommend target checking --- .../attributes/diagnostic/do_not_recommend.rs | 20 ++++++++++++-- compiler/rustc_attr_parsing/src/errors.rs | 4 +++ compiler/rustc_passes/src/check_attr.rs | 26 +------------------ compiler/rustc_passes/src/errors.rs | 4 --- 4 files changed, 23 insertions(+), 31 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/attributes/diagnostic/do_not_recommend.rs b/compiler/rustc_attr_parsing/src/attributes/diagnostic/do_not_recommend.rs index 9f3d1e29b4dec..70e673ed6dfab 100644 --- a/compiler/rustc_attr_parsing/src/attributes/diagnostic/do_not_recommend.rs +++ b/compiler/rustc_attr_parsing/src/attributes/diagnostic/do_not_recommend.rs @@ -1,11 +1,16 @@ +use rustc_errors::Diagnostic; use rustc_feature::{AttributeTemplate, template}; +use rustc_hir::Target; use rustc_hir::attrs::AttributeKind; use rustc_hir::lints::AttributeLintKind; -use rustc_session::lint::builtin::MALFORMED_DIAGNOSTIC_ATTRIBUTES; +use rustc_session::lint::builtin::{ + MALFORMED_DIAGNOSTIC_ATTRIBUTES, MISPLACED_DIAGNOSTIC_ATTRIBUTES, +}; use rustc_span::{Symbol, sym}; use crate::attributes::{OnDuplicate, SingleAttributeParser}; use crate::context::{AcceptContext, Stage}; +use crate::errors::IncorrectDoNotRecommendLocation; use crate::parser::ArgParser; use crate::target_checking::{ALL_TARGETS, AllowedTargets}; @@ -13,7 +18,8 @@ pub(crate) struct DoNotRecommendParser; impl SingleAttributeParser for DoNotRecommendParser { const PATH: &[Symbol] = &[sym::diagnostic, sym::do_not_recommend]; const ON_DUPLICATE: OnDuplicate = OnDuplicate::Warn; - const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(ALL_TARGETS); // Checked in check_attr. + // "Allowed" on any target, noop on all but trait impls + const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(ALL_TARGETS); const TEMPLATE: AttributeTemplate = template!(Word /*doesn't matter */); fn convert(cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser) -> Option { @@ -25,6 +31,16 @@ impl SingleAttributeParser for DoNotRecommendParser { attr_span, ); } + + if !matches!(cx.target, Target::Impl { of_trait: true }) { + cx.emit_dyn_lint( + MISPLACED_DIAGNOSTIC_ATTRIBUTES, + move |dcx, level| IncorrectDoNotRecommendLocation.into_diag(dcx, level), + attr_span, + ); + return None; + } + Some(AttributeKind::DoNotRecommend { attr_span }) } } diff --git a/compiler/rustc_attr_parsing/src/errors.rs b/compiler/rustc_attr_parsing/src/errors.rs index 0dd843d34f154..4e560cf01e263 100644 --- a/compiler/rustc_attr_parsing/src/errors.rs +++ b/compiler/rustc_attr_parsing/src/errors.rs @@ -274,3 +274,7 @@ pub(crate) struct DiagnosticOnUnknownOnlyForImports { #[label("not an import")] pub target_span: Span, } + +#[derive(Diagnostic)] +#[diag("`#[diagnostic::do_not_recommend]` can only be placed on trait implementations")] +pub(crate) struct IncorrectDoNotRecommendLocation; diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index 27aafb97cc739..4e1fc6d79e763 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -199,7 +199,6 @@ impl<'tcx> CheckAttrVisitor<'tcx> { Attribute::Parsed(AttributeKind::RustcMustImplementOneOf { attr_span, fn_names }) => { self.check_rustc_must_implement_one_of(*attr_span, fn_names, hir_id,target) }, - Attribute::Parsed(AttributeKind::DoNotRecommend{attr_span}) => {self.check_do_not_recommend(*attr_span, hir_id, target, item)}, Attribute::Parsed(AttributeKind::OnUnimplemented{directive,..}) => {self.check_diagnostic_on_unimplemented(hir_id, directive.as_deref())}, Attribute::Parsed(AttributeKind::OnConst{span, ..}) => {self.check_diagnostic_on_const(*span, hir_id, target, item)} Attribute::Parsed(AttributeKind::OnMove { directive , .. }) => { @@ -222,6 +221,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> { | AttributeKind::CustomMir(..) | AttributeKind::DebuggerVisualizer(..) | AttributeKind::DefaultLibAllocator + | AttributeKind::DoNotRecommend {..} // `#[doc]` is actually a lot more than just doc comments, so is checked below | AttributeKind::DocComment {..} | AttributeKind::EiiDeclaration { .. } @@ -490,30 +490,6 @@ impl<'tcx> CheckAttrVisitor<'tcx> { } } - /// Checks if `#[diagnostic::do_not_recommend]` is applied on a trait impl - fn check_do_not_recommend( - &self, - attr_span: Span, - hir_id: HirId, - target: Target, - item: Option>, - ) { - if !matches!(target, Target::Impl { .. }) - || matches!( - item, - Some(ItemLike::Item(hir::Item { kind: hir::ItemKind::Impl(_impl),.. })) - if _impl.of_trait.is_none() - ) - { - self.tcx.emit_node_span_lint( - MISPLACED_DIAGNOSTIC_ATTRIBUTES, - hir_id, - attr_span, - errors::IncorrectDoNotRecommendLocation, - ); - } - } - /// Checks use of generic formatting parameters in `#[diagnostic::on_unimplemented]` fn check_diagnostic_on_unimplemented(&self, hir_id: HirId, directive: Option<&Directive>) { if let Some(directive) = directive { diff --git a/compiler/rustc_passes/src/errors.rs b/compiler/rustc_passes/src/errors.rs index cb56a28b8628a..a916b4670fded 100644 --- a/compiler/rustc_passes/src/errors.rs +++ b/compiler/rustc_passes/src/errors.rs @@ -13,10 +13,6 @@ use rustc_span::{DUMMY_SP, Ident, Span, Symbol}; use crate::check_attr::ProcMacroKind; use crate::lang_items::Duplicate; -#[derive(Diagnostic)] -#[diag("`#[diagnostic::do_not_recommend]` can only be placed on trait implementations")] -pub(crate) struct IncorrectDoNotRecommendLocation; - #[derive(Diagnostic)] #[diag("`#[loop_match]` should be applied to a loop")] pub(crate) struct LoopMatchAttr { From 3990dda8607726b84c3ad0792664919e1b434a26 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:19:00 +0200 Subject: [PATCH 11/12] say "implementation(s) in "not on trait impl" lints --- .../attributes/diagnostic/do_not_recommend.rs | 5 +++- compiler/rustc_attr_parsing/src/errors.rs | 9 ++++--- compiler/rustc_passes/src/check_attr.rs | 4 +-- .../incorrect-locations.current.stderr | 27 +++++++++++++++++++ .../incorrect-locations.next.stderr | 27 +++++++++++++++++++ .../on_const/misplaced_attr.rs | 8 +++--- .../on_const/misplaced_attr.stderr | 16 +++++------ 7 files changed, 78 insertions(+), 18 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/attributes/diagnostic/do_not_recommend.rs b/compiler/rustc_attr_parsing/src/attributes/diagnostic/do_not_recommend.rs index 70e673ed6dfab..8cf6be088a7a5 100644 --- a/compiler/rustc_attr_parsing/src/attributes/diagnostic/do_not_recommend.rs +++ b/compiler/rustc_attr_parsing/src/attributes/diagnostic/do_not_recommend.rs @@ -33,9 +33,12 @@ impl SingleAttributeParser for DoNotRecommendParser { } if !matches!(cx.target, Target::Impl { of_trait: true }) { + let target_span = cx.target_span; cx.emit_dyn_lint( MISPLACED_DIAGNOSTIC_ATTRIBUTES, - move |dcx, level| IncorrectDoNotRecommendLocation.into_diag(dcx, level), + move |dcx, level| { + IncorrectDoNotRecommendLocation { target_span }.into_diag(dcx, level) + }, attr_span, ); return None; diff --git a/compiler/rustc_attr_parsing/src/errors.rs b/compiler/rustc_attr_parsing/src/errors.rs index 4e560cf01e263..8e4dfb40ad868 100644 --- a/compiler/rustc_attr_parsing/src/errors.rs +++ b/compiler/rustc_attr_parsing/src/errors.rs @@ -254,9 +254,9 @@ pub(crate) struct DocUnknownAny { pub(crate) struct DocAutoCfgWrongLiteral; #[derive(Diagnostic)] -#[diag("`#[diagnostic::on_const]` can only be applied to non-const trait impls")] +#[diag("`#[diagnostic::on_const]` can only be applied to non-const trait implementations")] pub(crate) struct DiagnosticOnConstOnlyForTraitImpls { - #[label("not a trait impl")] + #[label("not a trait implementation")] pub target_span: Span, } @@ -277,4 +277,7 @@ pub(crate) struct DiagnosticOnUnknownOnlyForImports { #[derive(Diagnostic)] #[diag("`#[diagnostic::do_not_recommend]` can only be placed on trait implementations")] -pub(crate) struct IncorrectDoNotRecommendLocation; +pub(crate) struct IncorrectDoNotRecommendLocation { + #[label("not a trait implementation")] + pub target_span: Span, +} diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index 4e1fc6d79e763..06dd8cbc089b6 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -51,9 +51,9 @@ use rustc_trait_selection::traits::ObligationCtxt; use crate::errors; #[derive(Diagnostic)] -#[diag("`#[diagnostic::on_const]` can only be applied to non-const trait impls")] +#[diag("`#[diagnostic::on_const]` can only be applied to non-const trait implementations")] struct DiagnosticOnConstOnlyForNonConstTraitImpls { - #[label("this is a const trait impl")] + #[label("this is a const trait implementation")] item_span: Span, } diff --git a/tests/ui/diagnostic_namespace/do_not_recommend/incorrect-locations.current.stderr b/tests/ui/diagnostic_namespace/do_not_recommend/incorrect-locations.current.stderr index d2ad65b3c7919..b43a67a656127 100644 --- a/tests/ui/diagnostic_namespace/do_not_recommend/incorrect-locations.current.stderr +++ b/tests/ui/diagnostic_namespace/do_not_recommend/incorrect-locations.current.stderr @@ -3,6 +3,9 @@ warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implement | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | const CONST: () = (); + | --------------------- not a trait implementation | = note: `#[warn(misplaced_diagnostic_attributes)]` (part of `#[warn(unknown_or_malformed_diagnostic_attributes)]`) on by default @@ -11,48 +14,72 @@ warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implement | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | static STATIC: () = (); + | ----------------------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:15:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | type Type = (); + | --------------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:19:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | enum Enum {} + | ------------ not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:23:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | impl Enum {} + | ------------ not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:27:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | extern "C" {} + | ------------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:31:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | fn fun() {} + | ----------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:35:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | struct Struct {} + | ---------------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:39:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | trait Trait {} + | -------------- not a trait implementation warning: 9 warnings emitted diff --git a/tests/ui/diagnostic_namespace/do_not_recommend/incorrect-locations.next.stderr b/tests/ui/diagnostic_namespace/do_not_recommend/incorrect-locations.next.stderr index d2ad65b3c7919..b43a67a656127 100644 --- a/tests/ui/diagnostic_namespace/do_not_recommend/incorrect-locations.next.stderr +++ b/tests/ui/diagnostic_namespace/do_not_recommend/incorrect-locations.next.stderr @@ -3,6 +3,9 @@ warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implement | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | const CONST: () = (); + | --------------------- not a trait implementation | = note: `#[warn(misplaced_diagnostic_attributes)]` (part of `#[warn(unknown_or_malformed_diagnostic_attributes)]`) on by default @@ -11,48 +14,72 @@ warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implement | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | static STATIC: () = (); + | ----------------------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:15:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | type Type = (); + | --------------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:19:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | enum Enum {} + | ------------ not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:23:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | impl Enum {} + | ------------ not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:27:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | extern "C" {} + | ------------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:31:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | fn fun() {} + | ----------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:35:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | struct Struct {} + | ---------------- not a trait implementation warning: `#[diagnostic::do_not_recommend]` can only be placed on trait implementations --> $DIR/incorrect-locations.rs:39:1 | LL | #[diagnostic::do_not_recommend] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +LL | +LL | trait Trait {} + | -------------- not a trait implementation warning: 9 warnings emitted diff --git a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs index 815d2168bc829..a580f540a847b 100644 --- a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs +++ b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.rs @@ -2,11 +2,11 @@ #![deny(misplaced_diagnostic_attributes)] #[diagnostic::on_const(message = "tadaa", note = "boing")] -//~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait impls +//~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait implementations pub struct Foo; #[diagnostic::on_const(message = "tadaa", note = "boing")] -//~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait impls +//~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait implementations impl const PartialEq for Foo { fn eq(&self, _other: &Foo) -> bool { true @@ -14,7 +14,7 @@ impl const PartialEq for Foo { } #[diagnostic::on_const(message = "tadaa", note = "boing")] -//~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait impls +//~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait implementations impl Foo { fn eq(&self, _other: &Foo) -> bool { true @@ -23,7 +23,7 @@ impl Foo { impl PartialOrd for Foo { #[diagnostic::on_const(message = "tadaa", note = "boing")] - //~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait impls + //~^ ERROR: `#[diagnostic::on_const]` can only be applied to non-const trait implementations fn partial_cmp(&self, other: &Foo) -> Option { None } diff --git a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr index 18246b806ae8e..0efead6720300 100644 --- a/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr +++ b/tests/ui/diagnostic_namespace/on_const/misplaced_attr.stderr @@ -1,11 +1,11 @@ -error: `#[diagnostic::on_const]` can only be applied to non-const trait impls +error: `#[diagnostic::on_const]` can only be applied to non-const trait implementations --> $DIR/misplaced_attr.rs:8:1 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | impl const PartialEq for Foo { - | ---------------------------- this is a const trait impl + | ---------------------------- this is a const trait implementation | note: the lint level is defined here --> $DIR/misplaced_attr.rs:2:9 @@ -13,16 +13,16 @@ note: the lint level is defined here LL | #![deny(misplaced_diagnostic_attributes)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: `#[diagnostic::on_const]` can only be applied to non-const trait impls +error: `#[diagnostic::on_const]` can only be applied to non-const trait implementations --> $DIR/misplaced_attr.rs:4:1 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LL | LL | pub struct Foo; - | --------------- not a trait impl + | --------------- not a trait implementation -error: `#[diagnostic::on_const]` can only be applied to non-const trait impls +error: `#[diagnostic::on_const]` can only be applied to non-const trait implementations --> $DIR/misplaced_attr.rs:16:1 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] @@ -33,9 +33,9 @@ LL | | fn eq(&self, _other: &Foo) -> bool { LL | | true LL | | } LL | | } - | |_- not a trait impl + | |_- not a trait implementation -error: `#[diagnostic::on_const]` can only be applied to non-const trait impls +error: `#[diagnostic::on_const]` can only be applied to non-const trait implementations --> $DIR/misplaced_attr.rs:25:5 | LL | #[diagnostic::on_const(message = "tadaa", note = "boing")] @@ -44,7 +44,7 @@ LL | LL | / fn partial_cmp(&self, other: &Foo) -> Option { LL | | None LL | | } - | |_____- not a trait impl + | |_____- not a trait implementation error: aborting due to 4 previous errors From c2c486a3c021222ce4f8bae37a6c6e2170d422ed Mon Sep 17 00:00:00 2001 From: Brian Oiko Date: Tue, 21 Apr 2026 12:20:19 +0000 Subject: [PATCH 12/12] tests: add whitespace tests for vertical tab behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tests: add whitespace tests for vertical tab behavior Add two small tests to highlight how vertical tab is handled differently. - vertical_tab_lexer.rs checks that the lexer treats vertical tab as whitespace - ascii_whitespace_excludes_vertical_tab.rs shows that split_ascii_whitespace does not split on it This helps document the difference between the Rust parser (which accepts vertical tab) and the standard library’s ASCII whitespace handling. See: rust-lang/rust-project-goals#53 * tests: add ignore-tidy-tab directive to whitespace tests * tests: expand vertical tab lexer test to cover all Pattern_White_Space chars * tests: add whitespace/ README entry explaining lexer vs stdlib mismatch * Update ascii_whitespace_excludes_vertical_tab.rs * Update ascii_whitespace_excludes_vertical_tab.rs make sure tabs and spaces are well checked * Update ascii_whitespace_excludes_vertical_tab.rs * fix tidy: add whitespace README entry * Update README.md with missing full stop * Update ascii_whitespace_excludes_vertical_tab.rs * fix tidy: use full path format for whitespace README entry * fix tidy: README order, trailing newlines in whitespace tests * fix: add run-pass directive and restore embedded whitespace bytes * fix tidy: remove duplicate whitespace README entry * Add failing UI test for invalid whitespace (zero width space) This adds a //@ check-fail test to ensure that disallowed whitespace characters like ZERO WIDTH SPACE are rejected by the Rust lexer. * git add tests/ui/whitespace/invalid_whitespace.rs git commit -m "Fix tidy: add trailing newline" git push * Fix tidy: add trailing newline * Update invalid_whitespace.rs * Update invalid_whitespace.rs * Clean up whitespace in invalid_whitespace.rs Remove unnecessary blank lines in invalid_whitespace.rs * Update invalid_whitespace.rs * Clarify ZERO WIDTH SPACE usage in test Update comment to clarify usage of ZERO WIDTH SPACE. * Improve error messages for invalid whitespace Updated error messages to clarify the issue with invisible characters. * Modify invalid_whitespace test for clarity Update test to check for invalid whitespace characters. * Resolve unknown token error in invalid_whitespace.rs Fix whitespace issue causing unknown token error. * Remove invisible character from variable assignment Fix invisible character issue in variable assignment. * Improve error message for invalid whitespace Updated error message to clarify invisible characters. * Improve error handling for invisible characters Updated error message for invisible characters in code. * Document error for unknown token due to whitespace Add error message for invalid whitespace in code * Update error message for invalid whitespace handling * Modify invalid_whitespace.rs for whitespace checks Updated the test to check for invalid whitespace handling. * Correct whitespace in variable declaration Fix formatting issue by adding space around '=' in variable declaration. * Update error message for invalid whitespace * Update invalid_whitespace.stderr * Refine error handling for invalid whitespace test Update the error messages for invalid whitespace in the test. * Update invalid_whitespace.rs * Fix whitespace issues in invalid_whitespace.rs * Update invalid_whitespace.stderr file * Clean up whitespace in invalid_whitespace.rs Removed unnecessary blank lines from the test file. * Update invalid_whitespace.stderr --- tests/ui/README.md | 15 +++++ .../ascii_whitespace_excludes_vertical_tab.rs | 22 +++++++ tests/ui/whitespace/invalid_whitespace.rs | 13 +++++ tests/ui/whitespace/invalid_whitespace.stderr | 10 ++++ tests/ui/whitespace/vertical_tab_lexer.rs | 58 +++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 tests/ui/whitespace/ascii_whitespace_excludes_vertical_tab.rs create mode 100644 tests/ui/whitespace/invalid_whitespace.rs create mode 100644 tests/ui/whitespace/invalid_whitespace.stderr create mode 100644 tests/ui/whitespace/vertical_tab_lexer.rs diff --git a/tests/ui/README.md b/tests/ui/README.md index 7c2df5048fc1b..d803af7948bde 100644 --- a/tests/ui/README.md +++ b/tests/ui/README.md @@ -1582,6 +1582,21 @@ Tests on various well-formedness checks, e.g. [Type-checking normal functions](h Tests on `where` clauses. See [Where clauses | Reference](https://doc.rust-lang.org/reference/items/generics.html#where-clauses). +## `tests/ui/whitespace/` + +Tests for whitespace handling in the Rust lexer. The Rust language +defines whitespace as Unicode Pattern_White_Space, which is not the +same as what the standard library gives you: + +- `is_ascii_whitespace` follows the WhatWG Infra Standard and skips + vertical tab (`\x0B`) +- `is_whitespace` matches Unicode White_Space, which is a broader set + +These tests make that gap visible and check that the lexer accepts +all 11 Pattern_White_Space characters correctly. + +See: https://github.com/rustfoundation/interop-initiative/issues/53 + ## `tests/ui/windows-subsystem/`: `#![windows_subsystem = ""]` See [the `windows_subsystem` attribute](https://doc.rust-lang.org/reference/runtime.html#the-windows_subsystem-attribute). diff --git a/tests/ui/whitespace/ascii_whitespace_excludes_vertical_tab.rs b/tests/ui/whitespace/ascii_whitespace_excludes_vertical_tab.rs new file mode 100644 index 0000000000000..aa4c09a9ba48b --- /dev/null +++ b/tests/ui/whitespace/ascii_whitespace_excludes_vertical_tab.rs @@ -0,0 +1,22 @@ +//@ run-pass +// This test checks that split_ascii_whitespace does NOT split on +// vertical tab (\x0B), because the standard library uses the WhatWG +// Infra Standard definition of ASCII whitespace, which excludes +// vertical tab. +// +// See: https://github.com/rust-lang/rust-project-goals/issues/53 + +fn main() { + let s = "a\x0Bb"; + + let parts: Vec<&str> = s.split_ascii_whitespace().collect(); + + assert_eq!(parts.len(), 1, + "vertical tab should not be treated as ASCII whitespace"); + + let s2 = "a b"; + let parts2: Vec<&str> = s2.split_ascii_whitespace().collect(); + assert_eq!(parts2.len(), 2, + "regular space should split correctly"); + +} diff --git a/tests/ui/whitespace/invalid_whitespace.rs b/tests/ui/whitespace/invalid_whitespace.rs new file mode 100644 index 0000000000000..809c8e2af0a25 --- /dev/null +++ b/tests/ui/whitespace/invalid_whitespace.rs @@ -0,0 +1,13 @@ +// This test ensures that the Rust lexer rejects invalid whitespace +// characters such as ZERO WIDTH SPACE. + +//@ check-fail + +fn main() { + let x = 5; + let y = 10; + + let a=​x + y; + //~^ ERROR unknown start of token + //~| HELP invisible characters like +} diff --git a/tests/ui/whitespace/invalid_whitespace.stderr b/tests/ui/whitespace/invalid_whitespace.stderr new file mode 100644 index 0000000000000..ebd203aa41037 --- /dev/null +++ b/tests/ui/whitespace/invalid_whitespace.stderr @@ -0,0 +1,10 @@ +error: unknown start of token: \u{200b} + --> $DIR/invalid_whitespace.rs:10:11 + | +LL | let a=​x + y; + | ^ + | + = help: invisible characters like '\u{200b}' are not usually visible in text editors + +error: aborting due to 1 previous error + diff --git a/tests/ui/whitespace/vertical_tab_lexer.rs b/tests/ui/whitespace/vertical_tab_lexer.rs new file mode 100644 index 0000000000000..75f4543a1fe2d --- /dev/null +++ b/tests/ui/whitespace/vertical_tab_lexer.rs @@ -0,0 +1,58 @@ +//@ run-pass +// ignore-tidy-tab +// +// Tests that the Rust lexer accepts Unicode Pattern_White_Space characters. +// +// Worth noting: the Rust reference defines whitespace as Pattern_White_Space, +// which is not the same as what is_ascii_whitespace or is_whitespace give you. +// +// is_ascii_whitespace follows WhatWG and skips vertical tab (\x0B). +// is_whitespace uses Unicode White_Space, which is a broader set. +// +// The 11 characters that actually count as whitespace in Rust source: +// \x09 \x0A \x0B \x0C \x0D \x20 \u{85} \u{200E} \u{200F} \u{2028} \u{2029} +// +// Ref: https://github.com/rustfoundation/interop-initiative/issues/53 + +#[rustfmt::skip] +fn main() { + // tab (\x09) between let and the name + let _ws1 = 1_i32; + + // vertical tab (\x0B) between let and the name + // this is the one is_ascii_whitespace gets wrong + let _ws2 = 2_i32; + + // form feed (\x0C) between let and the name + let _ws3 = 3_i32; + + // plain space (\x20), here just so every character is represented + let _ws4 = 4_i32; + + // NEL (\u{85}) between let and the name + let…_ws5 = 5_i32; + + // left-to-right mark (\u{200E}) between let and the name + let‎_ws6 = 6_i32; + + // right-to-left mark (\u{200F}) between let and the name + let‏_ws7 = 7_i32; + + // \x0A, \x0D, \u{2028}, \u{2029} are also Pattern_White_Space but they + // act as line endings, so you can't stick them in the middle of a statement. + // The lexer still handles them correctly at line boundaries. + + // These are Unicode White_Space but NOT Pattern_White_Space: + // \u{A0} no-break space \u{1680} ogham space mark + // \u{2000} en quad \u{2001} em quad + // \u{2002} en space \u{2003} em space + // \u{2004} three-per-em space \u{2005} four-per-em space + // \u{2006} six-per-em space \u{2007} figure space + // \u{2008} punctuation space \u{2009} thin space + // \u{200A} hair space \u{202F} narrow no-break space + // \u{205F} medium math space \u{3000} ideographic space + + // add them up so the compiler doesn't complain about unused variables + let _sum = _ws1 + _ws2 + _ws3 + _ws4 + _ws5 + _ws6 + _ws7; + println!("{}", _sum); +}