From d5d4cecee9a3456ec0ec6011090c947fe5409995 Mon Sep 17 00:00:00 2001
From: yukang <moorekang@gmail.com>
Date: Mon, 24 Feb 2025 11:04:04 +0800
Subject: [PATCH 01/16] Suggest mut when possbile for temporary value dropped
 while borrowed

---
 .../src/diagnostics/conflict_errors.rs        | 14 ++++++-
 tests/ui/coroutine/auto-trait-regions.stderr  |  4 +-
 .../sugg-mut-for-binding-issue-137486.fixed   | 23 ++++++++++++
 .../nll/sugg-mut-for-binding-issue-137486.rs  | 21 +++++++++++
 .../sugg-mut-for-binding-issue-137486.stderr  | 37 +++++++++++++++++++
 5 files changed, 96 insertions(+), 3 deletions(-)
 create mode 100644 tests/ui/nll/sugg-mut-for-binding-issue-137486.fixed
 create mode 100644 tests/ui/nll/sugg-mut-for-binding-issue-137486.rs
 create mode 100644 tests/ui/nll/sugg-mut-for-binding-issue-137486.stderr

diff --git a/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs b/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs
index 3b7d31b1b13bd..d0e014971f442 100644
--- a/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs
+++ b/compiler/rustc_borrowck/src/diagnostics/conflict_errors.rs
@@ -3229,8 +3229,20 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
                                     Applicability::MaybeIncorrect,
                                 );
                             }
+
+                            let mutability = if matches!(borrow.kind(), BorrowKind::Mut { .. }) {
+                                "mut "
+                            } else {
+                                ""
+                            };
+
                             if !is_format_arguments_item {
-                                let addition = format!("let binding = {};\n{}", s, " ".repeat(p));
+                                let addition = format!(
+                                    "let {}binding = {};\n{}",
+                                    mutability,
+                                    s,
+                                    " ".repeat(p)
+                                );
                                 err.multipart_suggestion_verbose(
                                     msg,
                                     vec![
diff --git a/tests/ui/coroutine/auto-trait-regions.stderr b/tests/ui/coroutine/auto-trait-regions.stderr
index a9a0bde2ba019..a3b5a5f006e61 100644
--- a/tests/ui/coroutine/auto-trait-regions.stderr
+++ b/tests/ui/coroutine/auto-trait-regions.stderr
@@ -11,7 +11,7 @@ LL |         assert_foo(a);
    |
 help: consider using a `let` binding to create a longer lived value
    |
-LL ~         let binding = true;
+LL ~         let mut binding = true;
 LL ~         let a = A(&mut binding, &mut true, No);
    |
 
@@ -28,7 +28,7 @@ LL |         assert_foo(a);
    |
 help: consider using a `let` binding to create a longer lived value
    |
-LL ~         let binding = true;
+LL ~         let mut binding = true;
 LL ~         let a = A(&mut true, &mut binding, No);
    |
 
diff --git a/tests/ui/nll/sugg-mut-for-binding-issue-137486.fixed b/tests/ui/nll/sugg-mut-for-binding-issue-137486.fixed
new file mode 100644
index 0000000000000..ee9d9a373de00
--- /dev/null
+++ b/tests/ui/nll/sugg-mut-for-binding-issue-137486.fixed
@@ -0,0 +1,23 @@
+//@ run-rustfix
+#![allow(unused_assignments)]
+
+use std::pin::Pin;
+fn main() {
+    let mut s = String::from("hello");
+    let mut ref_s = &mut s;
+
+    let mut binding = String::from("world");
+    ref_s = &mut binding; //~ ERROR temporary value dropped while borrowed [E0716]
+
+    print!("r1 = {}", ref_s);
+
+    let mut val: u8 = 5;
+    let mut s = Pin::new(&mut val);
+    let mut ref_s = &mut s;
+
+    let mut val2: u8 = 10;
+    let mut binding = Pin::new(&mut val2);
+    ref_s = &mut binding; //~ ERROR temporary value dropped while borrowed [E0716]
+
+    print!("r1 = {}", ref_s);
+}
diff --git a/tests/ui/nll/sugg-mut-for-binding-issue-137486.rs b/tests/ui/nll/sugg-mut-for-binding-issue-137486.rs
new file mode 100644
index 0000000000000..8f7ea7561077d
--- /dev/null
+++ b/tests/ui/nll/sugg-mut-for-binding-issue-137486.rs
@@ -0,0 +1,21 @@
+//@ run-rustfix
+#![allow(unused_assignments)]
+
+use std::pin::Pin;
+fn main() {
+    let mut s = String::from("hello");
+    let mut ref_s = &mut s;
+
+    ref_s = &mut String::from("world"); //~ ERROR temporary value dropped while borrowed [E0716]
+
+    print!("r1 = {}", ref_s);
+
+    let mut val: u8 = 5;
+    let mut s = Pin::new(&mut val);
+    let mut ref_s = &mut s;
+
+    let mut val2: u8 = 10;
+    ref_s = &mut Pin::new(&mut val2); //~ ERROR temporary value dropped while borrowed [E0716]
+
+    print!("r1 = {}", ref_s);
+}
diff --git a/tests/ui/nll/sugg-mut-for-binding-issue-137486.stderr b/tests/ui/nll/sugg-mut-for-binding-issue-137486.stderr
new file mode 100644
index 0000000000000..8432725f60ad3
--- /dev/null
+++ b/tests/ui/nll/sugg-mut-for-binding-issue-137486.stderr
@@ -0,0 +1,37 @@
+error[E0716]: temporary value dropped while borrowed
+  --> $DIR/sugg-mut-for-binding-issue-137486.rs:9:18
+   |
+LL |     ref_s = &mut String::from("world");
+   |                  ^^^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement
+   |                  |
+   |                  creates a temporary value which is freed while still in use
+LL |
+LL |     print!("r1 = {}", ref_s);
+   |                       ----- borrow later used here
+   |
+help: consider using a `let` binding to create a longer lived value
+   |
+LL ~     let mut binding = String::from("world");
+LL ~     ref_s = &mut binding;
+   |
+
+error[E0716]: temporary value dropped while borrowed
+  --> $DIR/sugg-mut-for-binding-issue-137486.rs:18:18
+   |
+LL |     ref_s = &mut Pin::new(&mut val2);
+   |                  ^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement
+   |                  |
+   |                  creates a temporary value which is freed while still in use
+LL |
+LL |     print!("r1 = {}", ref_s);
+   |                       ----- borrow later used here
+   |
+help: consider using a `let` binding to create a longer lived value
+   |
+LL ~     let mut binding = Pin::new(&mut val2);
+LL ~     ref_s = &mut binding;
+   |
+
+error: aborting due to 2 previous errors
+
+For more information about this error, try `rustc --explain E0716`.

From 6a5bad36a87a6f3420839f95175498ae413c817a Mon Sep 17 00:00:00 2001
From: Vadim Petrochenkov <vadim.petrochenkov@gmail.com>
Date: Mon, 2 Jun 2025 19:03:55 +0300
Subject: [PATCH 02/16] resolve: Tweak `private_macro_use` lint to be
 compatible with upcoming macro prelude changes

---
 .../rustc_resolve/src/build_reduced_graph.rs  | 28 +++++++++----------
 compiler/rustc_resolve/src/imports.rs         |  4 ++-
 compiler/rustc_resolve/src/lib.rs             | 26 +++++++++++++----
 3 files changed, 36 insertions(+), 22 deletions(-)

diff --git a/compiler/rustc_resolve/src/build_reduced_graph.rs b/compiler/rustc_resolve/src/build_reduced_graph.rs
index c30ed781f35f3..650a827ba5644 100644
--- a/compiler/rustc_resolve/src/build_reduced_graph.rs
+++ b/compiler/rustc_resolve/src/build_reduced_graph.rs
@@ -1098,22 +1098,20 @@ impl<'a, 'ra, 'tcx> BuildReducedGraphVisitor<'a, 'ra, 'tcx> {
             self.r.potentially_unused_imports.push(import);
             module.for_each_child(self, |this, ident, ns, binding| {
                 if ns == MacroNS {
-                    let imported_binding =
-                        if this.r.is_accessible_from(binding.vis, this.parent_scope.module) {
-                            this.r.import(binding, import)
-                        } else if !this.r.is_builtin_macro(binding.res())
-                            && !this.r.macro_use_prelude.contains_key(&ident.name)
-                        {
-                            // - `!r.is_builtin_macro(res)` excluding the built-in macros such as `Debug` or `Hash`.
-                            // - `!r.macro_use_prelude.contains_key(name)` excluding macros defined in other extern
-                            //    crates such as `std`.
-                            // FIXME: This branch should eventually be removed.
-                            let import = macro_use_import(this, span, true);
-                            this.r.import(binding, import)
-                        } else {
+                    let import = if this.r.is_accessible_from(binding.vis, this.parent_scope.module)
+                    {
+                        import
+                    } else {
+                        // FIXME: This branch is used for reporting the `private_macro_use` lint
+                        // and should eventually be removed.
+                        if this.r.macro_use_prelude.contains_key(&ident.name) {
+                            // Do not override already existing entries with compatibility entries.
                             return;
-                        };
-                    this.add_macro_use_binding(ident.name, imported_binding, span, allow_shadowing);
+                        }
+                        macro_use_import(this, span, true)
+                    };
+                    let import_binding = this.r.import(binding, import);
+                    this.add_macro_use_binding(ident.name, import_binding, span, allow_shadowing);
                 }
             });
         } else {
diff --git a/compiler/rustc_resolve/src/imports.rs b/compiler/rustc_resolve/src/imports.rs
index 816efd0d5fa2d..e989209e177a3 100644
--- a/compiler/rustc_resolve/src/imports.rs
+++ b/compiler/rustc_resolve/src/imports.rs
@@ -133,7 +133,9 @@ impl<'ra> std::fmt::Debug for ImportKind<'ra> {
                 .field("target", target)
                 .field("id", id)
                 .finish(),
-            MacroUse { .. } => f.debug_struct("MacroUse").finish(),
+            MacroUse { warn_private } => {
+                f.debug_struct("MacroUse").field("warn_private", warn_private).finish()
+            }
             MacroExport => f.debug_struct("MacroExport").finish(),
         }
     }
diff --git a/compiler/rustc_resolve/src/lib.rs b/compiler/rustc_resolve/src/lib.rs
index 9ba70abd4d933..ea8715b82547f 100644
--- a/compiler/rustc_resolve/src/lib.rs
+++ b/compiler/rustc_resolve/src/lib.rs
@@ -1934,12 +1934,26 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
         }
         if let NameBindingKind::Import { import, binding } = used_binding.kind {
             if let ImportKind::MacroUse { warn_private: true } = import.kind {
-                self.lint_buffer().buffer_lint(
-                    PRIVATE_MACRO_USE,
-                    import.root_id,
-                    ident.span,
-                    BuiltinLintDiag::MacroIsPrivate(ident),
-                );
+                // Do not report the lint if the macro name resolves in stdlib prelude
+                // even without the problematic `macro_use` import.
+                let found_in_stdlib_prelude = self.prelude.is_some_and(|prelude| {
+                    self.maybe_resolve_ident_in_module(
+                        ModuleOrUniformRoot::Module(prelude),
+                        ident,
+                        MacroNS,
+                        &ParentScope::module(self.empty_module, self),
+                        None,
+                    )
+                    .is_ok()
+                });
+                if !found_in_stdlib_prelude {
+                    self.lint_buffer().buffer_lint(
+                        PRIVATE_MACRO_USE,
+                        import.root_id,
+                        ident.span,
+                        BuiltinLintDiag::MacroIsPrivate(ident),
+                    );
+                }
             }
             // Avoid marking `extern crate` items that refer to a name from extern prelude,
             // but not introduce it, as used if they are accessed from lexical scope.

From 585a40963ea59808e74803f8610659a505b145e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Esteban=20K=C3=BCber?= <esteban@kuber.com.ar>
Date: Wed, 4 Jun 2025 18:18:06 +0000
Subject: [PATCH 03/16] Detect method not being present that is present in
 other tuple types

When a method is not present because of a trait bound not being met, and that trait bound is on a tuple, we check if making the tuple have no borrowed types makes the method to be found and highlight it if it does. This is a common problem for Bevy in particular and ORMs in general.
---
 .../rustc_hir_typeck/src/method/suggest.rs    | 123 +++++++++++++++++-
 tests/ui/methods/missing-bound-on-tuple.rs    |  39 ++++++
 .../ui/methods/missing-bound-on-tuple.stderr  |  58 +++++++++
 3 files changed, 218 insertions(+), 2 deletions(-)
 create mode 100644 tests/ui/methods/missing-bound-on-tuple.rs
 create mode 100644 tests/ui/methods/missing-bound-on-tuple.stderr

diff --git a/compiler/rustc_hir_typeck/src/method/suggest.rs b/compiler/rustc_hir_typeck/src/method/suggest.rs
index 7b71f5de7569f..54e0a42e79944 100644
--- a/compiler/rustc_hir_typeck/src/method/suggest.rs
+++ b/compiler/rustc_hir_typeck/src/method/suggest.rs
@@ -14,7 +14,9 @@ use rustc_data_structures::fx::{FxIndexMap, FxIndexSet};
 use rustc_data_structures::sorted_map::SortedMap;
 use rustc_data_structures::unord::UnordSet;
 use rustc_errors::codes::*;
-use rustc_errors::{Applicability, Diag, MultiSpan, StashKey, pluralize, struct_span_code_err};
+use rustc_errors::{
+    Applicability, Diag, DiagStyledString, MultiSpan, StashKey, pluralize, struct_span_code_err,
+};
 use rustc_hir::def::{CtorKind, DefKind, Res};
 use rustc_hir::def_id::DefId;
 use rustc_hir::intravisit::{self, Visitor};
@@ -1569,7 +1571,11 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
             );
         }
 
-        if rcvr_ty.is_numeric() && rcvr_ty.is_fresh() || restrict_type_params || suggested_derive {
+        if rcvr_ty.is_numeric() && rcvr_ty.is_fresh()
+            || restrict_type_params
+            || suggested_derive
+            || self.lookup_alternative_tuple_impls(&mut err, &unsatisfied_predicates)
+        {
         } else {
             self.suggest_traits_to_import(
                 &mut err,
@@ -1744,6 +1750,119 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
         err.emit()
     }
 
+    /// If the predicate failure is caused by an unmet bound on a tuple, recheck if the bound would
+    /// succeed if all the types on the tuple had no borrows. This is a common problem for libraries
+    /// like Bevy and ORMs, which rely heavily on traits being implemented on tuples.
+    fn lookup_alternative_tuple_impls(
+        &self,
+        err: &mut Diag<'_>,
+        unsatisfied_predicates: &[(
+            ty::Predicate<'tcx>,
+            Option<ty::Predicate<'tcx>>,
+            Option<ObligationCause<'tcx>>,
+        )],
+    ) -> bool {
+        let mut found_tuple = false;
+        for (pred, root, _ob) in unsatisfied_predicates {
+            let mut preds = vec![pred];
+            if let Some(root) = root {
+                // We will look at both the current predicate and the root predicate that caused it
+                // to be needed. If calling something like `<(A, &B)>::default()`, then `pred` is
+                // `&B: Default` and `root` is `(A, &B): Default`, which is the one we are checking
+                // for further down, so we check both.
+                preds.push(root);
+            }
+            for pred in preds {
+                if let Some(clause) = pred.as_clause()
+                    && let Some(clause) = clause.as_trait_clause()
+                    && let ty = clause.self_ty().skip_binder()
+                    && let ty::Tuple(types) = ty.kind()
+                {
+                    let path = clause.skip_binder().trait_ref.print_only_trait_path();
+                    let def_id = clause.def_id();
+                    let ty = Ty::new_tup(
+                        self.tcx,
+                        self.tcx.mk_type_list_from_iter(types.iter().map(|ty| ty.peel_refs())),
+                    );
+                    let args = ty::GenericArgs::for_item(self.tcx, def_id, |param, _| {
+                        if param.index == 0 {
+                            ty.into()
+                        } else {
+                            self.infcx.var_for_def(DUMMY_SP, param)
+                        }
+                    });
+                    if self
+                        .infcx
+                        .type_implements_trait(def_id, args, self.param_env)
+                        .must_apply_modulo_regions()
+                    {
+                        // "`Trait` is implemented for `(A, B)` but not for `(A, &B)`"
+                        let mut msg = DiagStyledString::normal(format!("`{path}` "));
+                        msg.push_highlighted("is");
+                        msg.push_normal(" implemented for `(");
+                        let len = types.len();
+                        for (i, t) in types.iter().enumerate() {
+                            msg.push(
+                                format!("{}", with_forced_trimmed_paths!(t.peel_refs())),
+                                t.peel_refs() != t,
+                            );
+                            if i < len - 1 {
+                                msg.push_normal(", ");
+                            }
+                        }
+                        msg.push_normal(")` but ");
+                        msg.push_highlighted("not");
+                        msg.push_normal(" for `(");
+                        for (i, t) in types.iter().enumerate() {
+                            msg.push(
+                                format!("{}", with_forced_trimmed_paths!(t)),
+                                t.peel_refs() != t,
+                            );
+                            if i < len - 1 {
+                                msg.push_normal(", ");
+                            }
+                        }
+                        msg.push_normal(")`");
+
+                        // Find the span corresponding to the impl that was found to point at it.
+                        if let Some(impl_span) = self
+                            .tcx
+                            .all_impls(def_id)
+                            .filter(|&impl_def_id| {
+                                let header = self.tcx.impl_trait_header(impl_def_id).unwrap();
+                                let trait_ref = header.trait_ref.instantiate(
+                                    self.tcx,
+                                    self.infcx.fresh_args_for_item(DUMMY_SP, impl_def_id),
+                                );
+
+                                let value = ty::fold_regions(self.tcx, ty, |_, _| {
+                                    self.tcx.lifetimes.re_erased
+                                });
+                                // FIXME: Don't bother dealing with non-lifetime binders here...
+                                if value.has_escaping_bound_vars() {
+                                    return false;
+                                }
+                                self.infcx.can_eq(ty::ParamEnv::empty(), trait_ref.self_ty(), value)
+                                    && header.polarity == ty::ImplPolarity::Positive
+                            })
+                            .map(|impl_def_id| self.tcx.def_span(impl_def_id))
+                            .next()
+                        {
+                            err.highlighted_span_note(impl_span, msg.0);
+                        } else {
+                            err.highlighted_note(msg.0);
+                        }
+                        found_tuple = true;
+                    }
+                    // If `pred` was already on the tuple, we don't need to look at the root
+                    // obligation too.
+                    break;
+                }
+            }
+        }
+        found_tuple
+    }
+
     /// If an appropriate error source is not found, check method chain for possible candidates
     fn lookup_segments_chain_for_no_match_method(
         &self,
diff --git a/tests/ui/methods/missing-bound-on-tuple.rs b/tests/ui/methods/missing-bound-on-tuple.rs
new file mode 100644
index 0000000000000..25deabf59267b
--- /dev/null
+++ b/tests/ui/methods/missing-bound-on-tuple.rs
@@ -0,0 +1,39 @@
+trait WorksOnDefault {
+    fn do_something() {}
+}
+
+impl<T: Default> WorksOnDefault for T {}
+//~^ NOTE the following trait bounds were not satisfied
+//~| NOTE unsatisfied trait bound introduced here
+
+trait Foo {}
+
+trait WorksOnFoo {
+    fn do_be_do() {}
+}
+
+impl<T: Foo> WorksOnFoo for T {}
+//~^ NOTE the following trait bounds were not satisfied
+//~| NOTE unsatisfied trait bound introduced here
+
+impl<A: Foo, B: Foo, C: Foo> Foo for (A, B, C) {}
+//~^ NOTE `Foo` is implemented for `(i32, u32, String)`
+impl Foo for i32 {}
+impl Foo for &i32 {}
+impl Foo for u32 {}
+impl Foo for String {}
+
+fn main() {
+    let _success = <(i32, u32, String)>::do_something();
+    let _failure = <(i32, &u32, String)>::do_something(); //~ ERROR E0599
+    //~^ NOTE `Default` is implemented for `(i32, u32, String)`
+    //~| NOTE function or associated item cannot be called on
+    let _success = <(i32, u32, String)>::do_be_do();
+    let _failure = <(i32, &u32, String)>::do_be_do(); //~ ERROR E0599
+    //~^ NOTE function or associated item cannot be called on
+    let _success = <(i32, u32, String)>::default();
+    let _failure = <(i32, &u32, String)>::default(); //~ ERROR E0599
+    //~^ NOTE `Default` is implemented for `(i32, u32, String)`
+    //~| NOTE function or associated item cannot be called on
+    //~| NOTE the following trait bounds were not satisfied
+}
diff --git a/tests/ui/methods/missing-bound-on-tuple.stderr b/tests/ui/methods/missing-bound-on-tuple.stderr
new file mode 100644
index 0000000000000..f3e0897e5e645
--- /dev/null
+++ b/tests/ui/methods/missing-bound-on-tuple.stderr
@@ -0,0 +1,58 @@
+error[E0599]: the function or associated item `do_something` exists for tuple `(i32, &u32, String)`, but its trait bounds were not satisfied
+  --> $DIR/missing-bound-on-tuple.rs:28:43
+   |
+LL |     let _failure = <(i32, &u32, String)>::do_something();
+   |                                           ^^^^^^^^^^^^ function or associated item cannot be called on `(i32, &u32, String)` due to unsatisfied trait bounds
+   |
+note: the following trait bounds were not satisfied:
+      `&(i32, &u32, String): Default`
+      `&mut (i32, &u32, String): Default`
+      `(i32, &u32, String): Default`
+  --> $DIR/missing-bound-on-tuple.rs:5:9
+   |
+LL | impl<T: Default> WorksOnDefault for T {}
+   |         ^^^^^^^  --------------     -
+   |         |
+   |         unsatisfied trait bound introduced here
+note: `Default` is implemented for `(i32, u32, String)` but not for `(i32, &u32, String)`
+  --> $SRC_DIR/core/src/tuple.rs:LL:COL
+   = note: this error originates in the macro `tuple_impls` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error[E0599]: the function or associated item `do_be_do` exists for tuple `(i32, &u32, String)`, but its trait bounds were not satisfied
+  --> $DIR/missing-bound-on-tuple.rs:32:43
+   |
+LL |     let _failure = <(i32, &u32, String)>::do_be_do();
+   |                                           ^^^^^^^^ function or associated item cannot be called on `(i32, &u32, String)` due to unsatisfied trait bounds
+   |
+note: the following trait bounds were not satisfied:
+      `&(i32, &u32, String): Foo`
+      `&mut (i32, &u32, String): Foo`
+      `(i32, &u32, String): Foo`
+  --> $DIR/missing-bound-on-tuple.rs:15:9
+   |
+LL | impl<T: Foo> WorksOnFoo for T {}
+   |         ^^^  ----------     -
+   |         |
+   |         unsatisfied trait bound introduced here
+note: `Foo` is implemented for `(i32, u32, String)` but not for `(i32, &u32, String)`
+  --> $DIR/missing-bound-on-tuple.rs:19:1
+   |
+LL | impl<A: Foo, B: Foo, C: Foo> Foo for (A, B, C) {}
+   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+error[E0599]: the function or associated item `default` exists for tuple `(i32, &u32, String)`, but its trait bounds were not satisfied
+  --> $DIR/missing-bound-on-tuple.rs:35:43
+   |
+LL |     let _failure = <(i32, &u32, String)>::default();
+   |                                           ^^^^^^^ function or associated item cannot be called on `(i32, &u32, String)` due to unsatisfied trait bounds
+   |
+   = note: the following trait bounds were not satisfied:
+           `&u32: Default`
+           which is required by `(i32, &u32, String): Default`
+note: `Default` is implemented for `(i32, u32, String)` but not for `(i32, &u32, String)`
+  --> $SRC_DIR/core/src/tuple.rs:LL:COL
+   = note: this error originates in the macro `tuple_impls` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: aborting due to 3 previous errors
+
+For more information about this error, try `rustc --explain E0599`.

From fafc0f249f8d6c2eabd2c75dca536f04ddead79f Mon Sep 17 00:00:00 2001
From: Urgau <urgau@numericable.fr>
Date: Thu, 19 Dec 2024 22:07:05 +0100
Subject: [PATCH 04/16] Report the `unpredictable_function_pointer_comparisons`
 lint in macro

---
 compiler/rustc_lint/src/types.rs              |  3 +-
 tests/ui/lint/fn-ptr-comparisons-some.rs      |  2 +-
 tests/ui/lint/fn-ptr-comparisons-some.stderr  | 13 ++++-
 tests/ui/lint/fn-ptr-comparisons-weird.rs     | 22 +++++++++
 tests/ui/lint/fn-ptr-comparisons-weird.stderr | 47 ++++++++++++++++---
 tests/ui/lint/fn-ptr-comparisons.fixed        |  2 -
 tests/ui/lint/fn-ptr-comparisons.rs           |  2 -
 tests/ui/lint/fn-ptr-comparisons.stderr       | 24 +++++-----
 8 files changed, 90 insertions(+), 25 deletions(-)

diff --git a/compiler/rustc_lint/src/types.rs b/compiler/rustc_lint/src/types.rs
index fec23354c9122..aaba0c14b1ce4 100644
--- a/compiler/rustc_lint/src/types.rs
+++ b/compiler/rustc_lint/src/types.rs
@@ -196,7 +196,8 @@ declare_lint! {
     /// same address after being merged together.
     UNPREDICTABLE_FUNCTION_POINTER_COMPARISONS,
     Warn,
-    "detects unpredictable function pointer comparisons"
+    "detects unpredictable function pointer comparisons",
+    report_in_external_macro
 }
 
 #[derive(Copy, Clone, Default)]
diff --git a/tests/ui/lint/fn-ptr-comparisons-some.rs b/tests/ui/lint/fn-ptr-comparisons-some.rs
index 152e16b9884ba..c6ddd759baa37 100644
--- a/tests/ui/lint/fn-ptr-comparisons-some.rs
+++ b/tests/ui/lint/fn-ptr-comparisons-some.rs
@@ -12,6 +12,6 @@ fn main() {
     let _ = Some::<FnPtr>(func) == Some(func as unsafe extern "C" fn());
     //~^ WARN function pointer comparisons
 
-    // Undecided as of https://github.com/rust-lang/rust/pull/134536
     assert_eq!(Some::<FnPtr>(func), Some(func as unsafe extern "C" fn()));
+    //~^ WARN function pointer comparisons
 }
diff --git a/tests/ui/lint/fn-ptr-comparisons-some.stderr b/tests/ui/lint/fn-ptr-comparisons-some.stderr
index eefad05b676de..522c4399bce1d 100644
--- a/tests/ui/lint/fn-ptr-comparisons-some.stderr
+++ b/tests/ui/lint/fn-ptr-comparisons-some.stderr
@@ -9,5 +9,16 @@ LL |     let _ = Some::<FnPtr>(func) == Some(func as unsafe extern "C" fn());
    = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
    = note: `#[warn(unpredictable_function_pointer_comparisons)]` on by default
 
-warning: 1 warning emitted
+warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
+  --> $DIR/fn-ptr-comparisons-some.rs:15:5
+   |
+LL |     assert_eq!(Some::<FnPtr>(func), Some(func as unsafe extern "C" fn()));
+   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: the address of the same function can vary between different codegen units
+   = note: furthermore, different functions could have the same address after being merged together
+   = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
+   = note: this warning originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+warning: 2 warnings emitted
 
diff --git a/tests/ui/lint/fn-ptr-comparisons-weird.rs b/tests/ui/lint/fn-ptr-comparisons-weird.rs
index 171fbfb87279d..4d756cb49df0f 100644
--- a/tests/ui/lint/fn-ptr-comparisons-weird.rs
+++ b/tests/ui/lint/fn-ptr-comparisons-weird.rs
@@ -1,5 +1,23 @@
 //@ check-pass
 
+#[derive(PartialEq, Eq)]
+struct A {
+    f: fn(),
+    //~^ WARN function pointer comparisons
+}
+
+#[allow(unpredictable_function_pointer_comparisons)]
+#[derive(PartialEq, Eq)]
+struct AllowedAbove {
+    f: fn(),
+}
+
+#[derive(PartialEq, Eq)]
+#[allow(unpredictable_function_pointer_comparisons)]
+struct AllowedBelow {
+    f: fn(),
+}
+
 fn main() {
     let f: fn() = main;
     let g: fn() = main;
@@ -12,4 +30,8 @@ fn main() {
     //~^ WARN function pointer comparisons
     let _ = f < g;
     //~^ WARN function pointer comparisons
+    let _ = assert_eq!(g, g);
+    //~^ WARN function pointer comparisons
+    let _ = assert_ne!(g, g);
+    //~^ WARN function pointer comparisons
 }
diff --git a/tests/ui/lint/fn-ptr-comparisons-weird.stderr b/tests/ui/lint/fn-ptr-comparisons-weird.stderr
index f237166392238..2014e519c2538 100644
--- a/tests/ui/lint/fn-ptr-comparisons-weird.stderr
+++ b/tests/ui/lint/fn-ptr-comparisons-weird.stderr
@@ -1,5 +1,19 @@
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons-weird.rs:7:13
+  --> $DIR/fn-ptr-comparisons-weird.rs:5:5
+   |
+LL | #[derive(PartialEq, Eq)]
+   |          --------- in this derive macro expansion
+LL | struct A {
+LL |     f: fn(),
+   |     ^^^^^^^
+   |
+   = note: the address of the same function can vary between different codegen units
+   = note: furthermore, different functions could have the same address after being merged together
+   = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
+   = note: `#[warn(unpredictable_function_pointer_comparisons)]` on by default
+
+warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
+  --> $DIR/fn-ptr-comparisons-weird.rs:25:13
    |
 LL |     let _ = f > g;
    |             ^^^^^
@@ -7,10 +21,9 @@ LL |     let _ = f > g;
    = note: the address of the same function can vary between different codegen units
    = note: furthermore, different functions could have the same address after being merged together
    = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
-   = note: `#[warn(unpredictable_function_pointer_comparisons)]` on by default
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons-weird.rs:9:13
+  --> $DIR/fn-ptr-comparisons-weird.rs:27:13
    |
 LL |     let _ = f >= g;
    |             ^^^^^^
@@ -20,7 +33,7 @@ LL |     let _ = f >= g;
    = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons-weird.rs:11:13
+  --> $DIR/fn-ptr-comparisons-weird.rs:29:13
    |
 LL |     let _ = f <= g;
    |             ^^^^^^
@@ -30,7 +43,7 @@ LL |     let _ = f <= g;
    = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons-weird.rs:13:13
+  --> $DIR/fn-ptr-comparisons-weird.rs:31:13
    |
 LL |     let _ = f < g;
    |             ^^^^^
@@ -39,5 +52,27 @@ LL |     let _ = f < g;
    = note: furthermore, different functions could have the same address after being merged together
    = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
 
-warning: 4 warnings emitted
+warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
+  --> $DIR/fn-ptr-comparisons-weird.rs:33:13
+   |
+LL |     let _ = assert_eq!(g, g);
+   |             ^^^^^^^^^^^^^^^^
+   |
+   = note: the address of the same function can vary between different codegen units
+   = note: furthermore, different functions could have the same address after being merged together
+   = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
+   = note: this warning originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
+  --> $DIR/fn-ptr-comparisons-weird.rs:35:13
+   |
+LL |     let _ = assert_ne!(g, g);
+   |             ^^^^^^^^^^^^^^^^
+   |
+   = note: the address of the same function can vary between different codegen units
+   = note: furthermore, different functions could have the same address after being merged together
+   = note: for more information visit <https://doc.rust-lang.org/nightly/core/ptr/fn.fn_addr_eq.html>
+   = note: this warning originates in the macro `assert_ne` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+warning: 7 warnings emitted
 
diff --git a/tests/ui/lint/fn-ptr-comparisons.fixed b/tests/ui/lint/fn-ptr-comparisons.fixed
index 22f16177a0446..41cdb7bf6aea0 100644
--- a/tests/ui/lint/fn-ptr-comparisons.fixed
+++ b/tests/ui/lint/fn-ptr-comparisons.fixed
@@ -11,7 +11,6 @@ extern "C" fn c() {}
 
 extern "C" fn args(_a: i32) -> i32 { 0 }
 
-#[derive(PartialEq, Eq)]
 struct A {
     f: fn(),
 }
@@ -52,7 +51,6 @@ fn main() {
     let _ = std::ptr::fn_addr_eq(t, test as unsafe extern "C" fn());
     //~^ WARN function pointer comparisons
 
-    let _ = a1 == a2; // should not warn
     let _ = std::ptr::fn_addr_eq(a1.f, a2.f);
     //~^ WARN function pointer comparisons
 }
diff --git a/tests/ui/lint/fn-ptr-comparisons.rs b/tests/ui/lint/fn-ptr-comparisons.rs
index 90a8ab5c926b6..c2601d6adfba4 100644
--- a/tests/ui/lint/fn-ptr-comparisons.rs
+++ b/tests/ui/lint/fn-ptr-comparisons.rs
@@ -11,7 +11,6 @@ extern "C" fn c() {}
 
 extern "C" fn args(_a: i32) -> i32 { 0 }
 
-#[derive(PartialEq, Eq)]
 struct A {
     f: fn(),
 }
@@ -52,7 +51,6 @@ fn main() {
     let _ = t == test;
     //~^ WARN function pointer comparisons
 
-    let _ = a1 == a2; // should not warn
     let _ = a1.f == a2.f;
     //~^ WARN function pointer comparisons
 }
diff --git a/tests/ui/lint/fn-ptr-comparisons.stderr b/tests/ui/lint/fn-ptr-comparisons.stderr
index e699332389864..5913acca16bbc 100644
--- a/tests/ui/lint/fn-ptr-comparisons.stderr
+++ b/tests/ui/lint/fn-ptr-comparisons.stderr
@@ -1,5 +1,5 @@
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:26:13
+  --> $DIR/fn-ptr-comparisons.rs:25:13
    |
 LL |     let _ = f == a;
    |             ^^^^^^
@@ -15,7 +15,7 @@ LL +     let _ = std::ptr::fn_addr_eq(f, a as fn());
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:28:13
+  --> $DIR/fn-ptr-comparisons.rs:27:13
    |
 LL |     let _ = f != a;
    |             ^^^^^^
@@ -30,7 +30,7 @@ LL +     let _ = !std::ptr::fn_addr_eq(f, a as fn());
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:30:13
+  --> $DIR/fn-ptr-comparisons.rs:29:13
    |
 LL |     let _ = f == g;
    |             ^^^^^^
@@ -45,7 +45,7 @@ LL +     let _ = std::ptr::fn_addr_eq(f, g);
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:32:13
+  --> $DIR/fn-ptr-comparisons.rs:31:13
    |
 LL |     let _ = f == f;
    |             ^^^^^^
@@ -60,7 +60,7 @@ LL +     let _ = std::ptr::fn_addr_eq(f, f);
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:34:13
+  --> $DIR/fn-ptr-comparisons.rs:33:13
    |
 LL |     let _ = g == g;
    |             ^^^^^^
@@ -75,7 +75,7 @@ LL +     let _ = std::ptr::fn_addr_eq(g, g);
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:36:13
+  --> $DIR/fn-ptr-comparisons.rs:35:13
    |
 LL |     let _ = g == g;
    |             ^^^^^^
@@ -90,7 +90,7 @@ LL +     let _ = std::ptr::fn_addr_eq(g, g);
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:38:13
+  --> $DIR/fn-ptr-comparisons.rs:37:13
    |
 LL |     let _ = &g == &g;
    |             ^^^^^^^^
@@ -105,7 +105,7 @@ LL +     let _ = std::ptr::fn_addr_eq(g, g);
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:40:13
+  --> $DIR/fn-ptr-comparisons.rs:39:13
    |
 LL |     let _ = a as fn() == g;
    |             ^^^^^^^^^^^^^^
@@ -120,7 +120,7 @@ LL +     let _ = std::ptr::fn_addr_eq(a as fn(), g);
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:44:13
+  --> $DIR/fn-ptr-comparisons.rs:43:13
    |
 LL |     let _ = cfn == c;
    |             ^^^^^^^^
@@ -135,7 +135,7 @@ LL +     let _ = std::ptr::fn_addr_eq(cfn, c as extern "C" fn());
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:48:13
+  --> $DIR/fn-ptr-comparisons.rs:47:13
    |
 LL |     let _ = argsfn == args;
    |             ^^^^^^^^^^^^^^
@@ -150,7 +150,7 @@ LL +     let _ = std::ptr::fn_addr_eq(argsfn, args as extern "C" fn(i32) -> i32)
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:52:13
+  --> $DIR/fn-ptr-comparisons.rs:51:13
    |
 LL |     let _ = t == test;
    |             ^^^^^^^^^
@@ -165,7 +165,7 @@ LL +     let _ = std::ptr::fn_addr_eq(t, test as unsafe extern "C" fn());
    |
 
 warning: function pointer comparisons do not produce meaningful results since their addresses are not guaranteed to be unique
-  --> $DIR/fn-ptr-comparisons.rs:56:13
+  --> $DIR/fn-ptr-comparisons.rs:54:13
    |
 LL |     let _ = a1.f == a2.f;
    |             ^^^^^^^^^^^^

From 14b3e630129a3c89020e79bf7f0f1e60f6a6e93a Mon Sep 17 00:00:00 2001
From: Urgau <urgau@numericable.fr>
Date: Thu, 19 Dec 2024 22:33:49 +0100
Subject: [PATCH 05/16] Allow `unpredictable_function_pointer_comparisons` lint
 in more places

---
 library/core/src/task/wake.rs              | 1 +
 tests/ui/issues/issue-28561.rs             | 1 +
 tests/ui/mir/mir_refs_correct.rs           | 2 ++
 tests/ui/nullable-pointer-iotareduction.rs | 2 ++
 4 files changed, 6 insertions(+)

diff --git a/library/core/src/task/wake.rs b/library/core/src/task/wake.rs
index 9b8fefe42af60..bb7efe582f7a3 100644
--- a/library/core/src/task/wake.rs
+++ b/library/core/src/task/wake.rs
@@ -104,6 +104,7 @@ impl RawWaker {
 /// synchronization. This is because [`LocalWaker`] is not thread safe itself, so it cannot
 /// be sent across threads.
 #[stable(feature = "futures_api", since = "1.36.0")]
+#[allow(unpredictable_function_pointer_comparisons)]
 #[derive(PartialEq, Copy, Clone, Debug)]
 pub struct RawWakerVTable {
     /// This function will be called when the [`RawWaker`] gets cloned, e.g. when
diff --git a/tests/ui/issues/issue-28561.rs b/tests/ui/issues/issue-28561.rs
index f9b0ceb22fc66..642b2193a4f9b 100644
--- a/tests/ui/issues/issue-28561.rs
+++ b/tests/ui/issues/issue-28561.rs
@@ -37,6 +37,7 @@ struct Array<T> {
 }
 
 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+#[allow(unpredictable_function_pointer_comparisons)]
 struct Fn<A, B, C, D, E, F, G, H, I, J, K, L> {
     f00: fn(),
     f01: fn(A),
diff --git a/tests/ui/mir/mir_refs_correct.rs b/tests/ui/mir/mir_refs_correct.rs
index fc23c8c3631eb..f1832d90a0f56 100644
--- a/tests/ui/mir/mir_refs_correct.rs
+++ b/tests/ui/mir/mir_refs_correct.rs
@@ -1,6 +1,8 @@
 //@ run-pass
 //@ aux-build:mir_external_refs.rs
 
+#![allow(unpredictable_function_pointer_comparisons)]
+
 extern crate mir_external_refs as ext;
 
 struct S(#[allow(dead_code)] u8);
diff --git a/tests/ui/nullable-pointer-iotareduction.rs b/tests/ui/nullable-pointer-iotareduction.rs
index fa837dab51b6c..1b73164c9fc74 100644
--- a/tests/ui/nullable-pointer-iotareduction.rs
+++ b/tests/ui/nullable-pointer-iotareduction.rs
@@ -8,6 +8,8 @@
 // trying to get assert failure messages that at least identify which case
 // failed.
 
+#![allow(unpredictable_function_pointer_comparisons)]
+
 enum E<T> { Thing(isize, T), #[allow(dead_code)] Nothing((), ((), ()), [i8; 0]) }
 impl<T> E<T> {
     fn is_none(&self) -> bool {

From 8584c7c6a4441ee5d1500bc80816ce6c1eb026c9 Mon Sep 17 00:00:00 2001
From: Shun Sakai <sorairolake@protonmail.ch>
Date: Thu, 12 Jun 2025 16:29:09 +0900
Subject: [PATCH 06/16] chore(doctest): Remove redundant blank lines

---
 library/core/src/num/int_macros.rs | 1 -
 library/core/src/num/mod.rs        | 1 -
 library/core/src/slice/ascii.rs    | 1 -
 3 files changed, 3 deletions(-)

diff --git a/library/core/src/num/int_macros.rs b/library/core/src/num/int_macros.rs
index 65560f63c1859..3a7bc902f93cd 100644
--- a/library/core/src/num/int_macros.rs
+++ b/library/core/src/num/int_macros.rs
@@ -239,7 +239,6 @@ macro_rules! int_impl {
         /// Basic usage:
         ///
         /// ```
-        ///
         #[doc = concat!("let n = -1", stringify!($SelfT), ";")]
         ///
         #[doc = concat!("assert_eq!(n.cast_unsigned(), ", stringify!($UnsignedT), "::MAX);")]
diff --git a/library/core/src/num/mod.rs b/library/core/src/num/mod.rs
index 5c73bddbef2d1..ab2fcff61cd12 100644
--- a/library/core/src/num/mod.rs
+++ b/library/core/src/num/mod.rs
@@ -1053,7 +1053,6 @@ impl u8 {
     /// # Examples
     ///
     /// ```
-    ///
     /// assert_eq!("0", b'0'.escape_ascii().to_string());
     /// assert_eq!("\\t", b'\t'.escape_ascii().to_string());
     /// assert_eq!("\\r", b'\r'.escape_ascii().to_string());
diff --git a/library/core/src/slice/ascii.rs b/library/core/src/slice/ascii.rs
index d91f8bba548fc..b4d9a1b1ca4fd 100644
--- a/library/core/src/slice/ascii.rs
+++ b/library/core/src/slice/ascii.rs
@@ -128,7 +128,6 @@ impl [u8] {
     /// # Examples
     ///
     /// ```
-    ///
     /// let s = b"0\t\r\n'\"\\\x9d";
     /// let escaped = s.escape_ascii().to_string();
     /// assert_eq!(escaped, "0\\t\\r\\n\\'\\\"\\\\\\x9d");

From 28bf61b9b32a2db0a9a7f32d75349b4e0db72201 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= <jana@donsz.nl>
Date: Tue, 10 Jun 2025 13:00:09 +0200
Subject: [PATCH 07/16] introduce duplicate attribute diagnostic logic

---
 .../src/attributes/deprecation.rs             |   9 +-
 .../rustc_attr_parsing/src/attributes/mod.rs  | 106 ++++++++++++++++--
 .../src/attributes/stability.rs               |   5 +-
 .../src/attributes/transparency.rs            |   9 +-
 4 files changed, 112 insertions(+), 17 deletions(-)

diff --git a/compiler/rustc_attr_parsing/src/attributes/deprecation.rs b/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
index 006c1fe3b9c98..dcb12ac29cdaf 100644
--- a/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
@@ -1,8 +1,8 @@
 use rustc_attr_data_structures::{AttributeKind, DeprecatedSince, Deprecation};
 use rustc_span::{Span, Symbol, sym};
 
-use super::SingleAttributeParser;
 use super::util::parse_version;
+use super::{AttributeDuplicates, SingleAttributeParser};
 use crate::context::AcceptContext;
 use crate::parser::ArgParser;
 use crate::session_diagnostics;
@@ -43,12 +43,13 @@ fn get(
 
 impl SingleAttributeParser for DeprecationParser {
     const PATH: &'static [Symbol] = &[sym::deprecated];
+    const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
 
-    fn on_duplicate(cx: &AcceptContext<'_>, first_span: Span) {
+    fn on_duplicate(cx: &AcceptContext<'_>, used: Span, unused: Span) {
         // FIXME(jdonszelmann): merge with errors from check_attrs.rs
         cx.emit_err(session_diagnostics::UnusedMultiple {
-            this: cx.attr_span,
-            other: first_span,
+            this: used,
+            other: unused,
             name: sym::deprecated,
         });
     }
diff --git a/compiler/rustc_attr_parsing/src/attributes/mod.rs b/compiler/rustc_attr_parsing/src/attributes/mod.rs
index 7cceca3c24dcd..7aac4744504b2 100644
--- a/compiler/rustc_attr_parsing/src/attributes/mod.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/mod.rs
@@ -22,6 +22,7 @@ use thin_vec::ThinVec;
 
 use crate::context::{AcceptContext, FinalizeContext};
 use crate::parser::ArgParser;
+use crate::session_diagnostics::UnusedMultiple;
 
 pub(crate) mod allow_unstable;
 pub(crate) mod cfg;
@@ -83,11 +84,28 @@ pub(crate) trait AttributeParser: Default + 'static {
 pub(crate) trait SingleAttributeParser: 'static {
     const PATH: &'static [Symbol];
 
+<<<<<<< Conflict 1 of 1
++++++++ Contents of side #1
     /// Called when a duplicate attribute is found.
+%%%%%%% Changes from base to side #2
++    const ON_DUPLICATE_STRATEGY: AttributeDuplicates;
++
+     /// Caled when a duplicate attribute is found.
+>>>>>>> Conflict 1 of 1 ends
     ///
-    /// `first_span` is the span of the first occurrence of this attribute.
+    /// - `unused` is the span of the attribute that was unused or bad because of some
+    ///   duplicate reason (see [`AttributeDuplicates`])
+    /// - `used` is the span of the attribute that was used in favor of the unused attribute
     // FIXME(jdonszelmann): default error
-    fn on_duplicate(cx: &AcceptContext<'_>, first_span: Span);
+    fn on_duplicate(cx: &AcceptContext<'_>, used: Span, unused: Span) {
+        cx.emit_err(UnusedMultiple {
+            this: used,
+            other: unused,
+            name: Symbol::intern(
+                &Self::PATH.into_iter().map(|i| i.to_string()).collect::<Vec<_>>().join(".."),
+            ),
+        });
+    }
 
     /// Converts a single syntactical attribute to a single semantic attribute, or [`AttributeKind`]
     fn convert(cx: &AcceptContext<'_>, args: &ArgParser<'_>) -> Option<AttributeKind>;
@@ -103,12 +121,24 @@ impl<T: SingleAttributeParser> Default for Single<T> {
 
 impl<T: SingleAttributeParser> AttributeParser for Single<T> {
     const ATTRIBUTES: AcceptMapping<Self> = &[(T::PATH, |group: &mut Single<T>, cx, args| {
-        if let Some((_, s)) = group.1 {
-            T::on_duplicate(cx, s);
-            return;
-        }
-
         if let Some(pa) = T::convert(cx, args) {
+            match T::ON_DUPLICATE_STRATEGY {
+                // keep the first and error
+                AttributeDuplicates::ErrorFollowing => {
+                    if let Some((_, unused)) = group.1 {
+                        T::on_duplicate(cx, cx.attr_span, unused);
+                        return;
+                    }
+                }
+                // keep the new one and warn about the previous,
+                // then replace
+                AttributeDuplicates::FutureWarnPreceding => {
+                    if let Some((_, used)) = group.1 {
+                        T::on_duplicate(cx, used, cx.attr_span);
+                    }
+                }
+            }
+
             group.1 = Some((pa, cx.attr_span));
         }
     })];
@@ -118,6 +148,68 @@ impl<T: SingleAttributeParser> AttributeParser for Single<T> {
     }
 }
 
+pub(crate) enum OnDuplicate {
+    /// Give a default warning
+    Warn,
+
+    /// Duplicates will be a warning, with a note that this will be an error in the future.
+    WarnButFutureError,
+
+    /// Give a default error
+    Error,
+
+    /// Ignore duplicates
+    Ignore,
+
+    /// Custom function called when a duplicate attribute is found.
+    ///
+    /// - `unused` is the span of the attribute that was unused or bad because of some
+    ///   duplicate reason (see [`AttributeDuplicates`])
+    /// - `used` is the span of the attribute that was used in favor of the unused attribute
+    Custom(fn(cx: &AcceptContext<'_>, used: Span, unused: Span)),
+}
+
+impl OnDuplicate {
+    fn exec<P: SingleAttributeParser>(&self, cx: &AcceptContext<'_>, used: Span, unused: Span) {
+        match self {
+            OnDuplicate::Warn => {
+                todo!()
+            }
+            OnDuplicate::WarnButFutureError => {
+                todo!()
+            }
+            OnDuplicate::Error => {
+                cx.emit_err(UnusedMultiple {
+                    this: used,
+                    other: unused,
+                    name: Symbol::intern(
+                        &P::PATH.into_iter().map(|i| i.to_string()).collect::<Vec<_>>().join(".."),
+                    ),
+                });
+            }
+            OnDuplicate::Ignore => {}
+            OnDuplicate::Custom(f) => f(cx, used, unused),
+        }
+    }
+}
+
+pub(crate) enum AttributeDuplicates {
+    /// Duplicates after the first attribute will be an error.
+    ///
+    /// This should be used where duplicates would be ignored, but carry extra
+    /// meaning that could cause confusion. For example, `#[stable(since="1.0")]
+    /// #[stable(since="2.0")]`, which version should be used for `stable`?
+    ErrorFollowing,
+
+    /// Duplicates preceding the last instance of the attribute will be a
+    /// warning, with a note that this will be an error in the future.
+    ///
+    /// This is the same as `FutureWarnFollowing`, except the last attribute is
+    /// the one that is "used". Ideally these can eventually migrate to
+    /// `ErrorPreceding`.
+    FutureWarnPreceding,
+}
+
 type ConvertFn<E> = fn(ThinVec<E>) -> AttributeKind;
 
 /// Alternative to [`AttributeParser`] that automatically handles state management.
diff --git a/compiler/rustc_attr_parsing/src/attributes/stability.rs b/compiler/rustc_attr_parsing/src/attributes/stability.rs
index ce69a54513d2c..407fd5e1b8b13 100644
--- a/compiler/rustc_attr_parsing/src/attributes/stability.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/stability.rs
@@ -8,7 +8,7 @@ use rustc_errors::ErrorGuaranteed;
 use rustc_span::{Span, Symbol, sym};
 
 use super::util::parse_version;
-use super::{AcceptMapping, AttributeParser, SingleAttributeParser};
+use super::{AcceptMapping, AttributeDuplicates, AttributeParser, SingleAttributeParser};
 use crate::context::{AcceptContext, FinalizeContext};
 use crate::parser::{ArgParser, MetaItemParser};
 use crate::session_diagnostics::{self, UnsupportedLiteralReason};
@@ -118,9 +118,10 @@ pub(crate) struct ConstStabilityIndirectParser;
 // FIXME(jdonszelmann): single word attribute group when we have these
 impl SingleAttributeParser for ConstStabilityIndirectParser {
     const PATH: &'static [Symbol] = &[sym::rustc_const_stable_indirect];
+    const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
 
     // ignore
-    fn on_duplicate(_cx: &AcceptContext<'_>, _first_span: Span) {}
+    fn on_duplicate(_cx: &AcceptContext<'_>, _used: Span, _unused: Span) {}
 
     fn convert(_cx: &AcceptContext<'_>, _args: &ArgParser<'_>) -> Option<AttributeKind> {
         Some(AttributeKind::ConstStabilityIndirect)
diff --git a/compiler/rustc_attr_parsing/src/attributes/transparency.rs b/compiler/rustc_attr_parsing/src/attributes/transparency.rs
index d229fc0974010..0ab4cb99a6215 100644
--- a/compiler/rustc_attr_parsing/src/attributes/transparency.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/transparency.rs
@@ -2,7 +2,7 @@ use rustc_attr_data_structures::AttributeKind;
 use rustc_span::hygiene::Transparency;
 use rustc_span::{Span, Symbol, sym};
 
-use super::{AcceptContext, SingleAttributeParser};
+use super::{AcceptContext, AttributeDuplicates, SingleAttributeParser};
 use crate::parser::ArgParser;
 
 pub(crate) struct TransparencyParser;
@@ -11,10 +11,11 @@ pub(crate) struct TransparencyParser;
 #[allow(rustc::untranslatable_diagnostic)]
 #[allow(rustc::diagnostic_outside_of_impl)]
 impl SingleAttributeParser for TransparencyParser {
-    const PATH: &'static [Symbol] = &[sym::rustc_macro_transparency];
+    const PATH: &[Symbol] = &[sym::rustc_macro_transparency];
+    const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
 
-    fn on_duplicate(cx: &crate::context::AcceptContext<'_>, first_span: Span) {
-        cx.dcx().span_err(vec![first_span, cx.attr_span], "multiple macro transparency attributes");
+    fn on_duplicate(cx: &crate::context::AcceptContext<'_>, used: Span, unused: Span) {
+        cx.dcx().span_err(vec![used, unused], "multiple macro transparency attributes");
     }
 
     fn convert(cx: &AcceptContext<'_>, args: &ArgParser<'_>) -> Option<AttributeKind> {

From 4e1b6d13a20e5b72922f085fb4b248848ca02910 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= <jonathan@donsz.nl>
Date: Thu, 6 Feb 2025 17:36:34 +0100
Subject: [PATCH 08/16] Start using new diagnostic logic on all existing
 `single` parsers

---
 .../src/attributes/deprecation.rs             | 12 ++------
 .../rustc_attr_parsing/src/attributes/mod.rs  | 28 +++----------------
 .../src/attributes/stability.rs               |  8 +++---
 .../src/attributes/transparency.rs            |  9 +++---
 4 files changed, 14 insertions(+), 43 deletions(-)

diff --git a/compiler/rustc_attr_parsing/src/attributes/deprecation.rs b/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
index dcb12ac29cdaf..1b18ada70fc99 100644
--- a/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
@@ -2,7 +2,7 @@ use rustc_attr_data_structures::{AttributeKind, DeprecatedSince, Deprecation};
 use rustc_span::{Span, Symbol, sym};
 
 use super::util::parse_version;
-use super::{AttributeDuplicates, SingleAttributeParser};
+use super::{AttributeDuplicates, OnDuplicate, SingleAttributeParser};
 use crate::context::AcceptContext;
 use crate::parser::ArgParser;
 use crate::session_diagnostics;
@@ -44,15 +44,7 @@ fn get(
 impl SingleAttributeParser for DeprecationParser {
     const PATH: &'static [Symbol] = &[sym::deprecated];
     const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
-
-    fn on_duplicate(cx: &AcceptContext<'_>, used: Span, unused: Span) {
-        // FIXME(jdonszelmann): merge with errors from check_attrs.rs
-        cx.emit_err(session_diagnostics::UnusedMultiple {
-            this: used,
-            other: unused,
-            name: sym::deprecated,
-        });
-    }
+    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Error;
 
     fn convert(cx: &AcceptContext<'_>, args: &ArgParser<'_>) -> Option<AttributeKind> {
         let features = cx.features();
diff --git a/compiler/rustc_attr_parsing/src/attributes/mod.rs b/compiler/rustc_attr_parsing/src/attributes/mod.rs
index 7aac4744504b2..852615b4fce6c 100644
--- a/compiler/rustc_attr_parsing/src/attributes/mod.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/mod.rs
@@ -84,28 +84,8 @@ pub(crate) trait AttributeParser: Default + 'static {
 pub(crate) trait SingleAttributeParser: 'static {
     const PATH: &'static [Symbol];
 
-<<<<<<< Conflict 1 of 1
-+++++++ Contents of side #1
-    /// Called when a duplicate attribute is found.
-%%%%%%% Changes from base to side #2
-+    const ON_DUPLICATE_STRATEGY: AttributeDuplicates;
-+
-     /// Caled when a duplicate attribute is found.
->>>>>>> Conflict 1 of 1 ends
-    ///
-    /// - `unused` is the span of the attribute that was unused or bad because of some
-    ///   duplicate reason (see [`AttributeDuplicates`])
-    /// - `used` is the span of the attribute that was used in favor of the unused attribute
-    // FIXME(jdonszelmann): default error
-    fn on_duplicate(cx: &AcceptContext<'_>, used: Span, unused: Span) {
-        cx.emit_err(UnusedMultiple {
-            this: used,
-            other: unused,
-            name: Symbol::intern(
-                &Self::PATH.into_iter().map(|i| i.to_string()).collect::<Vec<_>>().join(".."),
-            ),
-        });
-    }
+    const ON_DUPLICATE_STRATEGY: AttributeDuplicates;
+    const ON_DUPLICATE: OnDuplicate;
 
     /// Converts a single syntactical attribute to a single semantic attribute, or [`AttributeKind`]
     fn convert(cx: &AcceptContext<'_>, args: &ArgParser<'_>) -> Option<AttributeKind>;
@@ -126,7 +106,7 @@ impl<T: SingleAttributeParser> AttributeParser for Single<T> {
                 // keep the first and error
                 AttributeDuplicates::ErrorFollowing => {
                     if let Some((_, unused)) = group.1 {
-                        T::on_duplicate(cx, cx.attr_span, unused);
+                        T::ON_DUPLICATE.exec::<T>(cx, cx.attr_span, unused);
                         return;
                     }
                 }
@@ -134,7 +114,7 @@ impl<T: SingleAttributeParser> AttributeParser for Single<T> {
                 // then replace
                 AttributeDuplicates::FutureWarnPreceding => {
                     if let Some((_, used)) = group.1 {
-                        T::on_duplicate(cx, used, cx.attr_span);
+                        T::ON_DUPLICATE.exec::<T>(cx, used, cx.attr_span);
                     }
                 }
             }
diff --git a/compiler/rustc_attr_parsing/src/attributes/stability.rs b/compiler/rustc_attr_parsing/src/attributes/stability.rs
index 407fd5e1b8b13..e8653453b39ed 100644
--- a/compiler/rustc_attr_parsing/src/attributes/stability.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/stability.rs
@@ -8,7 +8,9 @@ use rustc_errors::ErrorGuaranteed;
 use rustc_span::{Span, Symbol, sym};
 
 use super::util::parse_version;
-use super::{AcceptMapping, AttributeDuplicates, AttributeParser, SingleAttributeParser};
+use super::{
+    AcceptMapping, AttributeDuplicates, AttributeParser, OnDuplicate, SingleAttributeParser,
+};
 use crate::context::{AcceptContext, FinalizeContext};
 use crate::parser::{ArgParser, MetaItemParser};
 use crate::session_diagnostics::{self, UnsupportedLiteralReason};
@@ -119,9 +121,7 @@ pub(crate) struct ConstStabilityIndirectParser;
 impl SingleAttributeParser for ConstStabilityIndirectParser {
     const PATH: &'static [Symbol] = &[sym::rustc_const_stable_indirect];
     const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
-
-    // ignore
-    fn on_duplicate(_cx: &AcceptContext<'_>, _used: Span, _unused: Span) {}
+    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Ignore;
 
     fn convert(_cx: &AcceptContext<'_>, _args: &ArgParser<'_>) -> Option<AttributeKind> {
         Some(AttributeKind::ConstStabilityIndirect)
diff --git a/compiler/rustc_attr_parsing/src/attributes/transparency.rs b/compiler/rustc_attr_parsing/src/attributes/transparency.rs
index 0ab4cb99a6215..22d2806e41e2e 100644
--- a/compiler/rustc_attr_parsing/src/attributes/transparency.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/transparency.rs
@@ -1,8 +1,8 @@
 use rustc_attr_data_structures::AttributeKind;
 use rustc_span::hygiene::Transparency;
-use rustc_span::{Span, Symbol, sym};
+use rustc_span::{Symbol, sym};
 
-use super::{AcceptContext, AttributeDuplicates, SingleAttributeParser};
+use super::{AcceptContext, AttributeDuplicates, OnDuplicate, SingleAttributeParser};
 use crate::parser::ArgParser;
 
 pub(crate) struct TransparencyParser;
@@ -13,10 +13,9 @@ pub(crate) struct TransparencyParser;
 impl SingleAttributeParser for TransparencyParser {
     const PATH: &[Symbol] = &[sym::rustc_macro_transparency];
     const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
-
-    fn on_duplicate(cx: &crate::context::AcceptContext<'_>, used: Span, unused: Span) {
+    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Custom(|cx, used, unused| {
         cx.dcx().span_err(vec![used, unused], "multiple macro transparency attributes");
-    }
+    });
 
     fn convert(cx: &AcceptContext<'_>, args: &ArgParser<'_>) -> Option<AttributeKind> {
         match args.name_value().and_then(|nv| nv.value_as_str()) {

From 6072207a1151f212163e3c19d92ff4ea22f291b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= <jana@donsz.nl>
Date: Wed, 12 Feb 2025 13:59:08 +0100
Subject: [PATCH 09/16] introduce new lint infra

 lint on duplicates during attribute parsing
To do this we stuff them in the diagnostic context to be emitted after
hir is constructed
---
 Cargo.lock                                    |   1 +
 compiler/rustc_ast_lowering/src/expr.rs       |  17 +-
 compiler/rustc_ast_lowering/src/lib.rs        |  67 ++++-
 .../rustc_attr_data_structures/src/lib.rs     |   2 +
 .../rustc_attr_data_structures/src/lints.rs   |  14 ++
 compiler/rustc_attr_parsing/messages.ftl      |  10 +
 .../src/attributes/allow_unstable.rs          |  34 +--
 .../src/attributes/confusables.rs             |   8 +-
 .../src/attributes/deprecation.rs             |  16 +-
 .../src/attributes/inline.rs                  |  97 ++++++++
 .../rustc_attr_parsing/src/attributes/mod.rs  | 137 ++++++-----
 .../rustc_attr_parsing/src/attributes/repr.rs |  21 +-
 .../src/attributes/stability.rs               |  48 ++--
 .../src/attributes/transparency.rs            |  11 +-
 compiler/rustc_attr_parsing/src/context.rs    | 228 +++++++++++++-----
 compiler/rustc_attr_parsing/src/lib.rs        |   6 +-
 compiler/rustc_attr_parsing/src/lints.rs      |  19 ++
 compiler/rustc_attr_parsing/src/parser.rs     |  14 +-
 .../src/session_diagnostics.rs                |  13 +-
 .../src/deriving/generic/mod.rs               |   2 +-
 compiler/rustc_errors/src/lib.rs              |  16 +-
 compiler/rustc_hir/src/hir.rs                 |   5 +
 compiler/rustc_hir/src/lib.rs                 |   1 +
 compiler/rustc_hir/src/lints.rs               |  23 ++
 compiler/rustc_hir/src/stable_hash_impls.rs   |   8 +
 compiler/rustc_hir_analysis/Cargo.toml        |   1 +
 compiler/rustc_hir_analysis/src/lib.rs        |  19 +-
 compiler/rustc_lint/src/nonstandard_style.rs  |   2 +-
 compiler/rustc_middle/src/hir/mod.rs          |  16 +-
 compiler/rustc_middle/src/query/mod.rs        |   8 +
 compiler/rustc_middle/src/ty/context.rs       |  18 +-
 compiler/rustc_resolve/src/def_collector.rs   |   8 +-
 32 files changed, 663 insertions(+), 227 deletions(-)
 create mode 100644 compiler/rustc_attr_data_structures/src/lints.rs
 create mode 100644 compiler/rustc_attr_parsing/src/attributes/inline.rs
 create mode 100644 compiler/rustc_attr_parsing/src/lints.rs
 create mode 100644 compiler/rustc_hir/src/lints.rs

diff --git a/Cargo.lock b/Cargo.lock
index 93abab8469a7b..5783cd3f941c8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3787,6 +3787,7 @@ dependencies = [
  "rustc_arena",
  "rustc_ast",
  "rustc_attr_data_structures",
+ "rustc_attr_parsing",
  "rustc_data_structures",
  "rustc_errors",
  "rustc_feature",
diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs
index 537d4a2a6af6a..718edad0cc61e 100644
--- a/compiler/rustc_ast_lowering/src/expr.rs
+++ b/compiler/rustc_ast_lowering/src/expr.rs
@@ -73,16 +73,15 @@ impl<'hir> LoweringContext<'_, 'hir> {
                     // Merge attributes into the inner expression.
                     if !e.attrs.is_empty() {
                         let old_attrs = self.attrs.get(&ex.hir_id.local_id).copied().unwrap_or(&[]);
-                        let attrs = &*self.arena.alloc_from_iter(
-                            self.lower_attrs_vec(&e.attrs, e.span)
-                                .into_iter()
-                                .chain(old_attrs.iter().cloned()),
-                        );
-                        if attrs.is_empty() {
+                        let new_attrs = self
+                            .lower_attrs_vec(&e.attrs, e.span, ex.hir_id)
+                            .into_iter()
+                            .chain(old_attrs.iter().cloned());
+                        let new_attrs = &*self.arena.alloc_from_iter(new_attrs);
+                        if new_attrs.is_empty() {
                             return ex;
                         }
-
-                        self.attrs.insert(ex.hir_id.local_id, attrs);
+                        self.attrs.insert(ex.hir_id.local_id, new_attrs);
                     }
                     return ex;
                 }
@@ -2035,7 +2034,7 @@ impl<'hir> LoweringContext<'_, 'hir> {
                 let ret_expr = self.checked_return(Some(from_residual_expr));
                 self.arena.alloc(self.expr(try_span, ret_expr))
             };
-            self.lower_attrs(ret_expr.hir_id, &attrs, ret_expr.span);
+            self.lower_attrs(ret_expr.hir_id, &attrs, span);
 
             let break_pat = self.pat_cf_break(try_span, residual_local);
             self.arm(break_pat, ret_expr)
diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs
index b99df8bd7e552..249d1a99d72a5 100644
--- a/compiler/rustc_ast_lowering/src/lib.rs
+++ b/compiler/rustc_ast_lowering/src/lib.rs
@@ -51,6 +51,7 @@ use rustc_data_structures::tagged_ptr::TaggedRef;
 use rustc_errors::{DiagArgFromDisplay, DiagCtxtHandle, StashKey};
 use rustc_hir::def::{DefKind, LifetimeRes, Namespace, PartialRes, PerNS, Res};
 use rustc_hir::def_id::{CRATE_DEF_ID, LOCAL_CRATE, LocalDefId};
+use rustc_hir::lints::DelayedLint;
 use rustc_hir::{
     self as hir, AngleBrackets, ConstArg, GenericArg, HirId, ItemLocalMap, LangItem,
     LifetimeSource, LifetimeSyntax, ParamName, TraitCandidate,
@@ -141,6 +142,8 @@ struct LoweringContext<'a, 'hir> {
     allow_for_await: Arc<[Symbol]>,
     allow_async_fn_traits: Arc<[Symbol]>,
 
+    delayed_lints: Vec<DelayedLint>,
+
     attribute_parser: AttributeParser<'hir>,
 }
 
@@ -190,6 +193,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
             allow_async_iterator: [sym::gen_future, sym::async_iterator].into(),
 
             attribute_parser: AttributeParser::new(tcx.sess, tcx.features(), registered_tools),
+            delayed_lints: Vec::new(),
         }
     }
 
@@ -198,6 +202,22 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
     }
 }
 
+struct SpanLowerer {
+    is_incremental: bool,
+    def_id: LocalDefId,
+}
+
+impl SpanLowerer {
+    fn lower(&self, span: Span) -> Span {
+        if self.is_incremental {
+            span.with_parent(Some(self.def_id))
+        } else {
+            // Do not make spans relative when not using incremental compilation.
+            span
+        }
+    }
+}
+
 #[extension(trait ResolverAstLoweringExt)]
 impl ResolverAstLowering {
     fn legacy_const_generic_args(&self, expr: &Expr) -> Option<Vec<usize>> {
@@ -573,6 +593,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
             std::mem::replace(&mut self.item_local_id_counter, hir::ItemLocalId::new(1));
         let current_impl_trait_defs = std::mem::take(&mut self.impl_trait_defs);
         let current_impl_trait_bounds = std::mem::take(&mut self.impl_trait_bounds);
+        let current_delayed_lints = std::mem::take(&mut self.delayed_lints);
 
         // Do not reset `next_node_id` and `node_id_to_def_id`:
         // we want `f` to be able to refer to the `LocalDefId`s that the caller created.
@@ -606,6 +627,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         self.item_local_id_counter = current_local_counter;
         self.impl_trait_defs = current_impl_trait_defs;
         self.impl_trait_bounds = current_impl_trait_bounds;
+        self.delayed_lints = current_delayed_lints;
 
         debug_assert!(!self.children.iter().any(|(id, _)| id == &owner_id.def_id));
         self.children.push((owner_id.def_id, hir::MaybeOwner::Owner(info)));
@@ -616,6 +638,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         let mut bodies = std::mem::take(&mut self.bodies);
         let define_opaque = std::mem::take(&mut self.define_opaque);
         let trait_map = std::mem::take(&mut self.trait_map);
+        let delayed_lints = std::mem::take(&mut self.delayed_lints).into_boxed_slice();
 
         #[cfg(debug_assertions)]
         for (id, attrs) in attrs.iter() {
@@ -629,14 +652,16 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         let bodies = SortedMap::from_presorted_elements(bodies);
 
         // Don't hash unless necessary, because it's expensive.
-        let (opt_hash_including_bodies, attrs_hash) =
-            self.tcx.hash_owner_nodes(node, &bodies, &attrs, define_opaque);
+        let (opt_hash_including_bodies, attrs_hash, delayed_lints_hash) =
+            self.tcx.hash_owner_nodes(node, &bodies, &attrs, &delayed_lints, define_opaque);
         let num_nodes = self.item_local_id_counter.as_usize();
         let (nodes, parenting) = index::index_hir(self.tcx, node, &bodies, num_nodes);
         let nodes = hir::OwnerNodes { opt_hash_including_bodies, nodes, bodies };
         let attrs = hir::AttributeMap { map: attrs, opt_hash: attrs_hash, define_opaque };
+        let delayed_lints =
+            hir::lints::DelayedLints { lints: delayed_lints, opt_hash: delayed_lints_hash };
 
-        self.arena.alloc(hir::OwnerInfo { nodes, parenting, attrs, trait_map })
+        self.arena.alloc(hir::OwnerInfo { nodes, parenting, attrs, trait_map, delayed_lints })
     }
 
     /// This method allocates a new `HirId` for the given `NodeId`.
@@ -759,15 +784,17 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         })
     }
 
+    fn span_lowerer(&self) -> SpanLowerer {
+        SpanLowerer {
+            is_incremental: self.tcx.sess.opts.incremental.is_some(),
+            def_id: self.current_hir_id_owner.def_id,
+        }
+    }
+
     /// Intercept all spans entering HIR.
     /// Mark a span as relative to the current owning item.
     fn lower_span(&self, span: Span) -> Span {
-        if self.tcx.sess.opts.incremental.is_some() {
-            span.with_parent(Some(self.current_hir_id_owner.def_id))
-        } else {
-            // Do not make spans relative when not using incremental compilation.
-            span
-        }
+        self.span_lowerer().lower(span)
     }
 
     fn lower_ident(&self, ident: Ident) -> Ident {
@@ -889,7 +916,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         if attrs.is_empty() {
             &[]
         } else {
-            let lowered_attrs = self.lower_attrs_vec(attrs, self.lower_span(target_span));
+            let lowered_attrs = self.lower_attrs_vec(attrs, self.lower_span(target_span), id);
 
             debug_assert_eq!(id.owner, self.current_hir_id_owner);
             let ret = self.arena.alloc_from_iter(lowered_attrs);
@@ -909,9 +936,23 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         }
     }
 
-    fn lower_attrs_vec(&self, attrs: &[Attribute], target_span: Span) -> Vec<hir::Attribute> {
-        self.attribute_parser
-            .parse_attribute_list(attrs, target_span, OmitDoc::Lower, |s| self.lower_span(s))
+    fn lower_attrs_vec(
+        &mut self,
+        attrs: &[Attribute],
+        target_span: Span,
+        target_hir_id: HirId,
+    ) -> Vec<hir::Attribute> {
+        let l = self.span_lowerer();
+        self.attribute_parser.parse_attribute_list(
+            attrs,
+            target_span,
+            target_hir_id,
+            OmitDoc::Lower,
+            |s| l.lower(s),
+            |l| {
+                self.delayed_lints.push(DelayedLint::AttributeParsing(l));
+            },
+        )
     }
 
     fn alias_attrs(&mut self, id: HirId, target_id: HirId) {
diff --git a/compiler/rustc_attr_data_structures/src/lib.rs b/compiler/rustc_attr_data_structures/src/lib.rs
index dbfc95b047ae4..b0fc19d1cd7ba 100644
--- a/compiler/rustc_attr_data_structures/src/lib.rs
+++ b/compiler/rustc_attr_data_structures/src/lib.rs
@@ -8,6 +8,8 @@ mod attributes;
 mod stability;
 mod version;
 
+pub mod lints;
+
 use std::num::NonZero;
 
 pub use attributes::*;
diff --git a/compiler/rustc_attr_data_structures/src/lints.rs b/compiler/rustc_attr_data_structures/src/lints.rs
new file mode 100644
index 0000000000000..7e3664b2263b4
--- /dev/null
+++ b/compiler/rustc_attr_data_structures/src/lints.rs
@@ -0,0 +1,14 @@
+use rustc_macros::HashStable_Generic;
+use rustc_span::Span;
+
+#[derive(Clone, Debug, HashStable_Generic)]
+pub struct AttributeLint<Id> {
+    pub id: Id,
+    pub span: Span,
+    pub kind: AttributeLintKind,
+}
+
+#[derive(Clone, Debug, HashStable_Generic)]
+pub enum AttributeLintKind {
+    UnusedDuplicate { this: Span, other: Span, warning: bool },
+}
diff --git a/compiler/rustc_attr_parsing/messages.ftl b/compiler/rustc_attr_parsing/messages.ftl
index 45174c9582d33..573854afa746c 100644
--- a/compiler/rustc_attr_parsing/messages.ftl
+++ b/compiler/rustc_attr_parsing/messages.ftl
@@ -131,7 +131,17 @@ attr_parsing_unsupported_literal_generic =
 attr_parsing_unsupported_literal_suggestion =
     consider removing the prefix
 
+attr_parsing_unused_duplicate =
+    unused attribute
+    .suggestion = remove this attribute
+    .note = attribute also specified here
+    .warn = {-attr_parsing_previously_accepted}
+
+
 attr_parsing_unused_multiple =
     multiple `{$name}` attributes
     .suggestion = remove this attribute
     .note = attribute also specified here
+
+-attr_parsing_perviously_accepted =
+    this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
diff --git a/compiler/rustc_attr_parsing/src/attributes/allow_unstable.rs b/compiler/rustc_attr_parsing/src/attributes/allow_unstable.rs
index d0465546b7314..2349ee15fe61e 100644
--- a/compiler/rustc_attr_parsing/src/attributes/allow_unstable.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/allow_unstable.rs
@@ -4,41 +4,43 @@ use rustc_attr_data_structures::AttributeKind;
 use rustc_span::{Span, Symbol, sym};
 
 use super::{CombineAttributeParser, ConvertFn};
-use crate::context::AcceptContext;
+use crate::context::{AcceptContext, Stage};
 use crate::parser::ArgParser;
 use crate::session_diagnostics;
 
 pub(crate) struct AllowInternalUnstableParser;
-impl CombineAttributeParser for AllowInternalUnstableParser {
+impl<S: Stage> CombineAttributeParser<S> for AllowInternalUnstableParser {
     const PATH: &'static [Symbol] = &[sym::allow_internal_unstable];
     type Item = (Symbol, Span);
     const CONVERT: ConvertFn<Self::Item> = AttributeKind::AllowInternalUnstable;
 
-    fn extend<'a>(
-        cx: &'a AcceptContext<'a>,
-        args: &'a ArgParser<'a>,
-    ) -> impl IntoIterator<Item = Self::Item> + 'a {
-        parse_unstable(cx, args, Self::PATH[0]).into_iter().zip(iter::repeat(cx.attr_span))
+    fn extend<'c>(
+        cx: &'c mut AcceptContext<'_, '_, S>,
+        args: &'c ArgParser<'_>,
+    ) -> impl IntoIterator<Item = Self::Item> {
+        parse_unstable(cx, args, <Self as CombineAttributeParser<S>>::PATH[0])
+            .into_iter()
+            .zip(iter::repeat(cx.attr_span))
     }
 }
 
 pub(crate) struct AllowConstFnUnstableParser;
-impl CombineAttributeParser for AllowConstFnUnstableParser {
+impl<S: Stage> CombineAttributeParser<S> for AllowConstFnUnstableParser {
     const PATH: &'static [Symbol] = &[sym::rustc_allow_const_fn_unstable];
     type Item = Symbol;
     const CONVERT: ConvertFn<Self::Item> = AttributeKind::AllowConstFnUnstable;
 
-    fn extend<'a>(
-        cx: &'a AcceptContext<'a>,
-        args: &'a ArgParser<'a>,
-    ) -> impl IntoIterator<Item = Self::Item> + 'a {
-        parse_unstable(cx, args, Self::PATH[0])
+    fn extend<'c>(
+        cx: &'c mut AcceptContext<'_, '_, S>,
+        args: &'c ArgParser<'_>,
+    ) -> impl IntoIterator<Item = Self::Item> + 'c {
+        parse_unstable(cx, args, <Self as CombineAttributeParser<S>>::PATH[0])
     }
 }
 
-fn parse_unstable<'a>(
-    cx: &AcceptContext<'_>,
-    args: &'a ArgParser<'a>,
+fn parse_unstable<S: Stage>(
+    cx: &AcceptContext<'_, '_, S>,
+    args: &ArgParser<'_>,
     symbol: Symbol,
 ) -> impl IntoIterator<Item = Symbol> {
     let mut res = Vec::new();
diff --git a/compiler/rustc_attr_parsing/src/attributes/confusables.rs b/compiler/rustc_attr_parsing/src/attributes/confusables.rs
index 6cff952fcf229..afd3c012f05ad 100644
--- a/compiler/rustc_attr_parsing/src/attributes/confusables.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/confusables.rs
@@ -3,7 +3,7 @@ use rustc_span::{Span, Symbol, sym};
 use thin_vec::ThinVec;
 
 use super::{AcceptMapping, AttributeParser};
-use crate::context::FinalizeContext;
+use crate::context::{FinalizeContext, Stage};
 use crate::session_diagnostics;
 
 #[derive(Default)]
@@ -12,8 +12,8 @@ pub(crate) struct ConfusablesParser {
     first_span: Option<Span>,
 }
 
-impl AttributeParser for ConfusablesParser {
-    const ATTRIBUTES: AcceptMapping<Self> = &[(&[sym::rustc_confusables], |this, cx, args| {
+impl<S: Stage> AttributeParser<S> for ConfusablesParser {
+    const ATTRIBUTES: AcceptMapping<Self, S> = &[(&[sym::rustc_confusables], |this, cx, args| {
         let Some(list) = args.list() else {
             // FIXME(jdonszelmann): error when not a list? Bring validation code here.
             //       NOTE: currently subsequent attributes are silently ignored using
@@ -45,7 +45,7 @@ impl AttributeParser for ConfusablesParser {
         this.first_span.get_or_insert(cx.attr_span);
     })];
 
-    fn finalize(self, _cx: &FinalizeContext<'_>) -> Option<AttributeKind> {
+    fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
         if self.confusables.is_empty() {
             return None;
         }
diff --git a/compiler/rustc_attr_parsing/src/attributes/deprecation.rs b/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
index 1b18ada70fc99..ba1bdb9fd02f0 100644
--- a/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
@@ -2,16 +2,16 @@ use rustc_attr_data_structures::{AttributeKind, DeprecatedSince, Deprecation};
 use rustc_span::{Span, Symbol, sym};
 
 use super::util::parse_version;
-use super::{AttributeDuplicates, OnDuplicate, SingleAttributeParser};
-use crate::context::AcceptContext;
+use super::{AttributeOrder, OnDuplicate, SingleAttributeParser};
+use crate::context::{AcceptContext, Stage};
 use crate::parser::ArgParser;
 use crate::session_diagnostics;
 use crate::session_diagnostics::UnsupportedLiteralReason;
 
 pub(crate) struct DeprecationParser;
 
-fn get(
-    cx: &AcceptContext<'_>,
+fn get<S: Stage>(
+    cx: &AcceptContext<'_, '_, S>,
     name: Symbol,
     param_span: Span,
     arg: &ArgParser<'_>,
@@ -41,12 +41,12 @@ fn get(
     }
 }
 
-impl SingleAttributeParser for DeprecationParser {
+impl<S: Stage> SingleAttributeParser<S> for DeprecationParser {
     const PATH: &'static [Symbol] = &[sym::deprecated];
-    const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
-    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Error;
+    const ATTRIBUTE_ORDER: AttributeOrder = AttributeOrder::KeepFirst;
+    const ON_DUPLICATE: OnDuplicate<S> = OnDuplicate::Error;
 
-    fn convert(cx: &AcceptContext<'_>, args: &ArgParser<'_>) -> Option<AttributeKind> {
+    fn convert(cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser<'_>) -> Option<AttributeKind> {
         let features = cx.features();
 
         let mut since = None;
diff --git a/compiler/rustc_attr_parsing/src/attributes/inline.rs b/compiler/rustc_attr_parsing/src/attributes/inline.rs
new file mode 100644
index 0000000000000..c7f82082c2eec
--- /dev/null
+++ b/compiler/rustc_attr_parsing/src/attributes/inline.rs
@@ -0,0 +1,97 @@
+// FIXME(jdonszelmann): merge these two parsers and error when both attributes are present here.
+//                      note: need to model better how duplicate attr errors work when not using
+//                      SingleAttributeParser which is what we have two of here.
+
+use rustc_attr_data_structures::lints::AttributeLintKind;
+use rustc_attr_data_structures::{AttributeKind, InlineAttr};
+use rustc_feature::{AttributeTemplate, template};
+use rustc_span::{Symbol, sym};
+
+use super::{AcceptContext, AttributeOrder, OnDuplicate};
+use crate::attributes::SingleAttributeParser;
+use crate::context::Stage;
+use crate::parser::ArgParser;
+
+pub(crate) struct InlineParser;
+
+impl<S: Stage> SingleAttributeParser<S> for InlineParser {
+    const PATH: &'static [Symbol] = &[sym::inline];
+    const ATTRIBUTE_ORDER: AttributeOrder = AttributeOrder::KeepLast;
+    const ON_DUPLICATE: OnDuplicate<S> = OnDuplicate::WarnButFutureError;
+    const TEMPLATE: AttributeTemplate = template!(Word, List: "always|never");
+
+    fn convert(cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser<'_>) -> Option<AttributeKind> {
+        match args {
+            ArgParser::NoArgs => Some(AttributeKind::Inline(InlineAttr::Hint, cx.attr_span)),
+            ArgParser::List(list) => {
+                let Some(l) = list.single() else {
+                    cx.expected_single_argument(list.span);
+                    return None;
+                };
+
+                match l.meta_item().and_then(|i| i.word_without_args().map(|i| i.name)) {
+                    Some(sym::always) => {
+                        Some(AttributeKind::Inline(InlineAttr::Always, cx.attr_span))
+                    }
+                    Some(sym::never) => {
+                        Some(AttributeKind::Inline(InlineAttr::Never, cx.attr_span))
+                    }
+                    _ => {
+                        cx.expected_specific_argument(l.span(), vec!["always", "never"]);
+                        return None;
+                    }
+                }
+            }
+            ArgParser::NameValue(_) => {
+                let suggestions =
+                    <Self as SingleAttributeParser<S>>::TEMPLATE.suggestions(false, "inline");
+                cx.emit_lint(
+                    AttributeLintKind::IllFormedAttributeInput { suggestions },
+                    cx.attr_span,
+                );
+                return None;
+            }
+        }
+    }
+}
+
+pub(crate) struct RustcForceInlineParser;
+
+impl<S: Stage> SingleAttributeParser<S> for RustcForceInlineParser {
+    const PATH: &'static [Symbol] = &[sym::rustc_force_inline];
+    const ATTRIBUTE_ORDER: AttributeOrder = AttributeOrder::KeepLast;
+    const ON_DUPLICATE: OnDuplicate<S> = OnDuplicate::WarnButFutureError;
+    const TEMPLATE: AttributeTemplate = template!(Word, List: "reason", NameValueStr: "reason");
+
+    fn convert(cx: &AcceptContext<'_, '_, S>, args: &ArgParser<'_>) -> Option<AttributeKind> {
+        let reason = match args {
+            ArgParser::NoArgs => None,
+            ArgParser::List(list) => {
+                let Some(l) = list.single() else {
+                    cx.expected_single_argument(list.span);
+                    return None;
+                };
+
+                let Some(reason) = l.lit().and_then(|i| i.kind.str()) else {
+                    cx.expected_string_literal(l.span());
+                    return None;
+                };
+
+                Some(reason)
+            }
+            ArgParser::NameValue(v) => {
+                let Some(reason) = v.value_as_str() else {
+                    cx.expected_string_literal(v.value_span);
+                    return None;
+                };
+
+                Some(reason)
+            }
+        };
+
+        Some(AttributeKind::Inline(
+            InlineAttr::Force { attr_span: cx.attr_span, reason },
+            cx.attr_span,
+        ))
+    }
+}
diff --git a/compiler/rustc_attr_parsing/src/attributes/mod.rs b/compiler/rustc_attr_parsing/src/attributes/mod.rs
index 852615b4fce6c..1d239bd43f40d 100644
--- a/compiler/rustc_attr_parsing/src/attributes/mod.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/mod.rs
@@ -17,10 +17,11 @@
 use std::marker::PhantomData;
 
 use rustc_attr_data_structures::AttributeKind;
+use rustc_attr_data_structures::lints::AttributeLintKind;
 use rustc_span::{Span, Symbol};
 use thin_vec::ThinVec;
 
-use crate::context::{AcceptContext, FinalizeContext};
+use crate::context::{AcceptContext, FinalizeContext, Stage};
 use crate::parser::ArgParser;
 use crate::session_diagnostics::UnusedMultiple;
 
@@ -33,8 +34,8 @@ pub(crate) mod stability;
 pub(crate) mod transparency;
 pub(crate) mod util;
 
-type AcceptFn<T> = fn(&mut T, &AcceptContext<'_>, &ArgParser<'_>);
-type AcceptMapping<T> = &'static [(&'static [Symbol], AcceptFn<T>)];
+type AcceptFn<T, S> = for<'sess> fn(&mut T, &mut AcceptContext<'_, 'sess, S>, &ArgParser<'_>);
+type AcceptMapping<T, S> = &'static [(&'static [Symbol], AcceptFn<T, S>)];
 
 /// An [`AttributeParser`] is a type which searches for syntactic attributes.
 ///
@@ -55,11 +56,11 @@ type AcceptMapping<T> = &'static [(&'static [Symbol], AcceptFn<T>)];
 ///
 /// For a simpler attribute parsing interface, consider using [`SingleAttributeParser`]
 /// or [`CombineAttributeParser`] instead.
-pub(crate) trait AttributeParser: Default + 'static {
+pub(crate) trait AttributeParser<S: Stage>: Default + 'static {
     /// The symbols for the attributes that this parser is interested in.
     ///
     /// If an attribute has this symbol, the `accept` function will be called on it.
-    const ATTRIBUTES: AcceptMapping<Self>;
+    const ATTRIBUTES: AcceptMapping<Self, S>;
 
     /// The parser has gotten a chance to accept the attributes on an item,
     /// here it can produce an attribute.
@@ -69,7 +70,7 @@ pub(crate) trait AttributeParser: Default + 'static {
     /// that'd be equivalent to unconditionally applying an attribute to
     /// every single syntax item that could have attributes applied to it.
     /// Your accept mappings should determine whether this returns something.
-    fn finalize(self, cx: &FinalizeContext<'_>) -> Option<AttributeKind>;
+    fn finalize(self, cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind>;
 }
 
 /// Alternative to [`AttributeParser`] that automatically handles state management.
@@ -81,54 +82,60 @@ pub(crate) trait AttributeParser: Default + 'static {
 ///
 /// [`SingleAttributeParser`] can only convert attributes one-to-one, and cannot combine multiple
 /// attributes together like is necessary for `#[stable()]` and `#[unstable()]` for example.
-pub(crate) trait SingleAttributeParser: 'static {
+pub(crate) trait SingleAttributeParser<S: Stage>: 'static {
     const PATH: &'static [Symbol];
-
-    const ON_DUPLICATE_STRATEGY: AttributeDuplicates;
-    const ON_DUPLICATE: OnDuplicate;
+    const ATTRIBUTE_ORDER: AttributeOrder;
+    const ON_DUPLICATE: OnDuplicate<S>;
 
     /// Converts a single syntactical attribute to a single semantic attribute, or [`AttributeKind`]
-    fn convert(cx: &AcceptContext<'_>, args: &ArgParser<'_>) -> Option<AttributeKind>;
+    fn convert(cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser<'_>) -> Option<AttributeKind>;
 }
 
-pub(crate) struct Single<T: SingleAttributeParser>(PhantomData<T>, Option<(AttributeKind, Span)>);
+pub(crate) struct Single<T: SingleAttributeParser<S>, S: Stage>(
+    PhantomData<(S, T)>,
+    Option<(AttributeKind, Span)>,
+);
 
-impl<T: SingleAttributeParser> Default for Single<T> {
+impl<T: SingleAttributeParser<S>, S: Stage> Default for Single<T, S> {
     fn default() -> Self {
         Self(Default::default(), Default::default())
     }
 }
 
-impl<T: SingleAttributeParser> AttributeParser for Single<T> {
-    const ATTRIBUTES: AcceptMapping<Self> = &[(T::PATH, |group: &mut Single<T>, cx, args| {
-        if let Some(pa) = T::convert(cx, args) {
-            match T::ON_DUPLICATE_STRATEGY {
-                // keep the first and error
-                AttributeDuplicates::ErrorFollowing => {
-                    if let Some((_, unused)) = group.1 {
-                        T::ON_DUPLICATE.exec::<T>(cx, cx.attr_span, unused);
-                        return;
+impl<T: SingleAttributeParser<S>, S: Stage> AttributeParser<S> for Single<T, S> {
+    const ATTRIBUTES: AcceptMapping<Self, S> =
+        &[(T::PATH, |group: &mut Single<T, S>, cx, args| {
+            if let Some(pa) = T::convert(cx, args) {
+                match T::ATTRIBUTE_ORDER {
+                    // keep the first and report immediately. ignore this attribute
+                    AttributeOrder::KeepFirst => {
+                        if let Some((_, unused)) = group.1 {
+                            T::ON_DUPLICATE.exec::<T>(cx, cx.attr_span, unused);
+                            return;
+                        }
                     }
-                }
-                // keep the new one and warn about the previous,
-                // then replace
-                AttributeDuplicates::FutureWarnPreceding => {
-                    if let Some((_, used)) = group.1 {
-                        T::ON_DUPLICATE.exec::<T>(cx, used, cx.attr_span);
+                    // keep the new one and warn about the previous,
+                    // then replace
+                    AttributeOrder::KeepLast => {
+                        if let Some((_, used)) = group.1 {
+                            T::ON_DUPLICATE.exec::<T>(cx, used, cx.attr_span);
+                        }
                     }
                 }
-            }
 
-            group.1 = Some((pa, cx.attr_span));
-        }
-    })];
+                group.1 = Some((pa, cx.attr_span));
+            }
+        })];
 
-    fn finalize(self, _cx: &FinalizeContext<'_>) -> Option<AttributeKind> {
+    fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
         Some(self.1?.0)
     }
 }
 
-pub(crate) enum OnDuplicate {
+// FIXME(jdonszelmann): logic is implemented but the attribute parsers needing
+// them will be merged in another PR
+#[allow(unused)]
+pub(crate) enum OnDuplicate<S: Stage> {
     /// Give a default warning
     Warn,
 
@@ -146,18 +153,25 @@ pub(crate) enum OnDuplicate {
     /// - `unused` is the span of the attribute that was unused or bad because of some
     ///   duplicate reason (see [`AttributeDuplicates`])
     /// - `used` is the span of the attribute that was used in favor of the unused attribute
-    Custom(fn(cx: &AcceptContext<'_>, used: Span, unused: Span)),
+    Custom(fn(cx: &AcceptContext<'_, '_, S>, used: Span, unused: Span)),
 }
 
-impl OnDuplicate {
-    fn exec<P: SingleAttributeParser>(&self, cx: &AcceptContext<'_>, used: Span, unused: Span) {
+impl<S: Stage> OnDuplicate<S> {
+    fn exec<P: SingleAttributeParser<S>>(
+        &self,
+        cx: &mut AcceptContext<'_, '_, S>,
+        used: Span,
+        unused: Span,
+    ) {
         match self {
-            OnDuplicate::Warn => {
-                todo!()
-            }
-            OnDuplicate::WarnButFutureError => {
-                todo!()
-            }
+            OnDuplicate::Warn => cx.emit_lint(
+                AttributeLintKind::UnusedDuplicate { this: unused, other: used, warning: false },
+                unused,
+            ),
+            OnDuplicate::WarnButFutureError => cx.emit_lint(
+                AttributeLintKind::UnusedDuplicate { this: unused, other: used, warning: true },
+                unused,
+            ),
             OnDuplicate::Error => {
                 cx.emit_err(UnusedMultiple {
                     this: used,
@@ -172,14 +186,17 @@ impl OnDuplicate {
         }
     }
 }
-
-pub(crate) enum AttributeDuplicates {
+//
+// FIXME(jdonszelmann): logic is implemented but the attribute parsers needing
+// them will be merged in another PR
+#[allow(unused)]
+pub(crate) enum AttributeOrder {
     /// Duplicates after the first attribute will be an error.
     ///
     /// This should be used where duplicates would be ignored, but carry extra
     /// meaning that could cause confusion. For example, `#[stable(since="1.0")]
     /// #[stable(since="2.0")]`, which version should be used for `stable`?
-    ErrorFollowing,
+    KeepFirst,
 
     /// Duplicates preceding the last instance of the attribute will be a
     /// warning, with a note that this will be an error in the future.
@@ -187,7 +204,7 @@ pub(crate) enum AttributeDuplicates {
     /// This is the same as `FutureWarnFollowing`, except the last attribute is
     /// the one that is "used". Ideally these can eventually migrate to
     /// `ErrorPreceding`.
-    FutureWarnPreceding,
+    KeepLast,
 }
 
 type ConvertFn<E> = fn(ThinVec<E>) -> AttributeKind;
@@ -199,35 +216,35 @@ type ConvertFn<E> = fn(ThinVec<E>) -> AttributeKind;
 ///
 /// [`CombineAttributeParser`] can only convert a single kind of attribute, and cannot combine multiple
 /// attributes together like is necessary for `#[stable()]` and `#[unstable()]` for example.
-pub(crate) trait CombineAttributeParser: 'static {
+pub(crate) trait CombineAttributeParser<S: Stage>: 'static {
     const PATH: &'static [Symbol];
 
     type Item;
     const CONVERT: ConvertFn<Self::Item>;
 
     /// Converts a single syntactical attribute to a number of elements of the semantic attribute, or [`AttributeKind`]
-    fn extend<'a>(
-        cx: &'a AcceptContext<'a>,
-        args: &'a ArgParser<'a>,
-    ) -> impl IntoIterator<Item = Self::Item> + 'a;
+    fn extend<'c>(
+        cx: &'c mut AcceptContext<'_, '_, S>,
+        args: &'c ArgParser<'_>,
+    ) -> impl IntoIterator<Item = Self::Item> + 'c;
 }
 
-pub(crate) struct Combine<T: CombineAttributeParser>(
-    PhantomData<T>,
-    ThinVec<<T as CombineAttributeParser>::Item>,
+pub(crate) struct Combine<T: CombineAttributeParser<S>, S: Stage>(
+    PhantomData<(S, T)>,
+    ThinVec<<T as CombineAttributeParser<S>>::Item>,
 );
 
-impl<T: CombineAttributeParser> Default for Combine<T> {
+impl<T: CombineAttributeParser<S>, S: Stage> Default for Combine<T, S> {
     fn default() -> Self {
         Self(Default::default(), Default::default())
     }
 }
 
-impl<T: CombineAttributeParser> AttributeParser for Combine<T> {
-    const ATTRIBUTES: AcceptMapping<Self> =
-        &[(T::PATH, |group: &mut Combine<T>, cx, args| group.1.extend(T::extend(cx, args)))];
+impl<T: CombineAttributeParser<S>, S: Stage> AttributeParser<S> for Combine<T, S> {
+    const ATTRIBUTES: AcceptMapping<Self, S> =
+        &[(T::PATH, |group: &mut Combine<T, S>, cx, args| group.1.extend(T::extend(cx, args)))];
 
-    fn finalize(self, _cx: &FinalizeContext<'_>) -> Option<AttributeKind> {
+    fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
         if self.1.is_empty() { None } else { Some(T::CONVERT(self.1)) }
     }
 }
diff --git a/compiler/rustc_attr_parsing/src/attributes/repr.rs b/compiler/rustc_attr_parsing/src/attributes/repr.rs
index 69316541e1919..8b2981ddb08c7 100644
--- a/compiler/rustc_attr_parsing/src/attributes/repr.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/repr.rs
@@ -4,7 +4,7 @@ use rustc_attr_data_structures::{AttributeKind, IntType, ReprAttr};
 use rustc_span::{DUMMY_SP, Span, Symbol, sym};
 
 use super::{CombineAttributeParser, ConvertFn};
-use crate::context::AcceptContext;
+use crate::context::{AcceptContext, Stage};
 use crate::parser::{ArgParser, MetaItemListParser, MetaItemParser};
 use crate::session_diagnostics;
 use crate::session_diagnostics::IncorrectReprFormatGenericCause;
@@ -19,15 +19,15 @@ use crate::session_diagnostics::IncorrectReprFormatGenericCause;
 // FIXME(jdonszelmann): is a vec the right representation here even? isn't it just a struct?
 pub(crate) struct ReprParser;
 
-impl CombineAttributeParser for ReprParser {
+impl<S: Stage> CombineAttributeParser<S> for ReprParser {
     type Item = (ReprAttr, Span);
     const PATH: &'static [Symbol] = &[sym::repr];
     const CONVERT: ConvertFn<Self::Item> = AttributeKind::Repr;
 
-    fn extend<'a>(
-        cx: &'a AcceptContext<'a>,
-        args: &'a ArgParser<'a>,
-    ) -> impl IntoIterator<Item = Self::Item> + 'a {
+    fn extend<'c>(
+        cx: &'c mut AcceptContext<'_, '_, S>,
+        args: &'c ArgParser<'_>,
+    ) -> impl IntoIterator<Item = Self::Item> + 'c {
         let mut reprs = Vec::new();
 
         let Some(list) = args.list() else {
@@ -91,7 +91,10 @@ fn int_type_of_word(s: Symbol) -> Option<IntType> {
     }
 }
 
-fn parse_repr(cx: &AcceptContext<'_>, param: &MetaItemParser<'_>) -> Option<ReprAttr> {
+fn parse_repr<S: Stage>(
+    cx: &AcceptContext<'_, '_, S>,
+    param: &MetaItemParser<'_>,
+) -> Option<ReprAttr> {
     use ReprAttr::*;
 
     // FIXME(jdonszelmann): invert the parsing here to match on the word first and then the
@@ -180,8 +183,8 @@ enum AlignKind {
     Align,
 }
 
-fn parse_repr_align(
-    cx: &AcceptContext<'_>,
+fn parse_repr_align<S: Stage>(
+    cx: &AcceptContext<'_, '_, S>,
     list: &MetaItemListParser<'_>,
     param_span: Span,
     align_kind: AlignKind,
diff --git a/compiler/rustc_attr_parsing/src/attributes/stability.rs b/compiler/rustc_attr_parsing/src/attributes/stability.rs
index e8653453b39ed..d8cf56a9830c0 100644
--- a/compiler/rustc_attr_parsing/src/attributes/stability.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/stability.rs
@@ -8,10 +8,8 @@ use rustc_errors::ErrorGuaranteed;
 use rustc_span::{Span, Symbol, sym};
 
 use super::util::parse_version;
-use super::{
-    AcceptMapping, AttributeDuplicates, AttributeParser, OnDuplicate, SingleAttributeParser,
-};
-use crate::context::{AcceptContext, FinalizeContext};
+use super::{AcceptMapping, AttributeOrder, AttributeParser, OnDuplicate, SingleAttributeParser};
+use crate::context::{AcceptContext, FinalizeContext, Stage};
 use crate::parser::{ArgParser, MetaItemParser};
 use crate::session_diagnostics::{self, UnsupportedLiteralReason};
 
@@ -33,7 +31,7 @@ pub(crate) struct StabilityParser {
 
 impl StabilityParser {
     /// Checks, and emits an error when a stability (or unstability) was already set, which would be a duplicate.
-    fn check_duplicate(&self, cx: &AcceptContext<'_>) -> bool {
+    fn check_duplicate<S: Stage>(&self, cx: &AcceptContext<'_, '_, S>) -> bool {
         if let Some((_, _)) = self.stability {
             cx.emit_err(session_diagnostics::MultipleStabilityLevels { span: cx.attr_span });
             true
@@ -43,8 +41,8 @@ impl StabilityParser {
     }
 }
 
-impl AttributeParser for StabilityParser {
-    const ATTRIBUTES: AcceptMapping<Self> = &[
+impl<S: Stage> AttributeParser<S> for StabilityParser {
+    const ATTRIBUTES: AcceptMapping<Self, S> = &[
         (&[sym::stable], |this, cx, args| {
             reject_outside_std!(cx);
             if !this.check_duplicate(cx)
@@ -67,7 +65,7 @@ impl AttributeParser for StabilityParser {
         }),
     ];
 
-    fn finalize(mut self, cx: &FinalizeContext<'_>) -> Option<AttributeKind> {
+    fn finalize(mut self, cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
         if let Some(atum) = self.allowed_through_unstable_modules {
             if let Some((
                 Stability {
@@ -97,8 +95,8 @@ pub(crate) struct BodyStabilityParser {
     stability: Option<(DefaultBodyStability, Span)>,
 }
 
-impl AttributeParser for BodyStabilityParser {
-    const ATTRIBUTES: AcceptMapping<Self> =
+impl<S: Stage> AttributeParser<S> for BodyStabilityParser {
+    const ATTRIBUTES: AcceptMapping<Self, S> =
         &[(&[sym::rustc_default_body_unstable], |this, cx, args| {
             reject_outside_std!(cx);
             if this.stability.is_some() {
@@ -109,7 +107,7 @@ impl AttributeParser for BodyStabilityParser {
             }
         })];
 
-    fn finalize(self, _cx: &FinalizeContext<'_>) -> Option<AttributeKind> {
+    fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
         let (stability, span) = self.stability?;
 
         Some(AttributeKind::BodyStability { stability, span })
@@ -118,12 +116,12 @@ impl AttributeParser for BodyStabilityParser {
 
 pub(crate) struct ConstStabilityIndirectParser;
 // FIXME(jdonszelmann): single word attribute group when we have these
-impl SingleAttributeParser for ConstStabilityIndirectParser {
+impl<S: Stage> SingleAttributeParser<S> for ConstStabilityIndirectParser {
     const PATH: &'static [Symbol] = &[sym::rustc_const_stable_indirect];
-    const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
-    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Ignore;
+    const ATTRIBUTE_ORDER: AttributeOrder = AttributeOrder::KeepFirst;
+    const ON_DUPLICATE: OnDuplicate<S> = OnDuplicate::Ignore;
 
-    fn convert(_cx: &AcceptContext<'_>, _args: &ArgParser<'_>) -> Option<AttributeKind> {
+    fn convert(_cx: &mut AcceptContext<'_, '_, S>, _args: &ArgParser<'_>) -> Option<AttributeKind> {
         Some(AttributeKind::ConstStabilityIndirect)
     }
 }
@@ -136,7 +134,7 @@ pub(crate) struct ConstStabilityParser {
 
 impl ConstStabilityParser {
     /// Checks, and emits an error when a stability (or unstability) was already set, which would be a duplicate.
-    fn check_duplicate(&self, cx: &AcceptContext<'_>) -> bool {
+    fn check_duplicate<S: Stage>(&self, cx: &AcceptContext<'_, '_, S>) -> bool {
         if let Some((_, _)) = self.stability {
             cx.emit_err(session_diagnostics::MultipleStabilityLevels { span: cx.attr_span });
             true
@@ -146,8 +144,8 @@ impl ConstStabilityParser {
     }
 }
 
-impl AttributeParser for ConstStabilityParser {
-    const ATTRIBUTES: AcceptMapping<Self> = &[
+impl<S: Stage> AttributeParser<S> for ConstStabilityParser {
+    const ATTRIBUTES: AcceptMapping<Self, S> = &[
         (&[sym::rustc_const_stable], |this, cx, args| {
             reject_outside_std!(cx);
 
@@ -177,7 +175,7 @@ impl AttributeParser for ConstStabilityParser {
         }),
     ];
 
-    fn finalize(mut self, cx: &FinalizeContext<'_>) -> Option<AttributeKind> {
+    fn finalize(mut self, cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
         if self.promotable {
             if let Some((ref mut stab, _)) = self.stability {
                 stab.promotable = true;
@@ -197,8 +195,8 @@ impl AttributeParser for ConstStabilityParser {
 ///
 /// Emits an error when either the option was already Some, or the arguments weren't of form
 /// `name = value`
-fn insert_value_into_option_or_error(
-    cx: &AcceptContext<'_>,
+fn insert_value_into_option_or_error<S: Stage>(
+    cx: &AcceptContext<'_, '_, S>,
     param: &MetaItemParser<'_>,
     item: &mut Option<Symbol>,
 ) -> Option<()> {
@@ -224,8 +222,8 @@ fn insert_value_into_option_or_error(
 
 /// Read the content of a `stable`/`rustc_const_stable` attribute, and return the feature name and
 /// its stability information.
-pub(crate) fn parse_stability(
-    cx: &AcceptContext<'_>,
+pub(crate) fn parse_stability<S: Stage>(
+    cx: &AcceptContext<'_, '_, S>,
     args: &ArgParser<'_>,
 ) -> Option<(Symbol, StabilityLevel)> {
     let mut feature = None;
@@ -290,8 +288,8 @@ pub(crate) fn parse_stability(
 
 // Read the content of a `unstable`/`rustc_const_unstable`/`rustc_default_body_unstable`
 /// attribute, and return the feature name and its stability information.
-pub(crate) fn parse_unstability(
-    cx: &AcceptContext<'_>,
+pub(crate) fn parse_unstability<S: Stage>(
+    cx: &AcceptContext<'_, '_, S>,
     args: &ArgParser<'_>,
 ) -> Option<(Symbol, StabilityLevel)> {
     let mut feature = None;
diff --git a/compiler/rustc_attr_parsing/src/attributes/transparency.rs b/compiler/rustc_attr_parsing/src/attributes/transparency.rs
index 22d2806e41e2e..1de3a9cfeba90 100644
--- a/compiler/rustc_attr_parsing/src/attributes/transparency.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/transparency.rs
@@ -2,7 +2,8 @@ use rustc_attr_data_structures::AttributeKind;
 use rustc_span::hygiene::Transparency;
 use rustc_span::{Symbol, sym};
 
-use super::{AcceptContext, AttributeDuplicates, OnDuplicate, SingleAttributeParser};
+use super::{AcceptContext, AttributeOrder, OnDuplicate, SingleAttributeParser};
+use crate::context::Stage;
 use crate::parser::ArgParser;
 
 pub(crate) struct TransparencyParser;
@@ -10,14 +11,14 @@ pub(crate) struct TransparencyParser;
 // FIXME(jdonszelmann): make these proper diagnostics
 #[allow(rustc::untranslatable_diagnostic)]
 #[allow(rustc::diagnostic_outside_of_impl)]
-impl SingleAttributeParser for TransparencyParser {
+impl<S: Stage> SingleAttributeParser<S> for TransparencyParser {
     const PATH: &[Symbol] = &[sym::rustc_macro_transparency];
-    const ON_DUPLICATE_STRATEGY: AttributeDuplicates = AttributeDuplicates::ErrorFollowing;
-    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Custom(|cx, used, unused| {
+    const ATTRIBUTE_ORDER: AttributeOrder = AttributeOrder::KeepFirst;
+    const ON_DUPLICATE: OnDuplicate<S> = OnDuplicate::Custom(|cx, used, unused| {
         cx.dcx().span_err(vec![used, unused], "multiple macro transparency attributes");
     });
 
-    fn convert(cx: &AcceptContext<'_>, args: &ArgParser<'_>) -> Option<AttributeKind> {
+    fn convert(cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser<'_>) -> Option<AttributeKind> {
         match args.name_value().and_then(|nv| nv.value_as_str()) {
             Some(sym::transparent) => Some(Transparency::Transparent),
             Some(sym::semiopaque | sym::semitransparent) => Some(Transparency::SemiOpaque),
diff --git a/compiler/rustc_attr_parsing/src/context.rs b/compiler/rustc_attr_parsing/src/context.rs
index 35fb768ad0bd9..47f72232828ee 100644
--- a/compiler/rustc_attr_parsing/src/context.rs
+++ b/compiler/rustc_attr_parsing/src/context.rs
@@ -1,13 +1,17 @@
 use std::cell::RefCell;
 use std::collections::BTreeMap;
-use std::ops::Deref;
+use std::marker::PhantomData;
+use std::ops::{Deref, DerefMut};
 use std::sync::LazyLock;
 
+use private::Sealed;
 use rustc_ast as ast;
+use rustc_ast::NodeId;
 use rustc_attr_data_structures::AttributeKind;
+use rustc_attr_data_structures::lints::{AttributeLint, AttributeLintKind};
 use rustc_errors::{DiagCtxtHandle, Diagnostic};
 use rustc_feature::Features;
-use rustc_hir::{AttrArgs, AttrItem, AttrPath, Attribute, HashIgnoredAttrId};
+use rustc_hir::{AttrArgs, AttrItem, AttrPath, Attribute, HashIgnoredAttrId, HirId};
 use rustc_session::Session;
 use rustc_span::{DUMMY_SP, ErrorGuaranteed, Span, Symbol, sym};
 
@@ -22,20 +26,40 @@ use crate::attributes::transparency::TransparencyParser;
 use crate::attributes::{AttributeParser as _, Combine, Single};
 use crate::parser::{ArgParser, MetaItemParser};
 
+macro_rules! group_type {
+    ($stage: ty) => {
+         LazyLock<(
+            BTreeMap<&'static [Symbol], Box<dyn for<'sess, 'a> Fn(&mut AcceptContext<'_, 'sess, $stage>, &ArgParser<'a>) + Send + Sync>>,
+            Vec<Box<dyn Send + Sync + Fn(&mut FinalizeContext<'_, '_, $stage>) -> Option<AttributeKind>>>
+        )>
+    };
+}
+
 macro_rules! attribute_parsers {
     (
         pub(crate) static $name: ident = [$($names: ty),* $(,)?];
     ) => {
-        type Accepts = BTreeMap<
-            &'static [Symbol],
-            Box<dyn Send + Sync + Fn(&AcceptContext<'_>, &ArgParser<'_>)>
-        >;
-        type Finalizes = Vec<
-            Box<dyn Send + Sync + Fn(&FinalizeContext<'_>) -> Option<AttributeKind>>
-        >;
-        pub(crate) static $name: LazyLock<(Accepts, Finalizes)> = LazyLock::new(|| {
-            let mut accepts = Accepts::new();
-            let mut finalizes = Finalizes::new();
+        mod early {
+            use super::*;
+            type Combine<T> = super::Combine<T, Early>;
+            type Single<T> = super::Single<T, Early>;
+
+            attribute_parsers!(@[Early] pub(crate) static $name = [$($names),*];);
+        }
+        mod late {
+            use super::*;
+            type Combine<T> = super::Combine<T, Late>;
+            type Single<T> = super::Single<T, Late>;
+
+            attribute_parsers!(@[Late] pub(crate) static $name = [$($names),*];);
+        }
+    };
+    (
+        @[$ty: ty] pub(crate) static $name: ident = [$($names: ty),* $(,)?];
+    ) => {
+        pub(crate) static $name: group_type!($ty) = LazyLock::new(|| {
+            let mut accepts = BTreeMap::<_, Box<dyn for<'sess, 'a> Fn(&mut AcceptContext<'_, 'sess, $ty>, &ArgParser<'a>) + Send + Sync>>::new();
+            let mut finalizes = Vec::<Box<dyn Send + Sync + Fn(&mut FinalizeContext<'_, '_, $ty>) -> Option<AttributeKind>>>::new();
             $(
                 {
                     thread_local! {
@@ -62,7 +86,6 @@ macro_rules! attribute_parsers {
         });
     };
 }
-
 attribute_parsers!(
     pub(crate) static ATTRIBUTE_PARSERS = [
         // tidy-alphabetical-start
@@ -86,50 +109,114 @@ attribute_parsers!(
     ];
 );
 
+mod private {
+    pub trait Sealed {}
+    impl Sealed for super::Early {}
+    impl Sealed for super::Late {}
+}
+
+// allow because it's a sealed trait
+#[allow(private_interfaces)]
+pub trait Stage: Sized + 'static + Sealed {
+    type Id: Copy;
+
+    fn parsers() -> &'static group_type!(Self);
+
+    fn emit_err<'sess>(sess: &'sess Session, diag: impl for<'x> Diagnostic<'x>) -> ErrorGuaranteed;
+}
+
+// allow because it's a sealed trait
+#[allow(private_interfaces)]
+impl Stage for Early {
+    type Id = NodeId;
+
+    fn parsers() -> &'static group_type!(Self) {
+        &early::ATTRIBUTE_PARSERS
+    }
+    fn emit_err<'sess>(sess: &'sess Session, diag: impl for<'x> Diagnostic<'x>) -> ErrorGuaranteed {
+        sess.dcx().create_err(diag).delay_as_bug()
+    }
+}
+
+// allow because it's a sealed trait
+#[allow(private_interfaces)]
+impl Stage for Late {
+    type Id = HirId;
+
+    fn parsers() -> &'static group_type!(Self) {
+        &late::ATTRIBUTE_PARSERS
+    }
+    fn emit_err<'sess>(tcx: &'sess Session, diag: impl for<'x> Diagnostic<'x>) -> ErrorGuaranteed {
+        tcx.dcx().emit_err(diag)
+    }
+}
+
+/// used when parsing attributes for miscelaneous things *before* ast lowering
+pub struct Early;
+/// used when parsing attributes during ast lowering
+pub struct Late;
+
 /// Context given to every attribute parser when accepting
 ///
 /// Gives [`AttributeParser`]s enough information to create errors, for example.
-pub(crate) struct AcceptContext<'a> {
-    pub(crate) finalize_cx: &'a FinalizeContext<'a>,
+pub(crate) struct AcceptContext<'f, 'sess, S: Stage> {
+    pub(crate) finalize_cx: FinalizeContext<'f, 'sess, S>,
     /// The span of the attribute currently being parsed
     pub(crate) attr_span: Span,
 }
 
-impl<'a> AcceptContext<'a> {
-    pub(crate) fn emit_err(&self, diag: impl Diagnostic<'a>) -> ErrorGuaranteed {
-        if self.limit_diagnostics {
-            self.dcx().create_err(diag).delay_as_bug()
-        } else {
-            self.dcx().emit_err(diag)
-        }
+impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> {
+    pub(crate) fn emit_err(&self, diag: impl for<'x> Diagnostic<'x>) -> ErrorGuaranteed {
+        S::emit_err(&self.sess, diag)
+    }
+
+    pub(crate) fn emit_lint(&mut self, lint: AttributeLintKind, span: Span) {
+        let id = self.target_id;
+        (self.emit_lint)(AttributeLint { id, span, kind: lint });
     }
 }
 
-impl<'a> Deref for AcceptContext<'a> {
-    type Target = FinalizeContext<'a>;
+impl<'f, 'sess, S: Stage> Deref for AcceptContext<'f, 'sess, S> {
+    type Target = FinalizeContext<'f, 'sess, S>;
 
     fn deref(&self) -> &Self::Target {
         &self.finalize_cx
     }
 }
 
+impl<'f, 'sess, S: Stage> DerefMut for AcceptContext<'f, 'sess, S> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.finalize_cx
+    }
+}
+
 /// Context given to every attribute parser during finalization.
 ///
 /// Gives [`AttributeParser`](crate::attributes::AttributeParser)s enough information to create
 /// errors, for example.
-pub(crate) struct FinalizeContext<'a> {
+pub(crate) struct FinalizeContext<'p, 'sess, S: Stage> {
     /// The parse context, gives access to the session and the
     /// diagnostics context.
-    pub(crate) cx: &'a AttributeParser<'a>,
+    pub(crate) cx: &'p mut AttributeParser<'sess, S>,
     /// The span of the syntactical component this attribute was applied to
     pub(crate) target_span: Span,
+    /// The id ([`NodeId`] if `S` is `Early`, [`HirId`] if `S` is `Late`) of the syntactical component this attribute was applied to
+    pub(crate) target_id: S::Id,
+
+    pub(crate) emit_lint: &'p mut dyn FnMut(AttributeLint<S::Id>),
 }
 
-impl<'a> Deref for FinalizeContext<'a> {
-    type Target = AttributeParser<'a>;
+impl<'p, 'sess: 'p, S: Stage> Deref for FinalizeContext<'p, 'sess, S> {
+    type Target = AttributeParser<'sess, S>;
 
     fn deref(&self) -> &Self::Target {
-        &self.cx
+        self.cx
+    }
+}
+
+impl<'p, 'sess: 'p, S: Stage> DerefMut for FinalizeContext<'p, 'sess, S> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.cx
     }
 }
 
@@ -141,23 +228,20 @@ pub enum OmitDoc {
 
 /// Context created once, for example as part of the ast lowering
 /// context, through which all attributes can be lowered.
-pub struct AttributeParser<'sess> {
+pub struct AttributeParser<'sess, S: Stage = Late> {
     #[expect(dead_code)] // FIXME(jdonszelmann): needed later to verify we parsed all attributes
     tools: Vec<Symbol>,
-    sess: &'sess Session,
     features: Option<&'sess Features>,
+    sess: &'sess Session,
+    stage: PhantomData<S>,
 
     /// *Only* parse attributes with this symbol.
     ///
     /// Used in cases where we want the lowering infrastructure for parse just a single attribute.
     parse_only: Option<Symbol>,
-
-    /// Can be used to instruct parsers to reduce the number of diagnostics it emits.
-    /// Useful when using `parse_limited` and you know the attr will be reparsed later.
-    pub(crate) limit_diagnostics: bool,
 }
 
-impl<'sess> AttributeParser<'sess> {
+impl<'sess> AttributeParser<'sess, Early> {
     /// This method allows you to parse attributes *before* you have access to features or tools.
     /// One example where this is necessary, is to parse `feature` attributes themselves for
     /// example.
@@ -168,33 +252,53 @@ impl<'sess> AttributeParser<'sess> {
     ///
     /// To make sure use is limited, supply a `Symbol` you'd like to parse. Only attributes with
     /// that symbol are picked out of the list of instructions and parsed. Those are returned.
+    ///
+    /// No diagnostics will be emitted when parsing limited. Lints are not emitted at all, while
+    /// errors will be emitted as a delayed bugs. in other words, we *expect* attributes parsed
+    /// with `parse_limited` to be reparsed later during ast lowering where we *do* emit the errors
     pub fn parse_limited(
         sess: &'sess Session,
         attrs: &[ast::Attribute],
         sym: Symbol,
         target_span: Span,
-        limit_diagnostics: bool,
+        target_node_id: NodeId,
     ) -> Option<Attribute> {
-        let mut parsed = Self {
-            sess,
+        let mut p = Self {
             features: None,
             tools: Vec::new(),
             parse_only: Some(sym),
-            limit_diagnostics,
-        }
-        .parse_attribute_list(attrs, target_span, OmitDoc::Skip, std::convert::identity);
-
+            sess,
+            stage: PhantomData,
+        };
+        let mut parsed = p.parse_attribute_list(
+            attrs,
+            target_span,
+            target_node_id,
+            OmitDoc::Skip,
+            std::convert::identity,
+            |_lint| {
+                panic!("can't emit lints here for now (nothing uses this atm)");
+            },
+        );
         assert!(parsed.len() <= 1);
 
         parsed.pop()
     }
 
+    pub fn new_early(sess: &'sess Session, features: &'sess Features, tools: Vec<Symbol>) -> Self {
+        Self { features: Some(features), tools, parse_only: None, sess, stage: PhantomData }
+    }
+}
+
+impl<'sess> AttributeParser<'sess, Late> {
     pub fn new(sess: &'sess Session, features: &'sess Features, tools: Vec<Symbol>) -> Self {
-        Self { sess, features: Some(features), tools, parse_only: None, limit_diagnostics: false }
+        Self { features: Some(features), tools, parse_only: None, sess, stage: PhantomData }
     }
+}
 
+impl<'sess, S: Stage> AttributeParser<'sess, S> {
     pub(crate) fn sess(&self) -> &'sess Session {
-        self.sess
+        &self.sess
     }
 
     pub(crate) fn features(&self) -> &'sess Features {
@@ -202,25 +306,25 @@ impl<'sess> AttributeParser<'sess> {
     }
 
     pub(crate) fn dcx(&self) -> DiagCtxtHandle<'sess> {
-        self.sess.dcx()
+        self.sess().dcx()
     }
 
     /// Parse a list of attributes.
     ///
     /// `target_span` is the span of the thing this list of attributes is applied to,
     /// and when `omit_doc` is set, doc attributes are filtered out.
-    pub fn parse_attribute_list<'a>(
-        &'a self,
-        attrs: &'a [ast::Attribute],
+    pub fn parse_attribute_list(
+        &mut self,
+        attrs: &[ast::Attribute],
         target_span: Span,
+        target_id: S::Id,
         omit_doc: OmitDoc,
 
         lower_span: impl Copy + Fn(Span) -> Span,
+        mut emit_lint: impl FnMut(AttributeLint<S::Id>),
     ) -> Vec<Attribute> {
         let mut attributes = Vec::new();
 
-        let finalize_cx = FinalizeContext { cx: self, target_span };
-
         for attr in attrs {
             // If we're only looking for a single attribute, skip all the ones we don't care about.
             if let Some(expected) = self.parse_only {
@@ -268,13 +372,18 @@ impl<'sess> AttributeParser<'sess> {
                     let args = parser.args();
                     let parts = path.segments().map(|i| i.name).collect::<Vec<_>>();
 
-                    if let Some(accept) = ATTRIBUTE_PARSERS.0.get(parts.as_slice()) {
-                        let cx = AcceptContext {
-                            finalize_cx: &finalize_cx,
+                    if let Some(accept) = S::parsers().0.get(parts.as_slice()) {
+                        let mut cx: AcceptContext<'_, 'sess, S> = AcceptContext {
+                            finalize_cx: FinalizeContext {
+                                cx: self,
+                                target_span,
+                                target_id,
+                                emit_lint: &mut emit_lint,
+                            },
                             attr_span: lower_span(attr.span),
                         };
 
-                        accept(&cx, &args)
+                        accept(&mut cx, args)
                     } else {
                         // If we're here, we must be compiling a tool attribute... Or someone
                         // forgot to parse their fancy new attribute. Let's warn them in any case.
@@ -304,8 +413,13 @@ impl<'sess> AttributeParser<'sess> {
         }
 
         let mut parsed_attributes = Vec::new();
-        for f in &ATTRIBUTE_PARSERS.1 {
-            if let Some(attr) = f(&finalize_cx) {
+        for f in &S::parsers().1 {
+            if let Some(attr) = f(&mut FinalizeContext {
+                cx: self,
+                target_span,
+                target_id,
+                emit_lint: &mut emit_lint,
+            }) {
                 parsed_attributes.push(Attribute::Parsed(attr));
             }
         }
diff --git a/compiler/rustc_attr_parsing/src/lib.rs b/compiler/rustc_attr_parsing/src/lib.rs
index 15037e802ff59..5c458575c767a 100644
--- a/compiler/rustc_attr_parsing/src/lib.rs
+++ b/compiler/rustc_attr_parsing/src/lib.rs
@@ -62,7 +62,7 @@
 //! a "stability" of an item. So, the stability attribute has an
 //! [`AttributeParser`](attributes::AttributeParser) that recognizes both the `#[stable()]`
 //! and `#[unstable()]` syntactic attributes, and at the end produce a single
-//! [`AttributeKind::Stability`](rustc_attr_data_structures::AttributeKind::Stability).
+//! [`AttributeKind::Stability`].
 //!
 //! When multiple instances of the same attribute are allowed, they're combined into a single
 //! semantic attribute. For example:
@@ -86,6 +86,7 @@
 #[macro_use]
 mod attributes;
 mod context;
+mod lints;
 pub mod parser;
 mod session_diagnostics;
 
@@ -93,6 +94,7 @@ pub use attributes::cfg::*;
 pub use attributes::util::{
     find_crate_name, is_builtin_attr, is_doc_alias_attrs_contain_symbol, parse_version,
 };
-pub use context::{AttributeParser, OmitDoc};
+pub use context::{AttributeParser, Early, Late, OmitDoc};
+pub use lints::emit_attribute_lint;
 
 rustc_fluent_macro::fluent_messages! { "../messages.ftl" }
diff --git a/compiler/rustc_attr_parsing/src/lints.rs b/compiler/rustc_attr_parsing/src/lints.rs
new file mode 100644
index 0000000000000..d0d112446b4d9
--- /dev/null
+++ b/compiler/rustc_attr_parsing/src/lints.rs
@@ -0,0 +1,19 @@
+use rustc_attr_data_structures::lints::{AttributeLint, AttributeLintKind};
+use rustc_errors::LintEmitter;
+use rustc_hir::HirId;
+
+use crate::session_diagnostics;
+
+pub fn emit_attribute_lint<L: LintEmitter>(lint: &AttributeLint<HirId>, lint_emitter: L) {
+    let AttributeLint { id, span, kind } = lint;
+
+    match kind {
+        &AttributeLintKind::UnusedDuplicate { this, other, warning } => lint_emitter
+            .emit_node_span_lint(
+                rustc_session::lint::builtin::UNUSED_ATTRIBUTES,
+                *id,
+                *span,
+                session_diagnostics::UnusedDuplicate { this, other, warning },
+            ),
+    }
+}
diff --git a/compiler/rustc_attr_parsing/src/parser.rs b/compiler/rustc_attr_parsing/src/parser.rs
index e10e3b511db60..1edbe3a9d27aa 100644
--- a/compiler/rustc_attr_parsing/src/parser.rs
+++ b/compiler/rustc_attr_parsing/src/parser.rs
@@ -115,7 +115,7 @@ impl<'a> ArgParser<'a> {
         }
     }
 
-    pub fn from_attr_args(value: &'a AttrArgs, dcx: DiagCtxtHandle<'a>) -> Self {
+    pub fn from_attr_args<'sess>(value: &'a AttrArgs, dcx: DiagCtxtHandle<'sess>) -> Self {
         match value {
             AttrArgs::Empty => Self::NoArgs,
             AttrArgs::Delimited(args) if args.delim == Delimiter::Parenthesis => {
@@ -235,7 +235,7 @@ impl<'a> Debug for MetaItemParser<'a> {
 impl<'a> MetaItemParser<'a> {
     /// Create a new parser from a [`NormalAttr`], which is stored inside of any
     /// [`ast::Attribute`](rustc_ast::Attribute)
-    pub fn from_attr(attr: &'a NormalAttr, dcx: DiagCtxtHandle<'a>) -> Self {
+    pub fn from_attr<'sess>(attr: &'a NormalAttr, dcx: DiagCtxtHandle<'sess>) -> Self {
         Self {
             path: PathParser::Ast(&attr.item.path),
             args: ArgParser::from_attr_args(&attr.item.args, dcx),
@@ -320,13 +320,13 @@ fn expr_to_lit(dcx: DiagCtxtHandle<'_>, expr: &Expr, span: Span) -> MetaItemLit
     }
 }
 
-struct MetaItemListParserContext<'a> {
+struct MetaItemListParserContext<'a, 'sess> {
     // the tokens inside the delimiters, so `#[some::attr(a b c)]` would have `a b c` inside
     inside_delimiters: Peekable<TokenStreamIter<'a>>,
-    dcx: DiagCtxtHandle<'a>,
+    dcx: DiagCtxtHandle<'sess>,
 }
 
-impl<'a> MetaItemListParserContext<'a> {
+impl<'a, 'sess> MetaItemListParserContext<'a, 'sess> {
     fn done(&mut self) -> bool {
         self.inside_delimiters.peek().is_none()
     }
@@ -507,11 +507,11 @@ pub struct MetaItemListParser<'a> {
 }
 
 impl<'a> MetaItemListParser<'a> {
-    fn new(delim: &'a DelimArgs, dcx: DiagCtxtHandle<'a>) -> MetaItemListParser<'a> {
+    fn new<'sess>(delim: &'a DelimArgs, dcx: DiagCtxtHandle<'sess>) -> Self {
         MetaItemListParser::new_tts(delim.tokens.iter(), delim.dspan.entire(), dcx)
     }
 
-    fn new_tts(tts: TokenStreamIter<'a>, span: Span, dcx: DiagCtxtHandle<'a>) -> Self {
+    fn new_tts<'sess>(tts: TokenStreamIter<'a>, span: Span, dcx: DiagCtxtHandle<'sess>) -> Self {
         MetaItemListParserContext { inside_delimiters: tts.peekable(), dcx }.parse(span)
     }
 
diff --git a/compiler/rustc_attr_parsing/src/session_diagnostics.rs b/compiler/rustc_attr_parsing/src/session_diagnostics.rs
index 2c434175b4b69..271ea15740fd6 100644
--- a/compiler/rustc_attr_parsing/src/session_diagnostics.rs
+++ b/compiler/rustc_attr_parsing/src/session_diagnostics.rs
@@ -3,7 +3,7 @@ use std::num::IntErrorKind;
 use rustc_ast as ast;
 use rustc_errors::codes::*;
 use rustc_errors::{Applicability, Diag, DiagCtxtHandle, Diagnostic, EmissionGuarantee, Level};
-use rustc_macros::{Diagnostic, Subdiagnostic};
+use rustc_macros::{Diagnostic, LintDiagnostic, Subdiagnostic};
 use rustc_span::{Span, Symbol};
 
 use crate::fluent_generated as fluent;
@@ -451,6 +451,17 @@ pub(crate) struct UnusedMultiple {
     pub name: Symbol,
 }
 
+#[derive(LintDiagnostic)]
+#[diag(attr_parsing_unused_multiple)]
+pub(crate) struct UnusedDuplicate {
+    #[suggestion(code = "", applicability = "machine-applicable")]
+    pub this: Span,
+    #[note]
+    pub other: Span,
+    #[warning]
+    pub warning: bool,
+}
+
 #[derive(Diagnostic)]
 #[diag(attr_parsing_stability_outside_std, code = E0734)]
 pub(crate) struct StabilityOutsideStd {
diff --git a/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs b/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
index 6dd3adf750dbd..d642851bb77bf 100644
--- a/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
+++ b/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
@@ -484,7 +484,7 @@ impl<'a> TraitDef<'a> {
         match item {
             Annotatable::Item(item) => {
                 let is_packed = matches!(
-                    AttributeParser::parse_limited(cx.sess, &item.attrs, sym::repr, item.span, true),
+                    AttributeParser::parse_limited(cx.sess, &item.attrs, sym::repr, item.span, item.id),
                     Some(Attribute::Parsed(AttributeKind::Repr(r))) if r.iter().any(|(x, _)| matches!(x, ReprPacked(..)))
                 );
 
diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs
index 133bd361ee773..9f72fc4705aea 100644
--- a/compiler/rustc_errors/src/lib.rs
+++ b/compiler/rustc_errors/src/lib.rs
@@ -60,8 +60,9 @@ pub use rustc_error_messages::{
     SubdiagMessage, fallback_fluent_bundle, fluent_bundle,
 };
 use rustc_hashes::Hash128;
-use rustc_lint_defs::LintExpectationId;
+use rustc_hir::HirId;
 pub use rustc_lint_defs::{Applicability, listify, pluralize};
+use rustc_lint_defs::{Lint, LintExpectationId};
 use rustc_macros::{Decodable, Encodable};
 pub use rustc_span::ErrorGuaranteed;
 pub use rustc_span::fatal_error::{FatalError, FatalErrorMarker};
@@ -101,6 +102,19 @@ rustc_data_structures::static_assert_size!(PResult<'_, ()>, 24);
 #[cfg(target_pointer_width = "64")]
 rustc_data_structures::static_assert_size!(PResult<'_, bool>, 24);
 
+/// Used to avoid depending on `rustc_middle` in `rustc_attr_parsing`.
+/// Always the `TyCtxt`.
+pub trait LintEmitter: Copy {
+    #[track_caller]
+    fn emit_node_span_lint(
+        self,
+        lint: &'static Lint,
+        hir_id: HirId,
+        span: impl Into<MultiSpan>,
+        decorator: impl for<'a> LintDiagnostic<'a, ()>,
+    );
+}
+
 #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Encodable, Decodable)]
 pub enum SuggestionStyle {
     /// Hide the suggested code when displaying this suggestion inline.
diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs
index 6f288bb39b953..336bf0d93fd03 100644
--- a/compiler/rustc_hir/src/hir.rs
+++ b/compiler/rustc_hir/src/hir.rs
@@ -34,6 +34,7 @@ use crate::def::{CtorKind, DefKind, PerNS, Res};
 use crate::def_id::{DefId, LocalDefIdMap};
 pub(crate) use crate::hir_id::{HirId, ItemLocalId, ItemLocalMap, OwnerId};
 use crate::intravisit::{FnKind, VisitorExt};
+use crate::lints::DelayedLints;
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, HashStable_Generic)]
 pub enum AngleBrackets {
@@ -1526,6 +1527,10 @@ pub struct OwnerInfo<'hir> {
     /// Map indicating what traits are in scope for places where this
     /// is relevant; generated by resolve.
     pub trait_map: ItemLocalMap<Box<[TraitCandidate]>>,
+
+    /// Lints delayed during ast lowering to be emitted
+    /// after hir has completely built
+    pub delayed_lints: DelayedLints,
 }
 
 impl<'tcx> OwnerInfo<'tcx> {
diff --git a/compiler/rustc_hir/src/lib.rs b/compiler/rustc_hir/src/lib.rs
index c6fe475b4609e..dafd31336fbbc 100644
--- a/compiler/rustc_hir/src/lib.rs
+++ b/compiler/rustc_hir/src/lib.rs
@@ -27,6 +27,7 @@ mod hir;
 pub mod hir_id;
 pub mod intravisit;
 pub mod lang_items;
+pub mod lints;
 pub mod pat_util;
 mod stable_hash_impls;
 mod target;
diff --git a/compiler/rustc_hir/src/lints.rs b/compiler/rustc_hir/src/lints.rs
new file mode 100644
index 0000000000000..7be6c32e57edc
--- /dev/null
+++ b/compiler/rustc_hir/src/lints.rs
@@ -0,0 +1,23 @@
+use rustc_attr_data_structures::lints::AttributeLint;
+use rustc_data_structures::fingerprint::Fingerprint;
+use rustc_macros::HashStable_Generic;
+
+use crate::HirId;
+
+/// During ast lowering, no lints can be emitted.
+/// That is because lints attach to nodes either in the AST, or on the built HIR.
+/// When attached to AST nodes, they're emitted just before building HIR,
+/// and then there's a gap where no lints can be emitted until HIR is done.
+/// The variants in this enum represent lints that are temporarily stashed during
+/// AST lowering to be emitted once HIR is built.
+#[derive(Clone, Debug, HashStable_Generic)]
+pub enum DelayedLint {
+    AttributeParsing(AttributeLint<HirId>),
+}
+
+#[derive(Debug)]
+pub struct DelayedLints {
+    pub lints: Box<[DelayedLint]>,
+    // Only present when the crate hash is needed.
+    pub opt_hash: Option<Fingerprint>,
+}
diff --git a/compiler/rustc_hir/src/stable_hash_impls.rs b/compiler/rustc_hir/src/stable_hash_impls.rs
index 91ea88cae4776..6acf1524b608f 100644
--- a/compiler/rustc_hir/src/stable_hash_impls.rs
+++ b/compiler/rustc_hir/src/stable_hash_impls.rs
@@ -6,6 +6,7 @@ use crate::hir::{
     AttributeMap, BodyId, Crate, ForeignItemId, ImplItemId, ItemId, OwnerNodes, TraitItemId,
 };
 use crate::hir_id::{HirId, ItemLocalId};
+use crate::lints::DelayedLints;
 
 /// Requirements for a `StableHashingContext` to be used in this crate.
 /// This is a hack to allow using the `HashStable_Generic` derive macro
@@ -102,6 +103,13 @@ impl<'tcx, HirCtx: crate::HashStableContext> HashStable<HirCtx> for OwnerNodes<'
     }
 }
 
+impl<HirCtx: crate::HashStableContext> HashStable<HirCtx> for DelayedLints {
+    fn hash_stable(&self, hcx: &mut HirCtx, hasher: &mut StableHasher) {
+        let DelayedLints { opt_hash, .. } = *self;
+        opt_hash.unwrap().hash_stable(hcx, hasher);
+    }
+}
+
 impl<'tcx, HirCtx: crate::HashStableContext> HashStable<HirCtx> for AttributeMap<'tcx> {
     fn hash_stable(&self, hcx: &mut HirCtx, hasher: &mut StableHasher) {
         // We ignore the `map` since it refers to information included in `opt_hash` which is
diff --git a/compiler/rustc_hir_analysis/Cargo.toml b/compiler/rustc_hir_analysis/Cargo.toml
index 899370b34e498..5d6c49ee862fc 100644
--- a/compiler/rustc_hir_analysis/Cargo.toml
+++ b/compiler/rustc_hir_analysis/Cargo.toml
@@ -14,6 +14,7 @@ rustc_abi = { path = "../rustc_abi" }
 rustc_arena = { path = "../rustc_arena" }
 rustc_ast = { path = "../rustc_ast" }
 rustc_attr_data_structures = { path = "../rustc_attr_data_structures" }
+rustc_attr_parsing = { path = "../rustc_attr_parsing" }
 rustc_data_structures = { path = "../rustc_data_structures" }
 rustc_errors = { path = "../rustc_errors" }
 rustc_feature = { path = "../rustc_feature" }
diff --git a/compiler/rustc_hir_analysis/src/lib.rs b/compiler/rustc_hir_analysis/src/lib.rs
index a92ee89186cf0..007f3a6abf6c4 100644
--- a/compiler/rustc_hir_analysis/src/lib.rs
+++ b/compiler/rustc_hir_analysis/src/lib.rs
@@ -92,8 +92,9 @@ mod variance;
 
 pub use errors::NoVariantNamed;
 use rustc_abi::ExternAbi;
-use rustc_hir as hir;
 use rustc_hir::def::DefKind;
+use rustc_hir::lints::DelayedLint;
+use rustc_hir::{self as hir};
 use rustc_middle::middle;
 use rustc_middle::mir::interpret::GlobalId;
 use rustc_middle::query::Providers;
@@ -174,6 +175,14 @@ pub fn provide(providers: &mut Providers) {
     };
 }
 
+fn emit_delayed_lint(lint: &DelayedLint, tcx: TyCtxt<'_>) {
+    match lint {
+        DelayedLint::AttributeParsing(attribute_lint) => {
+            rustc_attr_parsing::emit_attribute_lint(attribute_lint, tcx)
+        }
+    }
+}
+
 pub fn check_crate(tcx: TyCtxt<'_>) {
     let _prof_timer = tcx.sess.timer("type_check_crate");
 
@@ -192,6 +201,14 @@ pub fn check_crate(tcx: TyCtxt<'_>) {
         let _: R = tcx.ensure_ok().crate_inherent_impls_overlap_check(());
     });
 
+    for owner_id in tcx.hir_crate_items(()).owners() {
+        if let Some(delayed_lints) = tcx.opt_ast_lowering_delayed_lints(owner_id) {
+            for lint in &delayed_lints.lints {
+                emit_delayed_lint(lint, tcx);
+            }
+        }
+    }
+
     tcx.par_hir_body_owners(|item_def_id| {
         let def_kind = tcx.def_kind(item_def_id);
         // Make sure we evaluate all static and (non-associated) const items, even if unused.
diff --git a/compiler/rustc_lint/src/nonstandard_style.rs b/compiler/rustc_lint/src/nonstandard_style.rs
index 31c180744668a..1b60466a589d2 100644
--- a/compiler/rustc_lint/src/nonstandard_style.rs
+++ b/compiler/rustc_lint/src/nonstandard_style.rs
@@ -164,7 +164,7 @@ impl NonCamelCaseTypes {
 impl EarlyLintPass for NonCamelCaseTypes {
     fn check_item(&mut self, cx: &EarlyContext<'_>, it: &ast::Item) {
         let has_repr_c = matches!(
-            AttributeParser::parse_limited(cx.sess(), &it.attrs, sym::repr, it.span, true),
+            AttributeParser::parse_limited(cx.sess(), &it.attrs, sym::repr, it.span, it.id),
             Some(Attribute::Parsed(AttributeKind::Repr(r))) if r.iter().any(|(r, _)| r == &ReprAttr::ReprC)
         );
 
diff --git a/compiler/rustc_middle/src/hir/mod.rs b/compiler/rustc_middle/src/hir/mod.rs
index d1f5caaafb2b3..cb4760c18de5f 100644
--- a/compiler/rustc_middle/src/hir/mod.rs
+++ b/compiler/rustc_middle/src/hir/mod.rs
@@ -12,6 +12,7 @@ use rustc_data_structures::stable_hasher::{HashStable, StableHasher};
 use rustc_data_structures::sync::{DynSend, DynSync, try_par_for_each_in};
 use rustc_hir::def::DefKind;
 use rustc_hir::def_id::{DefId, LocalDefId, LocalModDefId};
+use rustc_hir::lints::DelayedLint;
 use rustc_hir::*;
 use rustc_macros::{Decodable, Encodable, HashStable};
 use rustc_span::{ErrorGuaranteed, ExpnId, Span};
@@ -161,8 +162,9 @@ impl<'tcx> TyCtxt<'tcx> {
         node: OwnerNode<'_>,
         bodies: &SortedMap<ItemLocalId, &Body<'_>>,
         attrs: &SortedMap<ItemLocalId, &[Attribute]>,
+        delayed_lints: &[DelayedLint],
         define_opaque: Option<&[(Span, LocalDefId)]>,
-    ) -> (Option<Fingerprint>, Option<Fingerprint>) {
+    ) -> (Option<Fingerprint>, Option<Fingerprint>, Option<Fingerprint>) {
         if self.needs_crate_hash() {
             self.with_stable_hashing_context(|mut hcx| {
                 let mut stable_hasher = StableHasher::new();
@@ -178,10 +180,16 @@ impl<'tcx> TyCtxt<'tcx> {
                 define_opaque.hash_stable(&mut hcx, &mut stable_hasher);
 
                 let h2 = stable_hasher.finish();
-                (Some(h1), Some(h2))
+
+                // hash lints emitted during ast lowering
+                let mut stable_hasher = StableHasher::new();
+                delayed_lints.hash_stable(&mut hcx, &mut stable_hasher);
+                let h3 = stable_hasher.finish();
+
+                (Some(h1), Some(h2), Some(h3))
             })
         } else {
-            (None, None)
+            (None, None, None)
         }
     }
 }
@@ -214,6 +222,8 @@ pub fn provide(providers: &mut Providers) {
     providers.hir_attr_map = |tcx, id| {
         tcx.hir_crate(()).owners[id.def_id].as_owner().map_or(AttributeMap::EMPTY, |o| &o.attrs)
     };
+    providers.opt_ast_lowering_delayed_lints =
+        |tcx, id| tcx.hir_crate(()).owners[id.def_id].as_owner().map(|o| &o.delayed_lints);
     providers.def_span = |tcx, def_id| tcx.hir_span(tcx.local_def_id_to_hir_id(def_id));
     providers.def_ident_span = |tcx, def_id| {
         let hir_id = tcx.local_def_id_to_hir_id(def_id);
diff --git a/compiler/rustc_middle/src/query/mod.rs b/compiler/rustc_middle/src/query/mod.rs
index d8c927e00db4a..15e8c1ef3cc04 100644
--- a/compiler/rustc_middle/src/query/mod.rs
+++ b/compiler/rustc_middle/src/query/mod.rs
@@ -221,6 +221,14 @@ rustc_queries! {
         feedable
     }
 
+    /// Gives access to lints emitted during ast lowering.
+    ///
+    /// This can be conveniently accessed by `tcx.hir_*` methods.
+    /// Avoid calling this query directly.
+    query opt_ast_lowering_delayed_lints(key: hir::OwnerId) -> Option<&'tcx hir::lints::DelayedLints> {
+        desc { |tcx| "getting AST lowering delayed lints in `{}`", tcx.def_path_str(key) }
+    }
+
     /// Returns the *default* of the const pararameter given by `DefId`.
     ///
     /// E.g., given `struct Ty<const N: usize = 3>;` this returns `3` for `N`.
diff --git a/compiler/rustc_middle/src/ty/context.rs b/compiler/rustc_middle/src/ty/context.rs
index cb132ae5573d7..6a47000fc8529 100644
--- a/compiler/rustc_middle/src/ty/context.rs
+++ b/compiler/rustc_middle/src/ty/context.rs
@@ -30,7 +30,7 @@ use rustc_data_structures::sync::{
     self, DynSend, DynSync, FreezeReadGuard, Lock, RwLock, WorkerLocal,
 };
 use rustc_errors::{
-    Applicability, Diag, DiagCtxtHandle, ErrorGuaranteed, LintDiagnostic, MultiSpan,
+    Applicability, Diag, DiagCtxtHandle, ErrorGuaranteed, LintDiagnostic, LintEmitter, MultiSpan,
 };
 use rustc_hir::def::{CtorKind, DefKind};
 use rustc_hir::def_id::{CrateNum, DefId, LOCAL_CRATE, LocalDefId};
@@ -1350,8 +1350,8 @@ impl<'tcx> TyCtxtFeed<'tcx, LocalDefId> {
         let bodies = Default::default();
         let attrs = hir::AttributeMap::EMPTY;
 
-        let (opt_hash_including_bodies, _) =
-            self.tcx.hash_owner_nodes(node, &bodies, &attrs.map, attrs.define_opaque);
+        let (opt_hash_including_bodies, _, _) =
+            self.tcx.hash_owner_nodes(node, &bodies, &attrs.map, &[], attrs.define_opaque);
         let node = node.into();
         self.opt_hir_owner_nodes(Some(self.tcx.arena.alloc(hir::OwnerNodes {
             opt_hash_including_bodies,
@@ -1389,6 +1389,18 @@ pub struct TyCtxt<'tcx> {
     gcx: &'tcx GlobalCtxt<'tcx>,
 }
 
+impl<'tcx> LintEmitter for TyCtxt<'tcx> {
+    fn emit_node_span_lint(
+        self,
+        lint: &'static Lint,
+        hir_id: HirId,
+        span: impl Into<MultiSpan>,
+        decorator: impl for<'a> LintDiagnostic<'a, ()>,
+    ) {
+        self.emit_node_span_lint(lint, hir_id, span, decorator);
+    }
+}
+
 // Explicitly implement `DynSync` and `DynSend` for `TyCtxt` to short circuit trait resolution. Its
 // field are asserted to implement these traits below, so this is trivially safe, and it greatly
 // speeds-up compilation of this crate and its dependents.
diff --git a/compiler/rustc_resolve/src/def_collector.rs b/compiler/rustc_resolve/src/def_collector.rs
index dc16fe212b110..f8e0a6936a007 100644
--- a/compiler/rustc_resolve/src/def_collector.rs
+++ b/compiler/rustc_resolve/src/def_collector.rs
@@ -128,7 +128,7 @@ impl<'a, 'ra, 'tcx> visit::Visitor<'a> for DefCollector<'a, 'ra, 'tcx> {
                 // FIXME(jdonszelmann) make one of these in the resolver?
                 // FIXME(jdonszelmann) don't care about tools here maybe? Just parse what we can.
                 // Does that prevents errors from happening? maybe
-                let parser = AttributeParser::new(
+                let mut parser = AttributeParser::new_early(
                     &self.resolver.tcx.sess,
                     self.resolver.tcx.features(),
                     Vec::new(),
@@ -136,8 +136,14 @@ impl<'a, 'ra, 'tcx> visit::Visitor<'a> for DefCollector<'a, 'ra, 'tcx> {
                 let attrs = parser.parse_attribute_list(
                     &i.attrs,
                     i.span,
+                    i.id,
                     OmitDoc::Skip,
                     std::convert::identity,
+                    |_l| {
+                        // FIXME(jdonszelmann): emit lints here properly
+                        // NOTE that before new attribute parsing, they didn't happen either
+                        // but it would be nice if we could change that.
+                    },
                 );
 
                 let macro_data =

From e6a5f281ec2b78b3089dcd9fd941d9d14921c6fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= <jana@donsz.nl>
Date: Mon, 3 Mar 2025 17:45:14 +0100
Subject: [PATCH 10/16] add error message for unused duplicate

---
 compiler/rustc_attr_parsing/messages.ftl               | 4 +---
 compiler/rustc_attr_parsing/src/session_diagnostics.rs | 2 +-
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/compiler/rustc_attr_parsing/messages.ftl b/compiler/rustc_attr_parsing/messages.ftl
index 573854afa746c..c9443feb021bb 100644
--- a/compiler/rustc_attr_parsing/messages.ftl
+++ b/compiler/rustc_attr_parsing/messages.ftl
@@ -135,9 +135,7 @@ attr_parsing_unused_duplicate =
     unused attribute
     .suggestion = remove this attribute
     .note = attribute also specified here
-    .warn = {-attr_parsing_previously_accepted}
-
-
+    .warn = {-passes_previously_accepted}
 attr_parsing_unused_multiple =
     multiple `{$name}` attributes
     .suggestion = remove this attribute
diff --git a/compiler/rustc_attr_parsing/src/session_diagnostics.rs b/compiler/rustc_attr_parsing/src/session_diagnostics.rs
index 271ea15740fd6..7f847d3dd4c5a 100644
--- a/compiler/rustc_attr_parsing/src/session_diagnostics.rs
+++ b/compiler/rustc_attr_parsing/src/session_diagnostics.rs
@@ -452,7 +452,7 @@ pub(crate) struct UnusedMultiple {
 }
 
 #[derive(LintDiagnostic)]
-#[diag(attr_parsing_unused_multiple)]
+#[diag(attr_parsing_unused_duplicate)]
 pub(crate) struct UnusedDuplicate {
     #[suggestion(code = "", applicability = "machine-applicable")]
     pub this: Span,

From e2afe04e2e45aff24bcc4e78cae04acdba8edcef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= <jana@donsz.nl>
Date: Fri, 7 Mar 2025 15:34:14 +0100
Subject: [PATCH 11/16] remove 'static in some places

---
 .../rustc_attr_parsing/src/attributes/allow_unstable.rs   | 4 ++--
 compiler/rustc_attr_parsing/src/attributes/deprecation.rs | 2 +-
 compiler/rustc_attr_parsing/src/attributes/mod.rs         | 8 ++++----
 compiler/rustc_attr_parsing/src/attributes/repr.rs        | 2 +-
 compiler/rustc_attr_parsing/src/attributes/stability.rs   | 2 +-
 .../rustc_attr_parsing/src/attributes/transparency.rs     | 4 ++--
 compiler/rustc_attr_parsing/src/lib.rs                    | 4 ++--
 7 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/compiler/rustc_attr_parsing/src/attributes/allow_unstable.rs b/compiler/rustc_attr_parsing/src/attributes/allow_unstable.rs
index 2349ee15fe61e..81192f902a232 100644
--- a/compiler/rustc_attr_parsing/src/attributes/allow_unstable.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/allow_unstable.rs
@@ -10,7 +10,7 @@ use crate::session_diagnostics;
 
 pub(crate) struct AllowInternalUnstableParser;
 impl<S: Stage> CombineAttributeParser<S> for AllowInternalUnstableParser {
-    const PATH: &'static [Symbol] = &[sym::allow_internal_unstable];
+    const PATH: &[Symbol] = &[sym::allow_internal_unstable];
     type Item = (Symbol, Span);
     const CONVERT: ConvertFn<Self::Item> = AttributeKind::AllowInternalUnstable;
 
@@ -26,7 +26,7 @@ impl<S: Stage> CombineAttributeParser<S> for AllowInternalUnstableParser {
 
 pub(crate) struct AllowConstFnUnstableParser;
 impl<S: Stage> CombineAttributeParser<S> for AllowConstFnUnstableParser {
-    const PATH: &'static [Symbol] = &[sym::rustc_allow_const_fn_unstable];
+    const PATH: &[Symbol] = &[sym::rustc_allow_const_fn_unstable];
     type Item = Symbol;
     const CONVERT: ConvertFn<Self::Item> = AttributeKind::AllowConstFnUnstable;
 
diff --git a/compiler/rustc_attr_parsing/src/attributes/deprecation.rs b/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
index ba1bdb9fd02f0..1faee41c2a9d6 100644
--- a/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/deprecation.rs
@@ -42,7 +42,7 @@ fn get<S: Stage>(
 }
 
 impl<S: Stage> SingleAttributeParser<S> for DeprecationParser {
-    const PATH: &'static [Symbol] = &[sym::deprecated];
+    const PATH: &[Symbol] = &[sym::deprecated];
     const ATTRIBUTE_ORDER: AttributeOrder = AttributeOrder::KeepFirst;
     const ON_DUPLICATE: OnDuplicate<S> = OnDuplicate::Error;
 
diff --git a/compiler/rustc_attr_parsing/src/attributes/mod.rs b/compiler/rustc_attr_parsing/src/attributes/mod.rs
index 1d239bd43f40d..caf55e6685efc 100644
--- a/compiler/rustc_attr_parsing/src/attributes/mod.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/mod.rs
@@ -12,7 +12,7 @@
 //! - [`CombineAttributeParser`]: makes it easy to implement an attribute which should combine the
 //! contents of attributes, if an attribute appear multiple times in a list
 //!
-//! Attributes should be added to [`ATTRIBUTE_PARSERS`](crate::context::ATTRIBUTE_PARSERS) to be parsed.
+//! Attributes should be added to `crate::context::ATTRIBUTE_PARSERS` to be parsed.
 
 use std::marker::PhantomData;
 
@@ -83,7 +83,7 @@ pub(crate) trait AttributeParser<S: Stage>: Default + 'static {
 /// [`SingleAttributeParser`] can only convert attributes one-to-one, and cannot combine multiple
 /// attributes together like is necessary for `#[stable()]` and `#[unstable()]` for example.
 pub(crate) trait SingleAttributeParser<S: Stage>: 'static {
-    const PATH: &'static [Symbol];
+    const PATH: &[Symbol];
     const ATTRIBUTE_ORDER: AttributeOrder;
     const ON_DUPLICATE: OnDuplicate<S>;
 
@@ -151,7 +151,7 @@ pub(crate) enum OnDuplicate<S: Stage> {
     /// Custom function called when a duplicate attribute is found.
     ///
     /// - `unused` is the span of the attribute that was unused or bad because of some
-    ///   duplicate reason (see [`AttributeDuplicates`])
+    ///   duplicate reason (see [`AttributeOrder`])
     /// - `used` is the span of the attribute that was used in favor of the unused attribute
     Custom(fn(cx: &AcceptContext<'_, '_, S>, used: Span, unused: Span)),
 }
@@ -217,7 +217,7 @@ type ConvertFn<E> = fn(ThinVec<E>) -> AttributeKind;
 /// [`CombineAttributeParser`] can only convert a single kind of attribute, and cannot combine multiple
 /// attributes together like is necessary for `#[stable()]` and `#[unstable()]` for example.
 pub(crate) trait CombineAttributeParser<S: Stage>: 'static {
-    const PATH: &'static [Symbol];
+    const PATH: &[rustc_span::Symbol];
 
     type Item;
     const CONVERT: ConvertFn<Self::Item>;
diff --git a/compiler/rustc_attr_parsing/src/attributes/repr.rs b/compiler/rustc_attr_parsing/src/attributes/repr.rs
index 8b2981ddb08c7..753b2366b410e 100644
--- a/compiler/rustc_attr_parsing/src/attributes/repr.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/repr.rs
@@ -21,7 +21,7 @@ pub(crate) struct ReprParser;
 
 impl<S: Stage> CombineAttributeParser<S> for ReprParser {
     type Item = (ReprAttr, Span);
-    const PATH: &'static [Symbol] = &[sym::repr];
+    const PATH: &[Symbol] = &[sym::repr];
     const CONVERT: ConvertFn<Self::Item> = AttributeKind::Repr;
 
     fn extend<'c>(
diff --git a/compiler/rustc_attr_parsing/src/attributes/stability.rs b/compiler/rustc_attr_parsing/src/attributes/stability.rs
index d8cf56a9830c0..3cbadd4c7ec22 100644
--- a/compiler/rustc_attr_parsing/src/attributes/stability.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/stability.rs
@@ -117,7 +117,7 @@ impl<S: Stage> AttributeParser<S> for BodyStabilityParser {
 pub(crate) struct ConstStabilityIndirectParser;
 // FIXME(jdonszelmann): single word attribute group when we have these
 impl<S: Stage> SingleAttributeParser<S> for ConstStabilityIndirectParser {
-    const PATH: &'static [Symbol] = &[sym::rustc_const_stable_indirect];
+    const PATH: &[Symbol] = &[sym::rustc_const_stable_indirect];
     const ATTRIBUTE_ORDER: AttributeOrder = AttributeOrder::KeepFirst;
     const ON_DUPLICATE: OnDuplicate<S> = OnDuplicate::Ignore;
 
diff --git a/compiler/rustc_attr_parsing/src/attributes/transparency.rs b/compiler/rustc_attr_parsing/src/attributes/transparency.rs
index 1de3a9cfeba90..16ad9d03e500d 100644
--- a/compiler/rustc_attr_parsing/src/attributes/transparency.rs
+++ b/compiler/rustc_attr_parsing/src/attributes/transparency.rs
@@ -2,8 +2,8 @@ use rustc_attr_data_structures::AttributeKind;
 use rustc_span::hygiene::Transparency;
 use rustc_span::{Symbol, sym};
 
-use super::{AcceptContext, AttributeOrder, OnDuplicate, SingleAttributeParser};
-use crate::context::Stage;
+use super::{AttributeOrder, OnDuplicate, SingleAttributeParser};
+use crate::context::{AcceptContext, Stage};
 use crate::parser::ArgParser;
 
 pub(crate) struct TransparencyParser;
diff --git a/compiler/rustc_attr_parsing/src/lib.rs b/compiler/rustc_attr_parsing/src/lib.rs
index 5c458575c767a..47eeb63bad3db 100644
--- a/compiler/rustc_attr_parsing/src/lib.rs
+++ b/compiler/rustc_attr_parsing/src/lib.rs
@@ -62,7 +62,7 @@
 //! a "stability" of an item. So, the stability attribute has an
 //! [`AttributeParser`](attributes::AttributeParser) that recognizes both the `#[stable()]`
 //! and `#[unstable()]` syntactic attributes, and at the end produce a single
-//! [`AttributeKind::Stability`].
+//! [`AttributeKind::Stability`](rustc_attr_data_structures::AttributeKind::Stability).
 //!
 //! When multiple instances of the same attribute are allowed, they're combined into a single
 //! semantic attribute. For example:
@@ -85,7 +85,7 @@
 
 #[macro_use]
 mod attributes;
-mod context;
+pub(crate) mod context;
 mod lints;
 pub mod parser;
 mod session_diagnostics;

From 975741c29417034e9026e53fba16f3b7d5c5721b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= <jana@donsz.nl>
Date: Thu, 12 Jun 2025 11:59:42 +0200
Subject: [PATCH 12/16] add test for dead code caused by enum variants
 shadowing an associated function

---
 .../ui/enum/dead-code-associated-function.rs  | 20 +++++++++++++++++++
 .../enum/dead-code-associated-function.stderr | 20 +++++++++++++++++++
 2 files changed, 40 insertions(+)
 create mode 100644 tests/ui/enum/dead-code-associated-function.rs
 create mode 100644 tests/ui/enum/dead-code-associated-function.stderr

diff --git a/tests/ui/enum/dead-code-associated-function.rs b/tests/ui/enum/dead-code-associated-function.rs
new file mode 100644
index 0000000000000..d172ceb41dde4
--- /dev/null
+++ b/tests/ui/enum/dead-code-associated-function.rs
@@ -0,0 +1,20 @@
+//@ check-pass
+#![warn(dead_code)]
+
+enum E {
+    F(),
+    C(),
+}
+
+impl E {
+    #[expect(non_snake_case)]
+    fn F() {}
+    //~^ WARN: associated items `F` and `C` are never used
+
+    const C: () = ();
+}
+
+fn main() {
+    let _: E = E::F();
+    let _: E = E::C();
+}
diff --git a/tests/ui/enum/dead-code-associated-function.stderr b/tests/ui/enum/dead-code-associated-function.stderr
new file mode 100644
index 0000000000000..df968783c27af
--- /dev/null
+++ b/tests/ui/enum/dead-code-associated-function.stderr
@@ -0,0 +1,20 @@
+warning: associated items `F` and `C` are never used
+  --> $DIR/dead-code-associated-function.rs:11:8
+   |
+LL | impl E {
+   | ------ associated items in this implementation
+LL |     #[expect(non_snake_case)]
+LL |     fn F() {}
+   |        ^
+...
+LL |     const C: () = ();
+   |           ^
+   |
+note: the lint level is defined here
+  --> $DIR/dead-code-associated-function.rs:2:9
+   |
+LL | #![warn(dead_code)]
+   |         ^^^^^^^^^
+
+warning: 1 warning emitted
+

From 2e7e52e07cec3adb2b14ce17faffe039216ed9d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jana=20D=C3=B6nszelmann?= <jana@donsz.nl>
Date: Thu, 12 Jun 2025 10:21:30 +0200
Subject: [PATCH 13/16] detect when variants have the same name as an
 associated function

---
 compiler/rustc_passes/messages.ftl            |  3 ++
 compiler/rustc_passes/src/dead.rs             | 30 ++++++++++++++++++-
 compiler/rustc_passes/src/errors.rs           | 12 ++++++++
 .../enum/dead-code-associated-function.stderr | 10 +++++++
 4 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/compiler/rustc_passes/messages.ftl b/compiler/rustc_passes/messages.ftl
index a4ef065ea2c83..81c7b42b105b5 100644
--- a/compiler/rustc_passes/messages.ftl
+++ b/compiler/rustc_passes/messages.ftl
@@ -290,6 +290,9 @@ passes_duplicate_lang_item_crate_depends =
     .first_definition_path = first definition in `{$orig_crate_name}` loaded from {$orig_path}
     .second_definition_path = second definition in `{$crate_name}` loaded from {$path}
 
+passes_enum_variant_same_name =
+    it is impossible to refer to the {$descr} `{$dead_name}` because it is shadowed by this enum variant with the same name
+
 passes_export_name =
     attribute should be applied to a free function, impl method or static
     .label = not a free function, impl method or static
diff --git a/compiler/rustc_passes/src/dead.rs b/compiler/rustc_passes/src/dead.rs
index e597c819a3aae..4257d8e8d16bd 100644
--- a/compiler/rustc_passes/src/dead.rs
+++ b/compiler/rustc_passes/src/dead.rs
@@ -14,7 +14,7 @@ use rustc_errors::MultiSpan;
 use rustc_hir::def::{CtorOf, DefKind, Res};
 use rustc_hir::def_id::{DefId, LocalDefId, LocalModDefId};
 use rustc_hir::intravisit::{self, Visitor};
-use rustc_hir::{self as hir, Node, PatKind, QPath, TyKind};
+use rustc_hir::{self as hir, ImplItem, ImplItemKind, Node, PatKind, QPath, TyKind};
 use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags;
 use rustc_middle::middle::privacy::Level;
 use rustc_middle::query::Providers;
@@ -936,7 +936,9 @@ enum ShouldWarnAboutField {
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 enum ReportOn {
+    /// Report on something that hasn't got a proper name to refer to
     TupleField,
+    /// Report on something that has got a name, which could be a field but also a method
     NamedField,
 }
 
@@ -1061,6 +1063,31 @@ impl<'tcx> DeadVisitor<'tcx> {
                 None
             };
 
+        let enum_variants_with_same_name = dead_codes
+            .iter()
+            .filter_map(|dead_item| {
+                if let Node::ImplItem(ImplItem {
+                    kind: ImplItemKind::Fn(..) | ImplItemKind::Const(..),
+                    ..
+                }) = tcx.hir_node_by_def_id(dead_item.def_id)
+                    && let Some(impl_did) = tcx.opt_parent(dead_item.def_id.to_def_id())
+                    && let DefKind::Impl { of_trait: false } = tcx.def_kind(impl_did)
+                    && let ty::Adt(maybe_enum, _) = tcx.type_of(impl_did).skip_binder().kind()
+                    && maybe_enum.is_enum()
+                    && let Some(variant) =
+                        maybe_enum.variants().iter().find(|i| i.name == dead_item.name)
+                {
+                    Some(crate::errors::EnumVariantSameName {
+                        descr: tcx.def_descr(dead_item.def_id.to_def_id()),
+                        dead_name: dead_item.name,
+                        variant_span: tcx.def_span(variant.def_id),
+                    })
+                } else {
+                    None
+                }
+            })
+            .collect();
+
         let diag = match report_on {
             ReportOn::TupleField => {
                 let tuple_fields = if let Some(parent_id) = parent_item
@@ -1114,6 +1141,7 @@ impl<'tcx> DeadVisitor<'tcx> {
                 name_list,
                 parent_info,
                 ignored_derived_impls,
+                enum_variants_with_same_name,
             },
         };
 
diff --git a/compiler/rustc_passes/src/errors.rs b/compiler/rustc_passes/src/errors.rs
index 74ce92624bd49..74c89f0c698b5 100644
--- a/compiler/rustc_passes/src/errors.rs
+++ b/compiler/rustc_passes/src/errors.rs
@@ -1478,6 +1478,9 @@ pub(crate) enum MultipleDeadCodes<'tcx> {
         participle: &'tcx str,
         name_list: DiagSymbolList,
         #[subdiagnostic]
+        // only on DeadCodes since it's never a problem for tuple struct fields
+        enum_variants_with_same_name: Vec<EnumVariantSameName<'tcx>>,
+        #[subdiagnostic]
         parent_info: Option<ParentInfo<'tcx>>,
         #[subdiagnostic]
         ignored_derived_impls: Option<IgnoredDerivedImpls>,
@@ -1498,6 +1501,15 @@ pub(crate) enum MultipleDeadCodes<'tcx> {
     },
 }
 
+#[derive(Subdiagnostic)]
+#[note(passes_enum_variant_same_name)]
+pub(crate) struct EnumVariantSameName<'tcx> {
+    #[primary_span]
+    pub variant_span: Span,
+    pub dead_name: Symbol,
+    pub descr: &'tcx str,
+}
+
 #[derive(Subdiagnostic)]
 #[label(passes_parent_info)]
 pub(crate) struct ParentInfo<'tcx> {
diff --git a/tests/ui/enum/dead-code-associated-function.stderr b/tests/ui/enum/dead-code-associated-function.stderr
index df968783c27af..e3c1a4c81a491 100644
--- a/tests/ui/enum/dead-code-associated-function.stderr
+++ b/tests/ui/enum/dead-code-associated-function.stderr
@@ -10,6 +10,16 @@ LL |     fn F() {}
 LL |     const C: () = ();
    |           ^
    |
+note: it is impossible to refer to the associated function `F` because it is shadowed by this enum variant with the same name
+  --> $DIR/dead-code-associated-function.rs:5:5
+   |
+LL |     F(),
+   |     ^
+note: it is impossible to refer to the associated constant `C` because it is shadowed by this enum variant with the same name
+  --> $DIR/dead-code-associated-function.rs:6:5
+   |
+LL |     C(),
+   |     ^
 note: the lint level is defined here
   --> $DIR/dead-code-associated-function.rs:2:9
    |

From 7a0d0de0e171e58e479bb3ed8923e37e78b728f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Ber=C3=A1nek?= <berykubik@gmail.com>
Date: Thu, 12 Jun 2025 12:34:22 +0200
Subject: [PATCH 14/16] Remove bootstrap adhoc group

It corresponds 1:1 to the bootstrap team, and with the review preferences we shouldn't need it.
---
 triagebot.toml | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/triagebot.toml b/triagebot.toml
index 52d18d2ca4b25..739685e718a9e 100644
--- a/triagebot.toml
+++ b/triagebot.toml
@@ -1240,13 +1240,6 @@ libs = [
     "@thomcc",
     "@ibraheemdev",
 ]
-bootstrap = [
-    "@Mark-Simulacrum",
-    "@albertlarsan68",
-    "@kobzol",
-    "@jieyouxu",
-    "@clubby789",
-]
 infra-ci = [
     "@Mark-Simulacrum",
     "@Kobzol",

From c871bf196f707c68a3c9765df9a429bfe05ddb08 Mon Sep 17 00:00:00 2001
From: Waffle Lapkin <waffle.lapkin@gmail.com>
Date: Thu, 12 Jun 2025 13:06:01 +0200
Subject: [PATCH 15/16] Add myself (WaffleLapkin) to review rotation

---
 triagebot.toml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/triagebot.toml b/triagebot.toml
index 52d18d2ca4b25..4cf42b34ff958 100644
--- a/triagebot.toml
+++ b/triagebot.toml
@@ -1229,6 +1229,7 @@ compiler = [
     "@oli-obk",
     "@petrochenkov",
     "@SparrowLii",
+    "@WaffleLapkin",
     "@wesleywiser",
 ]
 libs = [
@@ -1266,6 +1267,7 @@ codegen = [
     "@dianqk",
     "@saethlin",
     "@workingjubilee",
+    "@WaffleLapkin",
 ]
 query-system = [
     "@oli-obk",

From 488ebeecbea225606dccb610dea216a4bbdcb69c Mon Sep 17 00:00:00 2001
From: Oli Scherer <github333195615777966@oli-obk.de>
Date: Thu, 12 Jun 2025 15:03:34 +0000
Subject: [PATCH 16/16] Remove lower_arg_ty as all callers were passing `None`

---
 compiler/rustc_hir_analysis/src/collect.rs             |  2 +-
 compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs | 10 ----------
 compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs           |  2 +-
 3 files changed, 2 insertions(+), 12 deletions(-)

diff --git a/compiler/rustc_hir_analysis/src/collect.rs b/compiler/rustc_hir_analysis/src/collect.rs
index e1df0d60452d6..6e22ac5a28a85 100644
--- a/compiler/rustc_hir_analysis/src/collect.rs
+++ b/compiler/rustc_hir_analysis/src/collect.rs
@@ -494,7 +494,7 @@ impl<'tcx> HirTyLowerer<'tcx> for ItemCtxt<'tcx> {
 
                 // Only visit the type looking for `_` if we didn't fix the type above
                 visitor.visit_ty_unambig(a);
-                self.lowerer().lower_arg_ty(a, None)
+                self.lowerer().lower_ty(a)
             })
             .collect();
 
diff --git a/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs b/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs
index 4deb47dfff8ce..bf407cbaccb5a 100644
--- a/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs
+++ b/compiler/rustc_hir_analysis/src/hir_ty_lowering/mod.rs
@@ -2708,16 +2708,6 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ {
         }
     }
 
-    pub fn lower_arg_ty(&self, ty: &hir::Ty<'tcx>, expected_ty: Option<Ty<'tcx>>) -> Ty<'tcx> {
-        match ty.kind {
-            hir::TyKind::Infer(()) if let Some(expected_ty) = expected_ty => {
-                self.record_ty(ty.hir_id, expected_ty, ty.span);
-                expected_ty
-            }
-            _ => self.lower_ty(ty),
-        }
-    }
-
     /// Lower a function type from the HIR to our internal notion of a function signature.
     #[instrument(level = "debug", skip(self, hir_id, safety, abi, decl, generics, hir_ty), ret)]
     pub fn lower_fn_ty(
diff --git a/compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs b/compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs
index 393556928af89..e979798a40294 100644
--- a/compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs
+++ b/compiler/rustc_hir_typeck/src/fn_ctxt/mod.rs
@@ -382,7 +382,7 @@ impl<'tcx> HirTyLowerer<'tcx> for FnCtxt<'_, 'tcx> {
         _hir_id: rustc_hir::HirId,
         _hir_ty: Option<&hir::Ty<'_>>,
     ) -> (Vec<Ty<'tcx>>, Ty<'tcx>) {
-        let input_tys = decl.inputs.iter().map(|a| self.lowerer().lower_arg_ty(a, None)).collect();
+        let input_tys = decl.inputs.iter().map(|a| self.lowerer().lower_ty(a)).collect();
 
         let output_ty = match decl.output {
             hir::FnRetTy::Return(output) => self.lowerer().lower_ty(output),