about summary refs log tree commit diff
path: root/compiler/rustc_errors/src
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/rustc_errors/src')
-rw-r--r--compiler/rustc_errors/src/codes.rs2
-rw-r--r--compiler/rustc_errors/src/decorate_diag.rs85
-rw-r--r--compiler/rustc_errors/src/diagnostic.rs108
-rw-r--r--compiler/rustc_errors/src/diagnostic_impls.rs303
-rw-r--r--compiler/rustc_errors/src/emitter.rs290
-rw-r--r--compiler/rustc_errors/src/lib.rs66
-rw-r--r--compiler/rustc_errors/src/markdown/term.rs8
-rw-r--r--compiler/rustc_errors/src/styled_buffer.rs11
8 files changed, 396 insertions, 477 deletions
diff --git a/compiler/rustc_errors/src/codes.rs b/compiler/rustc_errors/src/codes.rs
index 947cf27ca79..787a8af99b1 100644
--- a/compiler/rustc_errors/src/codes.rs
+++ b/compiler/rustc_errors/src/codes.rs
@@ -20,6 +20,8 @@ impl fmt::Display for ErrCode {
     }
 }
 
+rustc_error_messages::into_diag_arg_using_display!(ErrCode);
+
 macro_rules! define_error_code_constants_and_diagnostics_table {
     ($($name:ident: $num:literal,)*) => (
         $(
diff --git a/compiler/rustc_errors/src/decorate_diag.rs b/compiler/rustc_errors/src/decorate_diag.rs
new file mode 100644
index 00000000000..5aef26ccf97
--- /dev/null
+++ b/compiler/rustc_errors/src/decorate_diag.rs
@@ -0,0 +1,85 @@
+/// This module provides types and traits for buffering lints until later in compilation.
+use rustc_ast::node_id::NodeId;
+use rustc_data_structures::fx::FxIndexMap;
+use rustc_error_messages::MultiSpan;
+use rustc_lint_defs::{BuiltinLintDiag, Lint, LintId};
+
+use crate::{DynSend, LintDiagnostic, LintDiagnosticBox};
+
+/// We can't implement `LintDiagnostic` for `BuiltinLintDiag`, because decorating some of its
+/// variants requires types we don't have yet. So, handle that case separately.
+pub enum DecorateDiagCompat {
+    Dynamic(Box<dyn for<'a> LintDiagnosticBox<'a, ()> + DynSend + 'static>),
+    Builtin(BuiltinLintDiag),
+}
+
+impl std::fmt::Debug for DecorateDiagCompat {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("DecorateDiagCompat").finish()
+    }
+}
+
+impl !LintDiagnostic<'_, ()> for BuiltinLintDiag {}
+
+impl<D: for<'a> LintDiagnostic<'a, ()> + DynSend + 'static> From<D> for DecorateDiagCompat {
+    #[inline]
+    fn from(d: D) -> Self {
+        Self::Dynamic(Box::new(d))
+    }
+}
+
+impl From<BuiltinLintDiag> for DecorateDiagCompat {
+    #[inline]
+    fn from(b: BuiltinLintDiag) -> Self {
+        Self::Builtin(b)
+    }
+}
+
+/// Lints that are buffered up early on in the `Session` before the
+/// `LintLevels` is calculated.
+#[derive(Debug)]
+pub struct BufferedEarlyLint {
+    /// The span of code that we are linting on.
+    pub span: Option<MultiSpan>,
+
+    /// The `NodeId` of the AST node that generated the lint.
+    pub node_id: NodeId,
+
+    /// A lint Id that can be passed to
+    /// `rustc_lint::early::EarlyContextAndPass::check_id`.
+    pub lint_id: LintId,
+
+    /// Customization of the `Diag<'_>` for the lint.
+    pub diagnostic: DecorateDiagCompat,
+}
+
+#[derive(Default, Debug)]
+pub struct LintBuffer {
+    pub map: FxIndexMap<NodeId, Vec<BufferedEarlyLint>>,
+}
+
+impl LintBuffer {
+    pub fn add_early_lint(&mut self, early_lint: BufferedEarlyLint) {
+        self.map.entry(early_lint.node_id).or_default().push(early_lint);
+    }
+
+    pub fn take(&mut self, id: NodeId) -> Vec<BufferedEarlyLint> {
+        // FIXME(#120456) - is `swap_remove` correct?
+        self.map.swap_remove(&id).unwrap_or_default()
+    }
+
+    pub fn buffer_lint(
+        &mut self,
+        lint: &'static Lint,
+        node_id: NodeId,
+        span: impl Into<MultiSpan>,
+        decorate: impl Into<DecorateDiagCompat>,
+    ) {
+        self.add_early_lint(BufferedEarlyLint {
+            lint_id: LintId::of(lint),
+            node_id,
+            span: Some(span.into()),
+            diagnostic: decorate.into(),
+        });
+    }
+}
diff --git a/compiler/rustc_errors/src/diagnostic.rs b/compiler/rustc_errors/src/diagnostic.rs
index a128f8d31a1..ae23ef1e255 100644
--- a/compiler/rustc_errors/src/diagnostic.rs
+++ b/compiler/rustc_errors/src/diagnostic.rs
@@ -8,7 +8,7 @@ use std::path::PathBuf;
 use std::thread::panicking;
 
 use rustc_data_structures::fx::FxIndexMap;
-use rustc_error_messages::{FluentValue, fluent_value_from_str_list_sep_by_and};
+use rustc_error_messages::{DiagArgName, DiagArgValue, IntoDiagArg};
 use rustc_lint_defs::{Applicability, LintExpectationId};
 use rustc_macros::{Decodable, Encodable};
 use rustc_span::source_map::Spanned;
@@ -22,26 +22,6 @@ use crate::{
     Suggestions,
 };
 
-/// Simplified version of `FluentArg` that can implement `Encodable` and `Decodable`. Collection of
-/// `DiagArg` are converted to `FluentArgs` (consuming the collection) at the start of diagnostic
-/// emission.
-pub type DiagArg<'iter> = (&'iter DiagArgName, &'iter DiagArgValue);
-
-/// Name of a diagnostic argument.
-pub type DiagArgName = Cow<'static, str>;
-
-/// Simplified version of `FluentValue` that can implement `Encodable` and `Decodable`. Converted
-/// to a `FluentValue` by the emitter to be used in diagnostic translation.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)]
-pub enum DiagArgValue {
-    Str(Cow<'static, str>),
-    // This gets converted to a `FluentNumber`, which is an `f64`. An `i32`
-    // safely fits in an `f64`. Any integers bigger than that will be converted
-    // to strings in `into_diag_arg` and stored using the `Str` variant.
-    Number(i32),
-    StrListSepByAnd(Vec<Cow<'static, str>>),
-}
-
 pub type DiagArgMap = FxIndexMap<DiagArgName, DiagArgValue>;
 
 /// Trait for types that `Diag::emit` can return as a "guarantee" (or "proof")
@@ -143,36 +123,6 @@ where
     }
 }
 
-/// Converts a value of a type into a `DiagArg` (typically a field of an `Diag` struct).
-/// Implemented as a custom trait rather than `From` so that it is implemented on the type being
-/// converted rather than on `DiagArgValue`, which enables types from other `rustc_*` crates to
-/// implement this.
-pub trait IntoDiagArg {
-    /// Convert `Self` into a `DiagArgValue` suitable for rendering in a diagnostic.
-    ///
-    /// It takes a `path` where "long values" could be written to, if the `DiagArgValue` is too big
-    /// for displaying on the terminal. This path comes from the `Diag` itself. When rendering
-    /// values that come from `TyCtxt`, like `Ty<'_>`, they can use `TyCtxt::short_string`. If a
-    /// value has no shortening logic that could be used, the argument can be safely ignored.
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue;
-}
-
-impl IntoDiagArg for DiagArgValue {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        self
-    }
-}
-
-impl From<DiagArgValue> for FluentValue<'static> {
-    fn from(val: DiagArgValue) -> Self {
-        match val {
-            DiagArgValue::Str(s) => From::from(s),
-            DiagArgValue::Number(n) => From::from(n),
-            DiagArgValue::StrListSepByAnd(l) => fluent_value_from_str_list_sep_by_and(l),
-        }
-    }
-}
-
 /// Trait implemented by error types. This should not be implemented manually. Instead, use
 /// `#[derive(Subdiagnostic)]` -- see [rustc_macros::Subdiagnostic].
 #[rustc_diagnostic_item = "Subdiagnostic"]
@@ -188,10 +138,20 @@ where
 /// `#[derive(LintDiagnostic)]` -- see [rustc_macros::LintDiagnostic].
 #[rustc_diagnostic_item = "LintDiagnostic"]
 pub trait LintDiagnostic<'a, G: EmissionGuarantee> {
-    /// Decorate and emit a lint.
+    /// Decorate a lint with the information from this type.
     fn decorate_lint<'b>(self, diag: &'b mut Diag<'a, G>);
 }
 
+pub trait LintDiagnosticBox<'a, G: EmissionGuarantee> {
+    fn decorate_lint_box<'b>(self: Box<Self>, diag: &'b mut Diag<'a, G>);
+}
+
+impl<'a, G: EmissionGuarantee, D: LintDiagnostic<'a, G>> LintDiagnosticBox<'a, G> for D {
+    fn decorate_lint_box<'b>(self: Box<Self>, diag: &'b mut Diag<'a, G>) {
+        self.decorate_lint(diag);
+    }
+}
+
 #[derive(Clone, Debug, Encodable, Decodable)]
 pub(crate) struct DiagLocation {
     file: Cow<'static, str>,
@@ -617,6 +577,29 @@ impl<'a, G: EmissionGuarantee> Diag<'a, G> {
         self.level = Level::DelayedBug;
     }
 
+    /// Make emitting this diagnostic fatal
+    ///
+    /// Changes the level of this diagnostic to Fatal, and importantly also changes the emission guarantee.
+    /// This is sound for errors that would otherwise be printed, but now simply exit the process instead.
+    /// This function still gives an emission guarantee, the guarantee is now just that it exits fatally.
+    /// For delayed bugs this is different, since those are buffered. If we upgrade one to fatal, another
+    /// might now be ignored.
+    #[rustc_lint_diagnostics]
+    #[track_caller]
+    pub fn upgrade_to_fatal(mut self) -> Diag<'a, FatalAbort> {
+        assert!(
+            matches!(self.level, Level::Error),
+            "upgrade_to_fatal: cannot upgrade {:?} to Fatal: not an error",
+            self.level
+        );
+        self.level = Level::Fatal;
+
+        // Take is okay since we immediately rewrap it in another diagnostic.
+        // i.e. we do emit it despite defusing the original diagnostic's drop bomb.
+        let diag = self.diag.take();
+        Diag { dcx: self.dcx, diag, _marker: PhantomData }
+    }
+
     with_fn! { with_span_label,
     /// Appends a labeled span to the diagnostic.
     ///
@@ -847,17 +830,18 @@ impl<'a, G: EmissionGuarantee> Diag<'a, G> {
         self
     }
 
+    with_fn! { with_span_help,
     /// Prints the span with some help above it.
     /// This is like [`Diag::help()`], but it gets its own span.
     #[rustc_lint_diagnostics]
-    pub fn span_help<S: Into<MultiSpan>>(
+    pub fn span_help(
         &mut self,
-        sp: S,
+        sp: impl Into<MultiSpan>,
         msg: impl Into<SubdiagMessage>,
     ) -> &mut Self {
         self.sub(Level::Help, msg, sp.into());
         self
-    }
+    } }
 
     /// Disallow attaching suggestions to this diagnostic.
     /// Any suggestions attached e.g. with the `span_suggestion_*` methods
@@ -961,11 +945,6 @@ impl<'a, G: EmissionGuarantee> Diag<'a, G> {
             None,
             "Span must not be empty and have no suggestion",
         );
-        debug_assert_eq!(
-            parts.array_windows().find(|[a, b]| a.span.overlaps(b.span)),
-            None,
-            "suggestion must not have overlapping parts",
-        );
 
         self.push_suggestion(CodeSuggestion {
             substitutions: vec![Substitution { parts }],
@@ -1112,7 +1091,7 @@ impl<'a, G: EmissionGuarantee> Diag<'a, G> {
             .map(|snippet| {
                 debug_assert!(
                     !(sp.is_empty() && snippet.is_empty()),
-                    "Span must not be empty and have no suggestion"
+                    "Span `{sp:?}` must not be empty and have no suggestion"
                 );
                 Substitution { parts: vec![SubstitutionPart { snippet, span: sp }] }
             })
@@ -1382,6 +1361,11 @@ impl<'a, G: EmissionGuarantee> Diag<'a, G> {
         &mut self.long_ty_path
     }
 
+    pub fn with_long_ty_path(mut self, long_ty_path: Option<PathBuf>) -> Self {
+        self.long_ty_path = long_ty_path;
+        self
+    }
+
     /// Most `emit_producing_guarantee` functions use this as a starting point.
     fn emit_producing_nothing(mut self) {
         let diag = self.take_diag();
@@ -1421,7 +1405,7 @@ impl<'a, G: EmissionGuarantee> Diag<'a, G> {
     ///
     /// See `emit` and `delay_as_bug` for details.
     #[track_caller]
-    pub fn emit_unless(mut self, delay: bool) -> G::EmitResult {
+    pub fn emit_unless_delay(mut self, delay: bool) -> G::EmitResult {
         if delay {
             self.downgrade_to_delayed_bug();
         }
diff --git a/compiler/rustc_errors/src/diagnostic_impls.rs b/compiler/rustc_errors/src/diagnostic_impls.rs
index eeb9ac28808..435d16a8380 100644
--- a/compiler/rustc_errors/src/diagnostic_impls.rs
+++ b/compiler/rustc_errors/src/diagnostic_impls.rs
@@ -1,317 +1,22 @@
-use std::backtrace::Backtrace;
 use std::borrow::Cow;
-use std::fmt;
-use std::num::ParseIntError;
-use std::path::{Path, PathBuf};
-use std::process::ExitStatus;
 
 use rustc_abi::TargetDataLayoutErrors;
-use rustc_ast::util::parser::ExprPrecedence;
-use rustc_ast_pretty::pprust;
-use rustc_attr_data_structures::RustcVersion;
+use rustc_error_messages::{DiagArgValue, IntoDiagArg};
 use rustc_macros::Subdiagnostic;
-use rustc_span::edition::Edition;
-use rustc_span::{Ident, MacroRulesNormalizedIdent, Span, Symbol};
-use rustc_target::spec::{PanicStrategy, SplitDebuginfo, StackProtector, TargetTuple};
-use rustc_type_ir::{ClosureKind, FloatTy};
-use {rustc_ast as ast, rustc_hir as hir};
+use rustc_span::{Span, Symbol};
 
 use crate::diagnostic::DiagLocation;
 use crate::{
-    Diag, DiagArgValue, DiagCtxtHandle, Diagnostic, EmissionGuarantee, ErrCode, IntoDiagArg, Level,
-    Subdiagnostic, fluent_generated as fluent,
+    Diag, DiagCtxtHandle, Diagnostic, EmissionGuarantee, Level, Subdiagnostic,
+    fluent_generated as fluent,
 };
 
-pub struct DiagArgFromDisplay<'a>(pub &'a dyn fmt::Display);
-
-impl IntoDiagArg for DiagArgFromDisplay<'_> {
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        self.0.to_string().into_diag_arg(path)
-    }
-}
-
-impl<'a> From<&'a dyn fmt::Display> for DiagArgFromDisplay<'a> {
-    fn from(t: &'a dyn fmt::Display) -> Self {
-        DiagArgFromDisplay(t)
-    }
-}
-
-impl<'a, T: fmt::Display> From<&'a T> for DiagArgFromDisplay<'a> {
-    fn from(t: &'a T) -> Self {
-        DiagArgFromDisplay(t)
-    }
-}
-
-impl<'a, T: Clone + IntoDiagArg> IntoDiagArg for &'a T {
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        self.clone().into_diag_arg(path)
-    }
-}
-
-#[macro_export]
-macro_rules! into_diag_arg_using_display {
-    ($( $ty:ty ),+ $(,)?) => {
-        $(
-            impl IntoDiagArg for $ty {
-                fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-                    self.to_string().into_diag_arg(path)
-                }
-            }
-        )+
-    }
-}
-
-macro_rules! into_diag_arg_for_number {
-    ($( $ty:ty ),+ $(,)?) => {
-        $(
-            impl IntoDiagArg for $ty {
-                fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-                    // Convert to a string if it won't fit into `Number`.
-                    #[allow(irrefutable_let_patterns)]
-                    if let Ok(n) = TryInto::<i32>::try_into(self) {
-                        DiagArgValue::Number(n)
-                    } else {
-                        self.to_string().into_diag_arg(path)
-                    }
-                }
-            }
-        )+
-    }
-}
-
-into_diag_arg_using_display!(
-    ast::ParamKindOrd,
-    std::io::Error,
-    Box<dyn std::error::Error>,
-    std::num::NonZero<u32>,
-    hir::Target,
-    Edition,
-    Ident,
-    MacroRulesNormalizedIdent,
-    ParseIntError,
-    StackProtector,
-    &TargetTuple,
-    SplitDebuginfo,
-    ExitStatus,
-    ErrCode,
-    rustc_abi::ExternAbi,
-);
-
-impl IntoDiagArg for RustcVersion {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(self.to_string()))
-    }
-}
-
-impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::TraitRef<I> {
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        self.to_string().into_diag_arg(path)
-    }
-}
-
-impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::ExistentialTraitRef<I> {
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        self.to_string().into_diag_arg(path)
-    }
-}
-
-impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::UnevaluatedConst<I> {
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        format!("{self:?}").into_diag_arg(path)
-    }
-}
-
-impl<I: rustc_type_ir::Interner> IntoDiagArg for rustc_type_ir::FnSig<I> {
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        format!("{self:?}").into_diag_arg(path)
-    }
-}
-
-impl<I: rustc_type_ir::Interner, T> IntoDiagArg for rustc_type_ir::Binder<I, T>
-where
-    T: IntoDiagArg,
-{
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        self.skip_binder().into_diag_arg(path)
-    }
-}
-
-into_diag_arg_for_number!(i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize);
-
-impl IntoDiagArg for bool {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        if self {
-            DiagArgValue::Str(Cow::Borrowed("true"))
-        } else {
-            DiagArgValue::Str(Cow::Borrowed("false"))
-        }
-    }
-}
-
-impl IntoDiagArg for char {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(format!("{self:?}")))
-    }
-}
-
-impl IntoDiagArg for Vec<char> {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::StrListSepByAnd(
-            self.into_iter().map(|c| Cow::Owned(format!("{c:?}"))).collect(),
-        )
-    }
-}
-
-impl IntoDiagArg for Symbol {
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        self.to_ident_string().into_diag_arg(path)
-    }
-}
-
-impl<'a> IntoDiagArg for &'a str {
-    fn into_diag_arg(self, path: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        self.to_string().into_diag_arg(path)
-    }
-}
-
-impl IntoDiagArg for String {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(self))
-    }
-}
-
-impl<'a> IntoDiagArg for Cow<'a, str> {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(self.into_owned()))
-    }
-}
-
-impl<'a> IntoDiagArg for &'a Path {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(self.display().to_string()))
-    }
-}
-
-impl IntoDiagArg for PathBuf {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(self.display().to_string()))
-    }
-}
-
-impl IntoDiagArg for PanicStrategy {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(self.desc().to_string()))
-    }
-}
-
-impl IntoDiagArg for hir::ConstContext {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Borrowed(match self {
-            hir::ConstContext::ConstFn => "const_fn",
-            hir::ConstContext::Static(_) => "static",
-            hir::ConstContext::Const { .. } => "const",
-        }))
-    }
-}
-
-impl IntoDiagArg for ast::Expr {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(pprust::expr_to_string(&self)))
-    }
-}
-
-impl IntoDiagArg for ast::Path {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(pprust::path_to_string(&self)))
-    }
-}
-
-impl IntoDiagArg for ast::token::Token {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(pprust::token_to_string(&self))
-    }
-}
-
-impl IntoDiagArg for ast::token::TokenKind {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(pprust::token_kind_to_string(&self))
-    }
-}
-
-impl IntoDiagArg for FloatTy {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Borrowed(self.name_str()))
-    }
-}
-
-impl IntoDiagArg for std::ffi::CString {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(self.to_string_lossy().into_owned()))
-    }
-}
-
-impl IntoDiagArg for rustc_data_structures::small_c_str::SmallCStr {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Owned(self.to_string_lossy().into_owned()))
-    }
-}
-
-impl IntoDiagArg for ast::Visibility {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        let s = pprust::vis_to_string(&self);
-        let s = s.trim_end().to_string();
-        DiagArgValue::Str(Cow::Owned(s))
-    }
-}
-
-impl IntoDiagArg for rustc_lint_defs::Level {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Borrowed(self.to_cmd_flag()))
-    }
-}
-
-impl<Id> IntoDiagArg for hir::def::Res<Id> {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Borrowed(self.descr()))
-    }
-}
-
 impl IntoDiagArg for DiagLocation {
     fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
         DiagArgValue::Str(Cow::from(self.to_string()))
     }
 }
 
-impl IntoDiagArg for Backtrace {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::from(self.to_string()))
-    }
-}
-
-impl IntoDiagArg for Level {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::from(self.to_string()))
-    }
-}
-
-impl IntoDiagArg for ClosureKind {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(self.as_str().into())
-    }
-}
-
-impl IntoDiagArg for hir::def::Namespace {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Str(Cow::Borrowed(self.descr()))
-    }
-}
-
-impl IntoDiagArg for ExprPrecedence {
-    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
-        DiagArgValue::Number(self as i32)
-    }
-}
-
 #[derive(Clone)]
 pub struct DiagSymbolList<S = Symbol>(Vec<S>);
 
diff --git a/compiler/rustc_errors/src/emitter.rs b/compiler/rustc_errors/src/emitter.rs
index 95400ac2ca3..93b1e6b7615 100644
--- a/compiler/rustc_errors/src/emitter.rs
+++ b/compiler/rustc_errors/src/emitter.rs
@@ -17,7 +17,7 @@ use std::path::Path;
 use std::sync::Arc;
 
 use derive_setters::Setters;
-use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet};
+use rustc_data_structures::fx::{FxIndexMap, FxIndexSet};
 use rustc_data_structures::sync::{DynSend, IntoDynSyncSend};
 use rustc_error_messages::{FluentArgs, SpanLabel};
 use rustc_lexer;
@@ -262,19 +262,11 @@ pub trait Emitter {
                     format!("help: {msg}")
                 } else {
                     // Show the default suggestion text with the substitution
-                    format!(
-                        "help: {}{}: `{}`",
-                        msg,
-                        if self
-                            .source_map()
-                            .is_some_and(|sm| is_case_difference(sm, snippet, part.span,))
-                        {
-                            " (notice the capitalization)"
-                        } else {
-                            ""
-                        },
-                        snippet,
-                    )
+                    let confusion_type = self
+                        .source_map()
+                        .map(|sm| detect_confusion_type(sm, snippet, part.span))
+                        .unwrap_or(ConfusionType::None);
+                    format!("help: {}{}: `{}`", msg, confusion_type.label_text(), snippet,)
                 };
                 primary_span.push_span_label(part.span, msg);
 
@@ -417,7 +409,7 @@ pub trait Emitter {
                 if !redundant_span || always_backtrace {
                     let msg: Cow<'static, _> = match trace.kind {
                         ExpnKind::Macro(MacroKind::Attr, _) => {
-                            "this procedural macro expansion".into()
+                            "this attribute macro expansion".into()
                         }
                         ExpnKind::Macro(MacroKind::Derive, _) => {
                             "this derive macro expansion".into()
@@ -713,8 +705,7 @@ impl HumanEmitter {
                 Style::LineNumber,
             );
         }
-        buffer.puts(line_offset, 0, &self.maybe_anonymized(line_index), Style::LineNumber);
-
+        self.draw_line_num(buffer, line_index, line_offset, width_offset - 3);
         self.draw_col_separator_no_space(buffer, line_offset, width_offset - 2);
         left
     }
@@ -1471,7 +1462,7 @@ impl HumanEmitter {
         max_line_num_len: usize,
         is_secondary: bool,
         is_cont: bool,
-    ) -> io::Result<()> {
+    ) -> io::Result<CodeWindowStatus> {
         let mut buffer = StyledBuffer::new();
 
         if !msp.has_primary_spans() && !msp.has_span_labels() && is_secondary && !self.short_message
@@ -1584,12 +1575,14 @@ impl HumanEmitter {
         }
         let mut annotated_files = FileWithAnnotatedLines::collect_annotations(self, args, msp);
         trace!("{annotated_files:#?}");
+        let mut code_window_status = CodeWindowStatus::Open;
 
         // Make sure our primary file comes first
         let primary_span = msp.primary_span().unwrap_or_default();
         let (Some(sm), false) = (self.sm.as_ref(), primary_span.is_dummy()) else {
             // If we don't have span information, emit and exit
-            return emit_to_destination(&buffer.render(), level, &mut self.dst, self.short_message);
+            return emit_to_destination(&buffer.render(), level, &mut self.dst, self.short_message)
+                .map(|_| code_window_status);
         };
         let primary_lo = sm.lookup_char_pos(primary_span.lo());
         if let Ok(pos) =
@@ -1598,8 +1591,12 @@ impl HumanEmitter {
             annotated_files.swap(0, pos);
         }
 
+        // An end column separator should be emitted when a file with with a
+        // source, is followed by one without a source
+        let mut col_sep_before_no_show_source = false;
+        let annotated_files_len = annotated_files.len();
         // Print out the annotate source lines that correspond with the error
-        for annotated_file in annotated_files {
+        for (file_idx, annotated_file) in annotated_files.into_iter().enumerate() {
             // we can't annotate anything if the source is unavailable.
             if !should_show_source_code(
                 &self.ignored_directories_in_source_blocks,
@@ -1607,6 +1604,26 @@ impl HumanEmitter {
                 &annotated_file.file,
             ) {
                 if !self.short_message {
+                    // Add an end column separator when a file without a source
+                    // comes after one with a source
+                    //    ╭▸ $DIR/deriving-meta-unknown-trait.rs:1:10
+                    //    │
+                    // LL │ #[derive(Eqr)]
+                    //    │          ━━━
+                    //    ╰╴ (<- It prints *this* line)
+                    //    ╭▸ $SRC_DIR/core/src/cmp.rs:356:0
+                    //    │
+                    //    ╰╴note: similarly named derive macro `Eq` defined here
+                    if col_sep_before_no_show_source {
+                        let buffer_msg_line_offset = buffer.num_lines();
+                        self.draw_col_separator_end(
+                            &mut buffer,
+                            buffer_msg_line_offset,
+                            max_line_num_len + 1,
+                        );
+                    }
+                    col_sep_before_no_show_source = false;
+
                     // We'll just print an unannotated message.
                     for (annotation_id, line) in annotated_file.lines.iter().enumerate() {
                         let mut annotations = line.annotations.clone();
@@ -1647,29 +1664,42 @@ impl HumanEmitter {
                             }
                             line_idx += 1;
                         }
-                        for (label, is_primary) in labels.into_iter() {
+                        if is_cont
+                            && file_idx == annotated_files_len - 1
+                            && annotation_id == annotated_file.lines.len() - 1
+                            && !labels.is_empty()
+                        {
+                            code_window_status = CodeWindowStatus::Closed;
+                        }
+                        let labels_len = labels.len();
+                        for (label_idx, (label, is_primary)) in labels.into_iter().enumerate() {
                             let style = if is_primary {
                                 Style::LabelPrimary
                             } else {
                                 Style::LabelSecondary
                             };
-                            let pipe = self.col_separator();
-                            buffer.prepend(line_idx, &format!(" {pipe}"), Style::LineNumber);
-                            for _ in 0..max_line_num_len {
-                                buffer.prepend(line_idx, " ", Style::NoStyle);
-                            }
+                            self.draw_col_separator_no_space(
+                                &mut buffer,
+                                line_idx,
+                                max_line_num_len + 1,
+                            );
                             line_idx += 1;
-                            let chr = self.note_separator();
-                            buffer.append(line_idx, &format!(" {chr} note: "), style);
-                            for _ in 0..max_line_num_len {
-                                buffer.prepend(line_idx, " ", Style::NoStyle);
-                            }
+                            self.draw_note_separator(
+                                &mut buffer,
+                                line_idx,
+                                max_line_num_len + 1,
+                                label_idx != labels_len - 1,
+                            );
+                            buffer.append(line_idx, "note", Style::MainHeaderMsg);
+                            buffer.append(line_idx, ": ", Style::NoStyle);
                             buffer.append(line_idx, label, style);
                             line_idx += 1;
                         }
                     }
                 }
                 continue;
+            } else {
+                col_sep_before_no_show_source = true;
             }
 
             // print out the span location and spacer before we print the annotated source
@@ -1856,10 +1886,12 @@ impl HumanEmitter {
                         width_offset,
                         code_offset,
                         margin,
-                        !is_cont && line_idx + 1 == annotated_file.lines.len(),
+                        !is_cont
+                            && file_idx + 1 == annotated_files_len
+                            && line_idx + 1 == annotated_file.lines.len(),
                     );
 
-                    let mut to_add = FxHashMap::default();
+                    let mut to_add = FxIndexMap::default();
 
                     for (depth, style) in depths {
                         // FIXME(#120456) - is `swap_remove` correct?
@@ -1982,7 +2014,7 @@ impl HumanEmitter {
         // final step: take our styled buffer, render it, then output it
         emit_to_destination(&buffer.render(), level, &mut self.dst, self.short_message)?;
 
-        Ok(())
+        Ok(code_window_status)
     }
 
     fn column_width(&self, code_offset: usize) -> usize {
@@ -2029,12 +2061,12 @@ impl HumanEmitter {
         buffer.append(0, ": ", Style::HeaderMsg);
 
         let mut msg = vec![(suggestion.msg.to_owned(), Style::NoStyle)];
-        if suggestions
-            .iter()
-            .take(MAX_SUGGESTIONS)
-            .any(|(_, _, _, only_capitalization)| *only_capitalization)
+        if let Some(confusion_type) =
+            suggestions.iter().take(MAX_SUGGESTIONS).find_map(|(_, _, _, confusion_type)| {
+                if confusion_type.has_confusion() { Some(*confusion_type) } else { None }
+            })
         {
-            msg.push((" (notice the capitalization difference)".into(), Style::NoStyle));
+            msg.push((confusion_type.label_text().into(), Style::NoStyle));
         }
         self.msgs_to_buffer(
             &mut buffer,
@@ -2128,11 +2160,11 @@ impl HumanEmitter {
                 // Account for a suggestion to completely remove a line(s) with whitespace (#94192).
                 let line_end = sm.lookup_char_pos(parts[0].span.hi()).line;
                 for line in line_start..=line_end {
-                    buffer.puts(
+                    self.draw_line_num(
+                        &mut buffer,
+                        line,
                         row_num - 1 + line - line_start,
-                        0,
-                        &self.maybe_anonymized(line),
-                        Style::LineNumber,
+                        max_line_num_len,
                     );
                     buffer.puts(
                         row_num - 1 + line - line_start,
@@ -2322,7 +2354,6 @@ impl HumanEmitter {
                         .sum();
                     let underline_start = (span_start_pos + start) as isize + offset;
                     let underline_end = (span_start_pos + start + sub_len) as isize + offset;
-                    assert!(underline_start >= 0 && underline_end >= 0);
                     let padding: usize = max_line_num_len + 3;
                     for p in underline_start..underline_end {
                         if let DisplaySuggestion::Underline = show_code_change
@@ -2497,7 +2528,7 @@ impl HumanEmitter {
             !children.is_empty()
                 || suggestions.iter().any(|s| s.style != SuggestionStyle::CompletelyHidden),
         ) {
-            Ok(()) => {
+            Ok(code_window_status) => {
                 if !children.is_empty()
                     || suggestions.iter().any(|s| s.style != SuggestionStyle::CompletelyHidden)
                 {
@@ -2508,7 +2539,7 @@ impl HumanEmitter {
                         {
                             // We'll continue the vertical bar to point into the next note.
                             self.draw_col_separator_no_space(&mut buffer, 0, max_line_num_len + 1);
-                        } else {
+                        } else if matches!(code_window_status, CodeWindowStatus::Open) {
                             // We'll close the vertical bar to visually end the code window.
                             self.draw_col_separator_end(&mut buffer, 0, max_line_num_len + 1);
                         }
@@ -2612,12 +2643,7 @@ impl HumanEmitter {
             // For more info: https://github.com/rust-lang/rust/issues/92741
             let lines_to_remove = file_lines.lines.iter().take(file_lines.lines.len() - 1);
             for (index, line_to_remove) in lines_to_remove.enumerate() {
-                buffer.puts(
-                    *row_num - 1,
-                    0,
-                    &self.maybe_anonymized(line_num + index),
-                    Style::LineNumber,
-                );
+                self.draw_line_num(buffer, line_num + index, *row_num - 1, max_line_num_len);
                 buffer.puts(*row_num - 1, max_line_num_len + 1, "- ", Style::Removal);
                 let line = normalize_whitespace(
                     &file_lines.file.get_line(line_to_remove.line_index).unwrap(),
@@ -2634,11 +2660,11 @@ impl HumanEmitter {
             let last_line_index = file_lines.lines[file_lines.lines.len() - 1].line_index;
             let last_line = &file_lines.file.get_line(last_line_index).unwrap();
             if last_line != line_to_add {
-                buffer.puts(
+                self.draw_line_num(
+                    buffer,
+                    line_num + file_lines.lines.len() - 1,
                     *row_num - 1,
-                    0,
-                    &self.maybe_anonymized(line_num + file_lines.lines.len() - 1),
-                    Style::LineNumber,
+                    max_line_num_len,
                 );
                 buffer.puts(*row_num - 1, max_line_num_len + 1, "- ", Style::Removal);
                 buffer.puts(
@@ -2661,7 +2687,7 @@ impl HumanEmitter {
                     // 2 -     .await
                     //   |
                     // *row_num -= 1;
-                    buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber);
+                    self.draw_line_num(buffer, line_num, *row_num, max_line_num_len);
                     buffer.puts(*row_num, max_line_num_len + 1, "+ ", Style::Addition);
                     buffer.append(*row_num, &normalize_whitespace(line_to_add), Style::NoStyle);
                 } else {
@@ -2671,7 +2697,7 @@ impl HumanEmitter {
                 *row_num -= 2;
             }
         } else if is_multiline {
-            buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber);
+            self.draw_line_num(buffer, line_num, *row_num, max_line_num_len);
             match &highlight_parts {
                 [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
                     buffer.puts(*row_num, max_line_num_len + 1, "+ ", Style::Addition);
@@ -2702,11 +2728,11 @@ impl HumanEmitter {
                 Style::NoStyle,
             );
         } else if let DisplaySuggestion::Add = show_code_change {
-            buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber);
+            self.draw_line_num(buffer, line_num, *row_num, max_line_num_len);
             buffer.puts(*row_num, max_line_num_len + 1, "+ ", Style::Addition);
             buffer.append(*row_num, &normalize_whitespace(line_to_add), Style::NoStyle);
         } else {
-            buffer.puts(*row_num, 0, &self.maybe_anonymized(line_num), Style::LineNumber);
+            self.draw_line_num(buffer, line_num, *row_num, max_line_num_len);
             self.draw_col_separator(buffer, *row_num, max_line_num_len + 1);
             buffer.append(*row_num, &normalize_whitespace(line_to_add), Style::NoStyle);
         }
@@ -2840,10 +2866,11 @@ impl HumanEmitter {
         }
     }
 
-    fn note_separator(&self) -> char {
+    fn note_separator(&self, is_cont: bool) -> &'static str {
         match self.theme {
-            OutputTheme::Ascii => '=',
-            OutputTheme::Unicode => '╰',
+            OutputTheme::Ascii => "= ",
+            OutputTheme::Unicode if is_cont => "├ ",
+            OutputTheme::Unicode => "╰ ",
         }
     }
 
@@ -2956,11 +2983,7 @@ impl HumanEmitter {
         col: usize,
         is_cont: bool,
     ) {
-        let chr = match self.theme {
-            OutputTheme::Ascii => "= ",
-            OutputTheme::Unicode if is_cont => "├ ",
-            OutputTheme::Unicode => "╰ ",
-        };
+        let chr = self.note_separator(is_cont);
         buffer.puts(line, col, chr, Style::LineNumber);
     }
 
@@ -2991,7 +3014,7 @@ impl HumanEmitter {
     fn secondary_file_start(&self) -> &'static str {
         match self.theme {
             OutputTheme::Ascii => "::: ",
-            OutputTheme::Unicode => " ⸬ ",
+            OutputTheme::Unicode => " ⸬  ",
         }
     }
 
@@ -3016,6 +3039,22 @@ impl HumanEmitter {
             OutputTheme::Unicode => "…",
         }
     }
+
+    fn draw_line_num(
+        &self,
+        buffer: &mut StyledBuffer,
+        line_num: usize,
+        line_offset: usize,
+        max_line_num_len: usize,
+    ) {
+        let line_num = self.maybe_anonymized(line_num);
+        buffer.puts(
+            line_offset,
+            max_line_num_len.saturating_sub(str_width(&line_num)),
+            &line_num,
+            Style::LineNumber,
+        );
+    }
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -3045,6 +3084,12 @@ enum DisplaySuggestion {
     Add,
 }
 
+#[derive(Clone, Copy, Debug)]
+enum CodeWindowStatus {
+    Closed,
+    Open,
+}
+
 impl FileWithAnnotatedLines {
     /// Preprocess all the annotations so that they are grouped by file and by line number
     /// This helps us quickly iterate over the whole message (including secondary file spans)
@@ -3518,24 +3563,107 @@ pub fn is_different(sm: &SourceMap, suggested: &str, sp: Span) -> bool {
 }
 
 /// Whether the original and suggested code are visually similar enough to warrant extra wording.
-pub fn is_case_difference(sm: &SourceMap, suggested: &str, sp: Span) -> bool {
-    // FIXME: this should probably be extended to also account for `FO0` → `FOO` and unicode.
+pub fn detect_confusion_type(sm: &SourceMap, suggested: &str, sp: Span) -> ConfusionType {
     let found = match sm.span_to_snippet(sp) {
         Ok(snippet) => snippet,
         Err(e) => {
             warn!(error = ?e, "Invalid span {:?}", sp);
-            return false;
+            return ConfusionType::None;
         }
     };
-    let ascii_confusables = &['c', 'f', 'i', 'k', 'o', 's', 'u', 'v', 'w', 'x', 'y', 'z'];
-    // All the chars that differ in capitalization are confusable (above):
-    let confusable = iter::zip(found.chars(), suggested.chars())
-        .filter(|(f, s)| f != s)
-        .all(|(f, s)| ascii_confusables.contains(&f) || ascii_confusables.contains(&s));
-    confusable && found.to_lowercase() == suggested.to_lowercase()
-            // FIXME: We sometimes suggest the same thing we already have, which is a
-            //        bug, but be defensive against that here.
-            && found != suggested
+
+    let mut has_case_confusion = false;
+    let mut has_digit_letter_confusion = false;
+
+    if found.len() == suggested.len() {
+        let mut has_case_diff = false;
+        let mut has_digit_letter_confusable = false;
+        let mut has_other_diff = false;
+
+        let ascii_confusables = &['c', 'f', 'i', 'k', 'o', 's', 'u', 'v', 'w', 'x', 'y', 'z'];
+
+        let digit_letter_confusables = [('0', 'O'), ('1', 'l'), ('5', 'S'), ('8', 'B'), ('9', 'g')];
+
+        for (f, s) in iter::zip(found.chars(), suggested.chars()) {
+            if f != s {
+                if f.eq_ignore_ascii_case(&s) {
+                    // Check for case differences (any character that differs only in case)
+                    if ascii_confusables.contains(&f) || ascii_confusables.contains(&s) {
+                        has_case_diff = true;
+                    } else {
+                        has_other_diff = true;
+                    }
+                } else if digit_letter_confusables.contains(&(f, s))
+                    || digit_letter_confusables.contains(&(s, f))
+                {
+                    // Check for digit-letter confusables (like 0 vs O, 1 vs l, etc.)
+                    has_digit_letter_confusable = true;
+                } else {
+                    has_other_diff = true;
+                }
+            }
+        }
+
+        // If we have case differences and no other differences
+        if has_case_diff && !has_other_diff && found != suggested {
+            has_case_confusion = true;
+        }
+        if has_digit_letter_confusable && !has_other_diff && found != suggested {
+            has_digit_letter_confusion = true;
+        }
+    }
+
+    match (has_case_confusion, has_digit_letter_confusion) {
+        (true, true) => ConfusionType::Both,
+        (true, false) => ConfusionType::Case,
+        (false, true) => ConfusionType::DigitLetter,
+        (false, false) => ConfusionType::None,
+    }
+}
+
+/// Represents the type of confusion detected between original and suggested code.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ConfusionType {
+    /// No confusion detected
+    None,
+    /// Only case differences (e.g., "hello" vs "Hello")
+    Case,
+    /// Only digit-letter confusion (e.g., "0" vs "O", "1" vs "l")
+    DigitLetter,
+    /// Both case and digit-letter confusion
+    Both,
+}
+
+impl ConfusionType {
+    /// Returns the appropriate label text for this confusion type.
+    pub fn label_text(&self) -> &'static str {
+        match self {
+            ConfusionType::None => "",
+            ConfusionType::Case => " (notice the capitalization)",
+            ConfusionType::DigitLetter => " (notice the digit/letter confusion)",
+            ConfusionType::Both => " (notice the capitalization and digit/letter confusion)",
+        }
+    }
+
+    /// Combines two confusion types. If either is `Both`, the result is `Both`.
+    /// If one is `Case` and the other is `DigitLetter`, the result is `Both`.
+    /// Otherwise, returns the non-`None` type, or `None` if both are `None`.
+    pub fn combine(self, other: ConfusionType) -> ConfusionType {
+        match (self, other) {
+            (ConfusionType::None, other) => other,
+            (this, ConfusionType::None) => this,
+            (ConfusionType::Both, _) | (_, ConfusionType::Both) => ConfusionType::Both,
+            (ConfusionType::Case, ConfusionType::DigitLetter)
+            | (ConfusionType::DigitLetter, ConfusionType::Case) => ConfusionType::Both,
+            (ConfusionType::Case, ConfusionType::Case) => ConfusionType::Case,
+            (ConfusionType::DigitLetter, ConfusionType::DigitLetter) => ConfusionType::DigitLetter,
+        }
+    }
+
+    /// Returns true if this confusion type represents any kind of confusion.
+    pub fn has_confusion(&self) -> bool {
+        *self != ConfusionType::None
+    }
 }
 
 pub(crate) fn should_show_source_code(
diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs
index 381d780077d..8869799ce90 100644
--- a/compiler/rustc_errors/src/lib.rs
+++ b/compiler/rustc_errors/src/lib.rs
@@ -40,27 +40,27 @@ use std::{fmt, panic};
 
 use Level::*;
 pub use codes::*;
+pub use decorate_diag::{BufferedEarlyLint, DecorateDiagCompat, LintBuffer};
 pub use diagnostic::{
-    BugAbort, Diag, DiagArg, DiagArgMap, DiagArgName, DiagArgValue, DiagInner, DiagStyledString,
-    Diagnostic, EmissionGuarantee, FatalAbort, IntoDiagArg, LintDiagnostic, StringPart, Subdiag,
-    Subdiagnostic,
+    BugAbort, Diag, DiagArgMap, DiagInner, DiagStyledString, Diagnostic, EmissionGuarantee,
+    FatalAbort, LintDiagnostic, LintDiagnosticBox, StringPart, Subdiag, Subdiagnostic,
 };
 pub use diagnostic_impls::{
-    DiagArgFromDisplay, DiagSymbolList, ElidedLifetimeInPathSubdiag, ExpectedLifetimeParameter,
+    DiagSymbolList, ElidedLifetimeInPathSubdiag, ExpectedLifetimeParameter,
     IndicateAnonymousLifetime, SingleLabelManySpans,
 };
 pub use emitter::ColorConfig;
-use emitter::{DynEmitter, Emitter, is_case_difference, is_different};
+use emitter::{ConfusionType, DynEmitter, Emitter, detect_confusion_type, is_different};
 use rustc_data_structures::AtomicRef;
 use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet};
 use rustc_data_structures::stable_hasher::StableHasher;
 use rustc_data_structures::sync::{DynSend, Lock};
 pub use rustc_error_messages::{
-    DiagMessage, FluentBundle, LanguageIdentifier, LazyFallbackBundle, MultiSpan, SpanLabel,
-    SubdiagMessage, fallback_fluent_bundle, fluent_bundle,
+    DiagArg, DiagArgFromDisplay, DiagArgName, DiagArgValue, DiagMessage, FluentBundle, IntoDiagArg,
+    LanguageIdentifier, LazyFallbackBundle, MultiSpan, SpanLabel, SubdiagMessage,
+    fallback_fluent_bundle, fluent_bundle, into_diag_arg_using_display,
 };
 use rustc_hashes::Hash128;
-use rustc_hir::HirId;
 pub use rustc_lint_defs::{Applicability, listify, pluralize};
 use rustc_lint_defs::{Lint, LintExpectationId};
 use rustc_macros::{Decodable, Encodable};
@@ -80,6 +80,7 @@ use crate::timings::TimingRecord;
 
 pub mod annotate_snippet_emitter_writer;
 pub mod codes;
+mod decorate_diag;
 mod diagnostic;
 mod diagnostic_impls;
 pub mod emitter;
@@ -108,13 +109,14 @@ 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 {
+    type Id: Copy;
     #[track_caller]
     fn emit_node_span_lint(
         self,
         lint: &'static Lint,
-        hir_id: HirId,
+        hir_id: Self::Id,
         span: impl Into<MultiSpan>,
-        decorator: impl for<'a> LintDiagnostic<'a, ()>,
+        decorator: impl for<'a> LintDiagnostic<'a, ()> + DynSend + 'static,
     );
 }
 
@@ -308,7 +310,7 @@ impl CodeSuggestion {
     pub(crate) fn splice_lines(
         &self,
         sm: &SourceMap,
-    ) -> Vec<(String, Vec<SubstitutionPart>, Vec<Vec<SubstitutionHighlight>>, bool)> {
+    ) -> Vec<(String, Vec<SubstitutionPart>, Vec<Vec<SubstitutionHighlight>>, ConfusionType)> {
         // For the `Vec<Vec<SubstitutionHighlight>>` value, the first level of the vector
         // corresponds to the output snippet's lines, while the second level corresponds to the
         // substrings within that line that should be highlighted.
@@ -379,6 +381,17 @@ impl CodeSuggestion {
                 // Assumption: all spans are in the same file, and all spans
                 // are disjoint. Sort in ascending order.
                 substitution.parts.sort_by_key(|part| part.span.lo());
+                // Verify the assumption that all spans are disjoint
+                assert_eq!(
+                    substitution.parts.array_windows().find(|[a, b]| a.span.overlaps(b.span)),
+                    None,
+                    "all spans must be disjoint",
+                );
+
+                // Account for cases where we are suggesting the same code that's already
+                // there. This shouldn't happen often, but in some cases for multipart
+                // suggestions it's much easier to handle it here than in the origin.
+                substitution.parts.retain(|p| is_different(sm, &p.snippet, p.span));
 
                 // Find the bounding span.
                 let lo = substitution.parts.iter().map(|part| part.span.lo()).min()?;
@@ -414,14 +427,15 @@ impl CodeSuggestion {
                 // We need to keep track of the difference between the existing code and the added
                 // or deleted code in order to point at the correct column *after* substitution.
                 let mut acc = 0;
-                let mut only_capitalization = false;
+                let mut confusion_type = ConfusionType::None;
                 for part in &mut substitution.parts {
                     // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the
                     // suggestion and snippet to look as if we just suggested to add
                     // `"b"`, which is typically much easier for the user to understand.
                     part.trim_trivial_replacements(sm);
 
-                    only_capitalization |= is_case_difference(sm, &part.snippet, part.span);
+                    let part_confusion = detect_confusion_type(sm, &part.snippet, part.span);
+                    confusion_type = confusion_type.combine(part_confusion);
                     let cur_lo = sm.lookup_char_pos(part.span.lo());
                     if prev_hi.line == cur_lo.line {
                         let mut count =
@@ -467,16 +481,12 @@ impl CodeSuggestion {
                             _ => 1,
                         })
                         .sum();
-                    if !is_different(sm, &part.snippet, part.span) {
-                        // Account for cases where we are suggesting the same code that's already
-                        // there. This shouldn't happen often, but in some cases for multipart
-                        // suggestions it's much easier to handle it here than in the origin.
-                    } else {
-                        line_highlight.push(SubstitutionHighlight {
-                            start: (cur_lo.col.0 as isize + acc) as usize,
-                            end: (cur_lo.col.0 as isize + acc + len) as usize,
-                        });
-                    }
+
+                    line_highlight.push(SubstitutionHighlight {
+                        start: (cur_lo.col.0 as isize + acc) as usize,
+                        end: (cur_lo.col.0 as isize + acc + len) as usize,
+                    });
+
                     buf.push_str(&part.snippet);
                     let cur_hi = sm.lookup_char_pos(part.span.hi());
                     // Account for the difference between the width of the current code and the
@@ -511,7 +521,7 @@ impl CodeSuggestion {
                 if highlights.iter().all(|parts| parts.is_empty()) {
                     None
                 } else {
-                    Some((buf, substitution.parts, highlights, only_capitalization))
+                    Some((buf, substitution.parts, highlights, confusion_type))
                 }
             })
             .collect()
@@ -1157,7 +1167,7 @@ impl<'a> DiagCtxtHandle<'a> {
         // - It's only produce with JSON output.
         // - It's not emitted the usual way, via `emit_diagnostic`.
         // - The `$message_type` field is "unused_externs" rather than the usual
-        //   "diagnosic".
+        //   "diagnostic".
         //
         // We count it as a lint error because it has a lint level. The value
         // of `loud` (which comes from "unused-externs" or
@@ -1998,6 +2008,12 @@ impl Level {
     }
 }
 
+impl IntoDiagArg for Level {
+    fn into_diag_arg(self, _: &mut Option<std::path::PathBuf>) -> DiagArgValue {
+        DiagArgValue::Str(Cow::from(self.to_string()))
+    }
+}
+
 // FIXME(eddyb) this doesn't belong here AFAICT, should be moved to callsite.
 pub fn elided_lifetime_in_path_suggestion(
     source_map: &SourceMap,
diff --git a/compiler/rustc_errors/src/markdown/term.rs b/compiler/rustc_errors/src/markdown/term.rs
index 579e00b8b85..fe1d80bdbe8 100644
--- a/compiler/rustc_errors/src/markdown/term.rs
+++ b/compiler/rustc_errors/src/markdown/term.rs
@@ -18,7 +18,7 @@ thread_local! {
 pub(crate) fn entrypoint(stream: &MdStream<'_>, buf: &mut Buffer) -> io::Result<()> {
     #[cfg(not(test))]
     if let Some((w, _)) = termize::dimensions() {
-        WIDTH.with(|c| c.set(std::cmp::min(w, DEFAULT_COLUMN_WIDTH)));
+        WIDTH.set(std::cmp::min(w, DEFAULT_COLUMN_WIDTH));
     }
     write_stream(stream, buf, None, 0)?;
     buf.write_all(b"\n")
@@ -84,7 +84,7 @@ fn write_tt(tt: &MdTree<'_>, buf: &mut Buffer, indent: usize) -> io::Result<()>
             reset_cursor();
         }
         MdTree::HorizontalRule => {
-            (0..WIDTH.with(Cell::get)).for_each(|_| buf.write_all(b"-").unwrap());
+            (0..WIDTH.get()).for_each(|_| buf.write_all(b"-").unwrap());
             reset_cursor();
         }
         MdTree::Heading(n, stream) => {
@@ -121,7 +121,7 @@ fn write_tt(tt: &MdTree<'_>, buf: &mut Buffer, indent: usize) -> io::Result<()>
 
 /// End of that block, just wrap the line
 fn reset_cursor() {
-    CURSOR.with(|cur| cur.set(0));
+    CURSOR.set(0);
 }
 
 /// Change to be generic on Write for testing. If we have a link URL, we don't
@@ -144,7 +144,7 @@ fn write_wrapping<B: io::Write>(
                 buf.write_all(ind_ws)?;
                 cur.set(indent);
             }
-            let ch_count = WIDTH.with(Cell::get) - cur.get();
+            let ch_count = WIDTH.get() - cur.get();
             let mut iter = to_write.char_indices();
             let Some((end_idx, _ch)) = iter.nth(ch_count) else {
                 // Write entire line
diff --git a/compiler/rustc_errors/src/styled_buffer.rs b/compiler/rustc_errors/src/styled_buffer.rs
index 790efd0286e..095502e80aa 100644
--- a/compiler/rustc_errors/src/styled_buffer.rs
+++ b/compiler/rustc_errors/src/styled_buffer.rs
@@ -153,12 +153,11 @@ impl StyledBuffer {
     /// 1. That line and column exist in `StyledBuffer`
     /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation`
     fn set_style(&mut self, line: usize, col: usize, style: Style, overwrite: bool) {
-        if let Some(ref mut line) = self.lines.get_mut(line) {
-            if let Some(StyledChar { style: s, .. }) = line.get_mut(col) {
-                if overwrite || matches!(s, Style::NoStyle | Style::Quotation) {
-                    *s = style;
-                }
-            }
+        if let Some(ref mut line) = self.lines.get_mut(line)
+            && let Some(StyledChar { style: s, .. }) = line.get_mut(col)
+            && (overwrite || matches!(s, Style::NoStyle | Style::Quotation))
+        {
+            *s = style;
         }
     }
 }