diff --git a/compiler/rustc_builtin_macros/messages.ftl b/compiler/rustc_builtin_macros/messages.ftl
index 4d088e27b36..83dc1ac50e5 100644
--- a/compiler/rustc_builtin_macros/messages.ftl
+++ b/compiler/rustc_builtin_macros/messages.ftl
@@ -3,3 +3,149 @@ builtin_macros_requires_cfg_pattern =
     .label = cfg-pattern required
 
 builtin_macros_expected_one_cfg_pattern = expected 1 cfg-pattern
+
+builtin_macros_alloc_error_must_be_fn = alloc_error_handler must be a function
+
+builtin_macros_assert_requires_boolean = macro requires a boolean expression as an argument
+    .label = boolean expression required
+
+builtin_macros_assert_requires_expression = macro requires an expression as an argument
+    .suggestion = try removing semicolon
+
+builtin_macros_assert_missing_comma = unexpected string literal
+    .suggestion = try adding a comma
+
+builtin_macros_cfg_accessible_unspecified_path = `cfg_accessible` path is not specified
+builtin_macros_cfg_accessible_multiple_paths = multiple `cfg_accessible` paths are specified
+builtin_macros_cfg_accessible_literal_path = `cfg_accessible` path cannot be a literal
+builtin_macros_cfg_accessible_has_args = `cfg_accessible` path cannot accept arguments
+
+builtin_macros_cfg_accessible_indeterminate = cannot determine whether the path is accessible or not
+
+builtin_macros_concat_bytestr = cannot concatenate a byte string literal
+
+builtin_macros_concat_missing_literal = expected a literal
+    .note = only literals (like `"foo"`, `-42` and `3.14`) can be passed to `concat!()`
+
+builtin_macros_concat_bytes_missing_literal = expected a byte literal
+    .note = only byte literals (like `b"foo"`, `b's'` and `[3, 4, 5]`) can be passed to `concat_bytes!()`
+
+builtin_macros_concat_bytes_invalid = cannot concatenate {$lit_kind} literals
+    .byte_char = try using a byte character
+    .byte_str = try using a byte string
+    .number_array = try wrapping the number in an array
+
+builtin_macros_concat_bytes_oob = numeric literal is out of bounds
+
+builtin_macros_concat_bytes_non_u8 = numeric literal is not a `u8`
+
+builtin_macros_concat_bytes_array = cannot concatenate doubly nested array
+    .note = byte strings are treated as arrays of bytes
+    .help = try flattening the array
+
+builtin_macros_concat_bytes_bad_repeat = repeat count is not a positive number
+
+builtin_macros_concat_idents_missing_args = `concat_idents!()` takes 1 or more arguments
+builtin_macros_concat_idents_missing_comma = `concat_idents!()` expecting comma
+builtin_macros_concat_idents_ident_args = `concat_idents!()` requires ident args
+
+builtin_macros_bad_derive_target = `derive` may only be applied to `struct`s, `enum`s and `union`s
+    .label = not applicable here
+    .label2 = not a `struct`, `enum` or `union`
+
+builtin_macros_unexpected_lit = expected path to a trait, found literal
+    .label = not a trait
+    .str_lit = try using `#[derive({$sym})]`
+    .other = for example, write `#[derive(Debug)]` for `Debug`
+
+builtin_macros_derive_path_args_list = traits in `#[derive(...)]` don't accept arguments
+    .suggestion = remove the arguments
+
+builtin_macros_derive_path_args_value = traits in `#[derive(...)]` don't accept values
+    .suggestion = remove the value
+
+builtin_macros_derive_macro_call = `derive` cannot be used on items with type macros
+
+builtin_macros_cannot_derive_union = this trait cannot be derived for unions
+
+builtin_macros_no_default_variant = no default declared
+    .help = make a unit variant default by placing `#[default]` above it
+    .suggestion = make `{$ident}` default
+
+builtin_macros_multiple_defaults = multiple declared defaults
+    .label = first default
+    .additional = additional default
+    .note = only one variant can be default
+    .suggestion = make `{$ident}` default
+
+builtin_macros_non_unit_default = the `#[default]` attribute may only be used on unit enum variants
+    .help = consider a manual implementation of `Default`
+
+builtin_macros_non_exhaustive_default = default variant must be exhaustive
+    .label = declared `#[non_exhaustive]` here
+    .help = consider a manual implementation of `Default`
+
+builtin_macros_multiple_default_attrs = multiple `#[default]` attributes
+    .note = only one `#[default]` attribute is needed
+    .label = `#[default]` used here
+    .label_again = `#[default]` used again here
+    .help = try removing {$only_one ->
+    [true] this
+    *[false] these
+    }
+
+builtin_macros_default_arg = `#[default]` attribute does not accept a value
+    .suggestion = try using `#[default]`
+
+builtin_macros_env_takes_args = `env!()` takes 1 or 2 arguments
+
+builtin_macros_env_not_defined = environment variable `{$var}` not defined at compile time
+    .cargo = Cargo sets build script variables at run time. Use `std::env::var("{$var}")` instead
+    .other = use `std::env::var("{$var}")` to read the variable at run time
+
+builtin_macros_format_requires_string = requires at least a format string argument
+
+builtin_macros_format_duplicate_arg = duplicate argument named `{$ident}`
+    .label1 = previously here
+    .label2 = duplicate argument
+
+builtin_macros_format_positional_after_named = positional arguments cannot follow named arguments
+    .label = positional arguments must be before named arguments
+    .named_args = named argument
+
+builtin_macros_format_string_invalid = invalid format string: {$desc}
+    .label = {$label1} in format string
+    .note = {$note}
+    .second_label = {$label}
+
+builtin_macros_sugg = consider using a positional formatting argument instead
+
+builtin_macros_format_no_arg_named = there is no argument named `{$name}`
+    .note = did you intend to capture a variable `{$name}` from the surrounding scope?
+    .note2 = to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro
+
+builtin_macros_format_unknown_trait = unknown format trait `{$ty}`
+    .note = the only appropriate formatting traits are:
+                                            - ``, which uses the `Display` trait
+                                            - `?`, which uses the `Debug` trait
+                                            - `e`, which uses the `LowerExp` trait
+                                            - `E`, which uses the `UpperExp` trait
+                                            - `o`, which uses the `Octal` trait
+                                            - `p`, which uses the `Pointer` trait
+                                            - `b`, which uses the `Binary` trait
+                                            - `x`, which uses the `LowerHex` trait
+                                            - `X`, which uses the `UpperHex` trait
+    .suggestion = use the `{$trait_name}` trait
+
+builtin_macros_format_unused_arg = {$named ->
+    [true] named argument
+    *[false] argument
+    } never used
+
+builtin_macros_format_unused_args = multiple unused formatting arguments
+    .label = multiple missing formatting specifiers
+
+builtin_macros_format_pos_mismatch = {$n} positional {$n ->
+    [one] argument
+    *[more] arguments
+    } in format string, but {$desc}
diff --git a/compiler/rustc_builtin_macros/src/alloc_error_handler.rs b/compiler/rustc_builtin_macros/src/alloc_error_handler.rs
index ac6697232cb..82bae9157e7 100644
--- a/compiler/rustc_builtin_macros/src/alloc_error_handler.rs
+++ b/compiler/rustc_builtin_macros/src/alloc_error_handler.rs
@@ -1,3 +1,4 @@
+use crate::errors;
 use crate::util::check_builtin_macro_attribute;
 
 use rustc_ast::ptr::P;
@@ -31,7 +32,7 @@ pub fn expand(
         {
             (item, true, ecx.with_def_site_ctxt(fn_kind.sig.span))
         } else {
-            ecx.sess.parse_sess.span_diagnostic.span_err(item.span(), "alloc_error_handler must be a function");
+            ecx.sess.parse_sess.span_diagnostic.emit_err(errors::AllocErrorMustBeFn {span: item.span() });
             return vec![orig_item];
         };
 
diff --git a/compiler/rustc_builtin_macros/src/assert.rs b/compiler/rustc_builtin_macros/src/assert.rs
index 75af5e2b1fa..0de424be2f1 100644
--- a/compiler/rustc_builtin_macros/src/assert.rs
+++ b/compiler/rustc_builtin_macros/src/assert.rs
@@ -1,12 +1,13 @@
 mod context;
 
 use crate::edition_panic::use_panic_2021;
+use crate::errors;
 use rustc_ast::ptr::P;
 use rustc_ast::token;
 use rustc_ast::tokenstream::{DelimSpan, TokenStream};
 use rustc_ast::{DelimArgs, Expr, ExprKind, MacCall, MacDelimiter, Path, PathSegment, UnOp};
 use rustc_ast_pretty::pprust;
-use rustc_errors::{Applicability, PResult};
+use rustc_errors::PResult;
 use rustc_expand::base::{DummyResult, ExtCtxt, MacEager, MacResult};
 use rustc_parse::parser::Parser;
 use rustc_span::symbol::{sym, Ident, Symbol};
@@ -114,9 +115,7 @@ fn parse_assert<'a>(cx: &mut ExtCtxt<'a>, sp: Span, stream: TokenStream) -> PRes
     let mut parser = cx.new_parser_from_tts(stream);
 
     if parser.token == token::Eof {
-        let mut err = cx.struct_span_err(sp, "macro requires a boolean expression as an argument");
-        err.span_label(sp, "boolean expression required");
-        return Err(err);
+        return Err(cx.create_err(errors::AssertRequiresBoolean { span: sp }));
     }
 
     let cond_expr = parser.parse_expr()?;
@@ -129,15 +128,7 @@ fn parse_assert<'a>(cx: &mut ExtCtxt<'a>, sp: Span, stream: TokenStream) -> PRes
     //
     // Emit an error about semicolon and suggest removing it.
     if parser.token == token::Semi {
-        let mut err = cx.struct_span_err(sp, "macro requires an expression as an argument");
-        err.span_suggestion(
-            parser.token.span,
-            "try removing semicolon",
-            "",
-            Applicability::MaybeIncorrect,
-        );
-        err.emit();
-
+        cx.emit_err(errors::AssertRequiresExpression { span: sp, token: parser.token.span });
         parser.bump();
     }
 
@@ -149,15 +140,8 @@ fn parse_assert<'a>(cx: &mut ExtCtxt<'a>, sp: Span, stream: TokenStream) -> PRes
     // Emit an error and suggest inserting a comma.
     let custom_message =
         if let token::Literal(token::Lit { kind: token::Str, .. }) = parser.token.kind {
-            let mut err = cx.struct_span_err(parser.token.span, "unexpected string literal");
-            let comma_span = parser.prev_token.span.shrink_to_hi();
-            err.span_suggestion_short(
-                comma_span,
-                "try adding a comma",
-                ", ",
-                Applicability::MaybeIncorrect,
-            );
-            err.emit();
+            let comma = parser.prev_token.span.shrink_to_hi();
+            cx.emit_err(errors::AssertMissingComma { span: parser.token.span, comma });
 
             parse_custom_message(&mut parser)
         } else if parser.eat(&token::Comma) {
diff --git a/compiler/rustc_builtin_macros/src/cfg.rs b/compiler/rustc_builtin_macros/src/cfg.rs
index 5638c2f6180..1397cee7af8 100644
--- a/compiler/rustc_builtin_macros/src/cfg.rs
+++ b/compiler/rustc_builtin_macros/src/cfg.rs
@@ -2,13 +2,13 @@
 //! a literal `true` or `false` based on whether the given cfg matches the
 //! current compilation environment.
 
+use crate::errors;
 use rustc_ast as ast;
 use rustc_ast::token;
 use rustc_ast::tokenstream::TokenStream;
 use rustc_attr as attr;
 use rustc_errors::PResult;
 use rustc_expand::base::{self, *};
-use rustc_macros::Diagnostic;
 use rustc_span::Span;
 
 pub fn expand_cfg(
@@ -35,26 +35,11 @@ pub fn expand_cfg(
     }
 }
 
-#[derive(Diagnostic)]
-#[diag(builtin_macros_requires_cfg_pattern)]
-struct RequiresCfgPattern {
-    #[primary_span]
-    #[label]
-    span: Span,
-}
-
-#[derive(Diagnostic)]
-#[diag(builtin_macros_expected_one_cfg_pattern)]
-struct OneCfgPattern {
-    #[primary_span]
-    span: Span,
-}
-
 fn parse_cfg<'a>(cx: &mut ExtCtxt<'a>, span: Span, tts: TokenStream) -> PResult<'a, ast::MetaItem> {
     let mut p = cx.new_parser_from_tts(tts);
 
     if p.token == token::Eof {
-        return Err(cx.create_err(RequiresCfgPattern { span }));
+        return Err(cx.create_err(errors::RequiresCfgPattern { span }));
     }
 
     let cfg = p.parse_meta_item()?;
@@ -62,7 +47,7 @@ fn parse_cfg<'a>(cx: &mut ExtCtxt<'a>, span: Span, tts: TokenStream) -> PResult<
     let _ = p.eat(&token::Comma);
 
     if !p.eat(&token::Eof) {
-        return Err(cx.create_err(OneCfgPattern { span }));
+        return Err(cx.create_err(errors::OneCfgPattern { span }));
     }
 
     Ok(cfg)
diff --git a/compiler/rustc_builtin_macros/src/cfg_accessible.rs b/compiler/rustc_builtin_macros/src/cfg_accessible.rs
index 4e4cafc7182..37ac09ccdff 100644
--- a/compiler/rustc_builtin_macros/src/cfg_accessible.rs
+++ b/compiler/rustc_builtin_macros/src/cfg_accessible.rs
@@ -1,5 +1,6 @@
 //! Implementation of the `#[cfg_accessible(path)]` attribute macro.
 
+use crate::errors;
 use rustc_ast as ast;
 use rustc_expand::base::{Annotatable, ExpandResult, ExtCtxt, Indeterminate, MultiItemModifier};
 use rustc_feature::AttributeTemplate;
@@ -10,15 +11,22 @@ use rustc_span::Span;
 pub(crate) struct Expander;
 
 fn validate_input<'a>(ecx: &mut ExtCtxt<'_>, mi: &'a ast::MetaItem) -> Option<&'a ast::Path> {
+    use errors::CfgAccessibleInvalid::*;
     match mi.meta_item_list() {
         None => {}
-        Some([]) => ecx.span_err(mi.span, "`cfg_accessible` path is not specified"),
-        Some([_, .., l]) => ecx.span_err(l.span(), "multiple `cfg_accessible` paths are specified"),
+        Some([]) => {
+            ecx.emit_err(UnspecifiedPath(mi.span));
+        }
+        Some([_, .., l]) => {
+            ecx.emit_err(MultiplePaths(l.span()));
+        }
         Some([nmi]) => match nmi.meta_item() {
-            None => ecx.span_err(nmi.span(), "`cfg_accessible` path cannot be a literal"),
+            None => {
+                ecx.emit_err(LiteralPath(nmi.span()));
+            }
             Some(mi) => {
                 if !mi.is_word() {
-                    ecx.span_err(mi.span, "`cfg_accessible` path cannot accept arguments");
+                    ecx.emit_err(HasArguments(mi.span));
                 }
                 return Some(&mi.path);
             }
@@ -53,7 +61,7 @@ impl MultiItemModifier for Expander {
             Ok(true) => ExpandResult::Ready(vec![item]),
             Ok(false) => ExpandResult::Ready(Vec::new()),
             Err(Indeterminate) if ecx.force_mode => {
-                ecx.span_err(span, "cannot determine whether the path is accessible or not");
+                ecx.emit_err(errors::CfgAccessibleIndeterminate { span });
                 ExpandResult::Ready(vec![item])
             }
             Err(Indeterminate) => ExpandResult::Retry(item),
diff --git a/compiler/rustc_builtin_macros/src/compile_error.rs b/compiler/rustc_builtin_macros/src/compile_error.rs
index 72397aa2500..aeb3bb80045 100644
--- a/compiler/rustc_builtin_macros/src/compile_error.rs
+++ b/compiler/rustc_builtin_macros/src/compile_error.rs
@@ -13,6 +13,11 @@ pub fn expand_compile_error<'cx>(
         return DummyResult::any(sp);
     };
 
+    #[expect(
+        rustc::diagnostic_outside_of_impl,
+        reason = "diagnostic message is specified by user"
+    )]
+    #[expect(rustc::untranslatable_diagnostic, reason = "diagnostic message is specified by user")]
     cx.span_err(sp, var.as_str());
 
     DummyResult::any(sp)
diff --git a/compiler/rustc_builtin_macros/src/concat.rs b/compiler/rustc_builtin_macros/src/concat.rs
index 36682bbe070..b92964d03e9 100644
--- a/compiler/rustc_builtin_macros/src/concat.rs
+++ b/compiler/rustc_builtin_macros/src/concat.rs
@@ -4,6 +4,8 @@ use rustc_expand::base::{self, DummyResult};
 use rustc_session::errors::report_lit_error;
 use rustc_span::symbol::Symbol;
 
+use crate::errors;
+
 pub fn expand_concat(
     cx: &mut base::ExtCtxt<'_>,
     sp: rustc_span::Span,
@@ -31,7 +33,7 @@ pub fn expand_concat(
                     accumulator.push_str(&b.to_string());
                 }
                 Ok(ast::LitKind::Byte(..) | ast::LitKind::ByteStr(..)) => {
-                    cx.span_err(e.span, "cannot concatenate a byte string literal");
+                    cx.emit_err(errors::ConcatBytestr { span: e.span });
                     has_errors = true;
                 }
                 Ok(ast::LitKind::Err) => {
@@ -55,7 +57,7 @@ pub fn expand_concat(
                 }
             }
             ast::ExprKind::IncludedBytes(..) => {
-                cx.span_err(e.span, "cannot concatenate a byte string literal")
+                cx.emit_err(errors::ConcatBytestr { span: e.span });
             }
             ast::ExprKind::Err => {
                 has_errors = true;
@@ -67,9 +69,7 @@ pub fn expand_concat(
     }
 
     if !missing_literal.is_empty() {
-        let mut err = cx.struct_span_err(missing_literal, "expected a literal");
-        err.note("only literals (like `\"foo\"`, `-42` and `3.14`) can be passed to `concat!()`");
-        err.emit();
+        cx.emit_err(errors::ConcatMissingLiteral { spans: missing_literal });
         return DummyResult::any(sp);
     } else if has_errors {
         return DummyResult::any(sp);
diff --git a/compiler/rustc_builtin_macros/src/concat_bytes.rs b/compiler/rustc_builtin_macros/src/concat_bytes.rs
index 4f1a7d709ff..ba639c0a9fe 100644
--- a/compiler/rustc_builtin_macros/src/concat_bytes.rs
+++ b/compiler/rustc_builtin_macros/src/concat_bytes.rs
@@ -1,10 +1,11 @@
 use rustc_ast as ast;
 use rustc_ast::{ptr::P, tokenstream::TokenStream};
-use rustc_errors::Applicability;
 use rustc_expand::base::{self, DummyResult};
 use rustc_session::errors::report_lit_error;
 use rustc_span::Span;
 
+use crate::errors;
+
 /// Emits errors for literal expressions that are invalid inside and outside of an array.
 fn invalid_type_err(
     cx: &mut base::ExtCtxt<'_>,
@@ -12,62 +13,46 @@ fn invalid_type_err(
     span: Span,
     is_nested: bool,
 ) {
+    use errors::{
+        ConcatBytesInvalid, ConcatBytesInvalidSuggestion, ConcatBytesNonU8, ConcatBytesOob,
+    };
+    let snippet = cx.sess.source_map().span_to_snippet(span).ok();
     match ast::LitKind::from_token_lit(token_lit) {
         Ok(ast::LitKind::Char(_)) => {
-            let mut err = cx.struct_span_err(span, "cannot concatenate character literals");
-            if let Ok(snippet) = cx.sess.source_map().span_to_snippet(span) {
-                err.span_suggestion(
-                    span,
-                    "try using a byte character",
-                    format!("b{}", snippet),
-                    Applicability::MachineApplicable,
-                )
-                .emit();
-            }
+            let sugg =
+                snippet.map(|snippet| ConcatBytesInvalidSuggestion::CharLit { span, snippet });
+            cx.sess.emit_err(ConcatBytesInvalid { span, lit_kind: "character", sugg });
         }
         Ok(ast::LitKind::Str(_, _)) => {
-            let mut err = cx.struct_span_err(span, "cannot concatenate string literals");
             // suggestion would be invalid if we are nested
-            if !is_nested {
-                if let Ok(snippet) = cx.sess.source_map().span_to_snippet(span) {
-                    err.span_suggestion(
-                        span,
-                        "try using a byte string",
-                        format!("b{}", snippet),
-                        Applicability::MachineApplicable,
-                    );
-                }
-            }
-            err.emit();
+            let sugg = if !is_nested {
+                snippet.map(|snippet| ConcatBytesInvalidSuggestion::StrLit { span, snippet })
+            } else {
+                None
+            };
+            cx.emit_err(ConcatBytesInvalid { span, lit_kind: "string", sugg });
         }
         Ok(ast::LitKind::Float(_, _)) => {
-            cx.span_err(span, "cannot concatenate float literals");
+            cx.emit_err(ConcatBytesInvalid { span, lit_kind: "float", sugg: None });
         }
         Ok(ast::LitKind::Bool(_)) => {
-            cx.span_err(span, "cannot concatenate boolean literals");
+            cx.emit_err(ConcatBytesInvalid { span, lit_kind: "boolean", sugg: None });
         }
         Ok(ast::LitKind::Err) => {}
         Ok(ast::LitKind::Int(_, _)) if !is_nested => {
-            let mut err = cx.struct_span_err(span, "cannot concatenate numeric literals");
-            if let Ok(snippet) = cx.sess.source_map().span_to_snippet(span) {
-                err.span_suggestion(
-                    span,
-                    "try wrapping the number in an array",
-                    format!("[{}]", snippet),
-                    Applicability::MachineApplicable,
-                );
-            }
-            err.emit();
+            let sugg =
+                snippet.map(|snippet| ConcatBytesInvalidSuggestion::IntLit { span: span, snippet });
+            cx.emit_err(ConcatBytesInvalid { span, lit_kind: "numeric", sugg });
         }
         Ok(ast::LitKind::Int(
             val,
             ast::LitIntType::Unsuffixed | ast::LitIntType::Unsigned(ast::UintTy::U8),
         )) => {
             assert!(val > u8::MAX.into()); // must be an error
-            cx.span_err(span, "numeric literal is out of bounds");
+            cx.emit_err(ConcatBytesOob { span });
         }
         Ok(ast::LitKind::Int(_, _)) => {
-            cx.span_err(span, "numeric literal is not a `u8`");
+            cx.emit_err(ConcatBytesNonU8 { span });
         }
         Ok(ast::LitKind::ByteStr(..) | ast::LitKind::Byte(_)) => unreachable!(),
         Err(err) => {
@@ -85,7 +70,7 @@ fn handle_array_element(
     match expr.kind {
         ast::ExprKind::Array(_) | ast::ExprKind::Repeat(_, _) => {
             if !*has_errors {
-                cx.span_err(expr.span, "cannot concatenate doubly nested array");
+                cx.emit_err(errors::ConcatBytesArray { span: expr.span, bytestr: false });
             }
             *has_errors = true;
             None
@@ -99,10 +84,7 @@ fn handle_array_element(
             Ok(ast::LitKind::Byte(val)) => Some(val),
             Ok(ast::LitKind::ByteStr(..)) => {
                 if !*has_errors {
-                    cx.struct_span_err(expr.span, "cannot concatenate doubly nested array")
-                        .note("byte strings are treated as arrays of bytes")
-                        .help("try flattening the array")
-                        .emit();
+                    cx.emit_err(errors::ConcatBytesArray { span: expr.span, bytestr: true });
                 }
                 *has_errors = true;
                 None
@@ -117,10 +99,7 @@ fn handle_array_element(
         },
         ast::ExprKind::IncludedBytes(..) => {
             if !*has_errors {
-                cx.struct_span_err(expr.span, "cannot concatenate doubly nested array")
-                    .note("byte strings are treated as arrays of bytes")
-                    .help("try flattening the array")
-                    .emit();
+                cx.emit_err(errors::ConcatBytesArray { span: expr.span, bytestr: false });
             }
             *has_errors = true;
             None
@@ -167,7 +146,7 @@ pub fn expand_concat_bytes(
                         }
                     }
                 } else {
-                    cx.span_err(count.value.span, "repeat count is not a positive number");
+                    cx.emit_err(errors::ConcatBytesBadRepeat {span: count.value.span });
                 }
             }
             &ast::ExprKind::Lit(token_lit) => match ast::LitKind::from_token_lit(token_lit) {
@@ -196,9 +175,7 @@ pub fn expand_concat_bytes(
         }
     }
     if !missing_literals.is_empty() {
-        let mut err = cx.struct_span_err(missing_literals, "expected a byte literal");
-        err.note("only byte literals (like `b\"foo\"`, `b's'`, and `[3, 4, 5]`) can be passed to `concat_bytes!()`");
-        err.emit();
+        cx.emit_err(errors::ConcatBytesMissingLiteral { spans: missing_literals });
         return base::MacEager::expr(DummyResult::raw_expr(sp, true));
     } else if has_errors {
         return base::MacEager::expr(DummyResult::raw_expr(sp, true));
diff --git a/compiler/rustc_builtin_macros/src/concat_idents.rs b/compiler/rustc_builtin_macros/src/concat_idents.rs
index 297c604e020..8c737f04323 100644
--- a/compiler/rustc_builtin_macros/src/concat_idents.rs
+++ b/compiler/rustc_builtin_macros/src/concat_idents.rs
@@ -6,13 +6,15 @@ use rustc_expand::base::{self, *};
 use rustc_span::symbol::{Ident, Symbol};
 use rustc_span::Span;
 
+use crate::errors;
+
 pub fn expand_concat_idents<'cx>(
     cx: &'cx mut ExtCtxt<'_>,
     sp: Span,
     tts: TokenStream,
 ) -> Box<dyn base::MacResult + 'cx> {
     if tts.is_empty() {
-        cx.span_err(sp, "concat_idents! takes 1 or more arguments");
+        cx.emit_err(errors::ConcatIdentsMissingArgs { span: sp });
         return DummyResult::any(sp);
     }
 
@@ -22,7 +24,7 @@ pub fn expand_concat_idents<'cx>(
             match e {
                 TokenTree::Token(Token { kind: token::Comma, .. }, _) => {}
                 _ => {
-                    cx.span_err(sp, "concat_idents! expecting comma");
+                    cx.emit_err(errors::ConcatIdentsMissingComma { span: sp });
                     return DummyResult::any(sp);
                 }
             }
@@ -34,7 +36,7 @@ pub fn expand_concat_idents<'cx>(
                 }
             }
 
-            cx.span_err(sp, "concat_idents! requires ident args");
+            cx.emit_err(errors::ConcatIdentsIdentArgs { span: sp });
             return DummyResult::any(sp);
         }
     }
diff --git a/compiler/rustc_builtin_macros/src/derive.rs b/compiler/rustc_builtin_macros/src/derive.rs
index 2a8dc02849e..fe4483104ee 100644
--- a/compiler/rustc_builtin_macros/src/derive.rs
+++ b/compiler/rustc_builtin_macros/src/derive.rs
@@ -1,8 +1,8 @@
 use crate::cfg_eval::cfg_eval;
+use crate::errors;
 
 use rustc_ast as ast;
 use rustc_ast::{GenericParamKind, ItemKind, MetaItemKind, NestedMetaItem, StmtKind};
-use rustc_errors::{struct_span_err, Applicability};
 use rustc_expand::base::{Annotatable, ExpandResult, ExtCtxt, Indeterminate, MultiItemModifier};
 use rustc_feature::AttributeTemplate;
 use rustc_parse::validate_attr;
@@ -116,49 +116,33 @@ fn report_bad_target(sess: &Session, item: &Annotatable, span: Span) -> bool {
     let bad_target =
         !matches!(item_kind, Some(ItemKind::Struct(..) | ItemKind::Enum(..) | ItemKind::Union(..)));
     if bad_target {
-        struct_span_err!(
-            sess,
-            span,
-            E0774,
-            "`derive` may only be applied to `struct`s, `enum`s and `union`s",
-        )
-        .span_label(span, "not applicable here")
-        .span_label(item.span(), "not a `struct`, `enum` or `union`")
-        .emit();
+        sess.emit_err(errors::BadDeriveTarget { span, item: item.span() });
     }
     bad_target
 }
 
 fn report_unexpected_meta_item_lit(sess: &Session, lit: &ast::MetaItemLit) {
-    let help_msg = match lit.kind {
+    let help = match lit.kind {
         ast::LitKind::Str(_, ast::StrStyle::Cooked)
             if rustc_lexer::is_ident(lit.symbol.as_str()) =>
         {
-            format!("try using `#[derive({})]`", lit.symbol)
+            errors::BadDeriveLitHelp::StrLit { sym: lit.symbol }
         }
-        _ => "for example, write `#[derive(Debug)]` for `Debug`".to_string(),
+        _ => errors::BadDeriveLitHelp::Other,
     };
-    struct_span_err!(sess, lit.span, E0777, "expected path to a trait, found literal",)
-        .span_label(lit.span, "not a trait")
-        .help(&help_msg)
-        .emit();
+    sess.emit_err(errors::BadDeriveLit { span: lit.span, help });
 }
 
 fn report_path_args(sess: &Session, meta: &ast::MetaItem) {
-    let report_error = |title, action| {
-        let span = meta.span.with_lo(meta.path.span.hi());
-        sess.struct_span_err(span, title)
-            .span_suggestion(span, action, "", Applicability::MachineApplicable)
-            .emit();
-    };
+    let span = meta.span.with_lo(meta.path.span.hi());
+
     match meta.kind {
         MetaItemKind::Word => {}
-        MetaItemKind::List(..) => report_error(
-            "traits in `#[derive(...)]` don't accept arguments",
-            "remove the arguments",
-        ),
+        MetaItemKind::List(..) => {
+            sess.emit_err(errors::DerivePathArgsList { span });
+        }
         MetaItemKind::NameValue(..) => {
-            report_error("traits in `#[derive(...)]` don't accept values", "remove the value")
+            sess.emit_err(errors::DerivePathArgsValue { span });
         }
     }
 }
diff --git a/compiler/rustc_builtin_macros/src/deriving/default.rs b/compiler/rustc_builtin_macros/src/deriving/default.rs
index cc32739d083..33fe98b40e1 100644
--- a/compiler/rustc_builtin_macros/src/deriving/default.rs
+++ b/compiler/rustc_builtin_macros/src/deriving/default.rs
@@ -1,8 +1,8 @@
 use crate::deriving::generic::ty::*;
 use crate::deriving::generic::*;
+use crate::errors;
 use rustc_ast as ast;
 use rustc_ast::{attr, walk_list, EnumDef, VariantData};
-use rustc_errors::Applicability;
 use rustc_expand::base::{Annotatable, DummyResult, ExtCtxt};
 use rustc_span::symbol::Ident;
 use rustc_span::symbol::{kw, sym};
@@ -118,67 +118,50 @@ fn extract_default_variant<'a>(
                 .filter(|variant| matches!(variant.data, VariantData::Unit(..)))
                 .filter(|variant| !attr::contains_name(&variant.attrs, sym::non_exhaustive));
 
-            let mut diag = cx.struct_span_err(trait_span, "no default declared");
-            diag.help("make a unit variant default by placing `#[default]` above it");
-            for variant in possible_defaults {
-                // Suggest making each unit variant default.
-                diag.tool_only_span_suggestion(
-                    variant.span,
-                    &format!("make `{}` default", variant.ident),
-                    format!("#[default] {}", variant.ident),
-                    Applicability::MaybeIncorrect,
-                );
-            }
-            diag.emit();
+            let suggs = possible_defaults
+                .map(|v| errors::NoDefaultVariantSugg { span: v.span, ident: v.ident })
+                .collect();
+            cx.emit_err(errors::NoDefaultVariant { span: trait_span, suggs });
 
             return Err(());
         }
         [first, rest @ ..] => {
-            let mut diag = cx.struct_span_err(trait_span, "multiple declared defaults");
-            diag.span_label(first.span, "first default");
-            diag.span_labels(rest.iter().map(|variant| variant.span), "additional default");
-            diag.note("only one variant can be default");
-            for variant in &default_variants {
-                // Suggest making each variant already tagged default.
-                let suggestion = default_variants
-                    .iter()
-                    .filter_map(|v| {
-                        if v.span == variant.span {
-                            None
-                        } else {
-                            Some((attr::find_by_name(&v.attrs, kw::Default)?.span, String::new()))
-                        }
-                    })
-                    .collect();
-
-                diag.tool_only_multipart_suggestion(
-                    &format!("make `{}` default", variant.ident),
-                    suggestion,
-                    Applicability::MaybeIncorrect,
-                );
-            }
-            diag.emit();
-
+            let suggs = default_variants
+                .iter()
+                .map(|variant| {
+                    let spans = default_variants
+                        .iter()
+                        .filter_map(|v| {
+                            if v.span == variant.span {
+                                None
+                            } else {
+                                Some(attr::find_by_name(&v.attrs, kw::Default)?.span)
+                            }
+                        })
+                        .collect();
+                    errors::MultipleDefaultsSugg { spans, ident: variant.ident }
+                })
+                .collect();
+            cx.emit_err(errors::MultipleDefaults {
+                span: trait_span,
+                first: first.span,
+                additional: rest.iter().map(|v| v.span).collect(),
+                suggs,
+            });
             return Err(());
         }
     };
 
     if !matches!(variant.data, VariantData::Unit(..)) {
-        cx.struct_span_err(
-            variant.ident.span,
-            "the `#[default]` attribute may only be used on unit enum variants",
-        )
-        .help("consider a manual implementation of `Default`")
-        .emit();
-
+        cx.emit_err(errors::NonUnitDefault { span: variant.ident.span });
         return Err(());
     }
 
     if let Some(non_exhaustive_attr) = attr::find_by_name(&variant.attrs, sym::non_exhaustive) {
-        cx.struct_span_err(variant.ident.span, "default variant must be exhaustive")
-            .span_label(non_exhaustive_attr.span, "declared `#[non_exhaustive]` here")
-            .help("consider a manual implementation of `Default`")
-            .emit();
+        cx.emit_err(errors::NonExhaustiveDefault {
+            span: variant.ident.span,
+            non_exhaustive: non_exhaustive_attr.span,
+        });
 
         return Err(());
     }
@@ -199,35 +182,23 @@ fn validate_default_attribute(
             "this method must only be called with a variant that has a `#[default]` attribute",
         ),
         [first, rest @ ..] => {
-            let suggestion_text =
-                if rest.len() == 1 { "try removing this" } else { "try removing these" };
-
-            cx.struct_span_err(default_variant.ident.span, "multiple `#[default]` attributes")
-                .note("only one `#[default]` attribute is needed")
-                .span_label(first.span, "`#[default]` used here")
-                .span_label(rest[0].span, "`#[default]` used again here")
-                .span_help(rest.iter().map(|attr| attr.span).collect::<Vec<_>>(), suggestion_text)
-                // This would otherwise display the empty replacement, hence the otherwise
-                // repetitive `.span_help` call above.
-                .tool_only_multipart_suggestion(
-                    suggestion_text,
-                    rest.iter().map(|attr| (attr.span, String::new())).collect(),
-                    Applicability::MachineApplicable,
-                )
-                .emit();
+            let sugg = errors::MultipleDefaultAttrsSugg {
+                spans: rest.iter().map(|attr| attr.span).collect(),
+            };
+            cx.emit_err(errors::MultipleDefaultAttrs {
+                span: default_variant.ident.span,
+                first: first.span,
+                first_rest: rest[0].span,
+                rest: rest.iter().map(|attr| attr.span).collect::<Vec<_>>().into(),
+                only_one: rest.len() == 1,
+                sugg,
+            });
 
             return Err(());
         }
     };
     if !attr.is_word() {
-        cx.struct_span_err(attr.span, "`#[default]` attribute does not accept a value")
-            .span_suggestion_hidden(
-                attr.span,
-                "try using `#[default]`",
-                "#[default]",
-                Applicability::MaybeIncorrect,
-            )
-            .emit();
+        cx.emit_err(errors::DefaultHasArg { span: attr.span });
 
         return Err(());
     }
@@ -241,12 +212,7 @@ struct DetectNonVariantDefaultAttr<'a, 'b> {
 impl<'a, 'b> rustc_ast::visit::Visitor<'a> for DetectNonVariantDefaultAttr<'a, 'b> {
     fn visit_attribute(&mut self, attr: &'a rustc_ast::Attribute) {
         if attr.has_name(kw::Default) {
-            self.cx
-                .struct_span_err(
-                    attr.span,
-                    "the `#[default]` attribute may only be used on unit enum variants",
-                )
-                .emit();
+            self.cx.emit_err(errors::NonUnitDefault { span: attr.span });
         }
 
         rustc_ast::visit::walk_attribute(self, attr);
diff --git a/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs b/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
index 6b3053fdfac..e5a00331588 100644
--- a/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
+++ b/compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
@@ -162,7 +162,7 @@
 pub use StaticFields::*;
 pub use SubstructureFields::*;
 
-use crate::deriving;
+use crate::{deriving, errors};
 use rustc_ast::ptr::P;
 use rustc_ast::{
     self as ast, BindingAnnotation, ByRef, EnumDef, Expr, GenericArg, GenericParamKind, Generics,
@@ -415,7 +415,7 @@ fn find_type_parameters(
         }
 
         fn visit_mac_call(&mut self, mac: &ast::MacCall) {
-            self.cx.span_err(mac.span(), "`derive` cannot be used on items with type macros");
+            self.cx.emit_err(errors::DeriveMacroCall { span: mac.span() });
         }
     }
 
@@ -488,7 +488,7 @@ impl<'a> TraitDef<'a> {
                                 is_packed,
                             )
                         } else {
-                            cx.span_err(mitem.span, "this trait cannot be derived for unions");
+                            cx.emit_err(errors::DeriveUnion { span: mitem.span });
                             return;
                         }
                     }
diff --git a/compiler/rustc_builtin_macros/src/env.rs b/compiler/rustc_builtin_macros/src/env.rs
index f011cb754cb..58c972738c4 100644
--- a/compiler/rustc_builtin_macros/src/env.rs
+++ b/compiler/rustc_builtin_macros/src/env.rs
@@ -11,6 +11,8 @@ use rustc_span::Span;
 use std::env;
 use thin_vec::thin_vec;
 
+use crate::errors;
+
 pub fn expand_option_env<'cx>(
     cx: &'cx mut ExtCtxt<'_>,
     sp: Span,
@@ -54,7 +56,7 @@ pub fn expand_env<'cx>(
 ) -> Box<dyn base::MacResult + 'cx> {
     let mut exprs = match get_exprs_from_tts(cx, tts) {
         Some(exprs) if exprs.is_empty() || exprs.len() > 2 => {
-            cx.span_err(sp, "env! takes 1 or 2 arguments");
+            cx.emit_err(errors::EnvTakesArgs { span: sp });
             return DummyResult::any(sp);
         }
         None => return DummyResult::any(sp),
@@ -78,18 +80,12 @@ pub fn expand_env<'cx>(
     cx.sess.parse_sess.env_depinfo.borrow_mut().insert((var, value));
     let e = match value {
         None => {
-            let (msg, help) = match custom_msg {
-                None => (
-                    format!("environment variable `{var}` not defined at compile time"),
-                    Some(help_for_missing_env_var(var.as_str())),
-                ),
-                Some(s) => (s.to_string(), None),
-            };
-            let mut diag = cx.struct_span_err(sp, &msg);
-            if let Some(help) = help {
-                diag.help(help);
-            }
-            diag.emit();
+            cx.emit_err(errors::EnvNotDefined {
+                span: sp,
+                msg: custom_msg,
+                var,
+                help: custom_msg.is_none().then(|| help_for_missing_env_var(var.as_str())),
+            });
             return DummyResult::any(sp);
         }
         Some(value) => cx.expr_str(sp, value),
@@ -97,15 +93,13 @@ pub fn expand_env<'cx>(
     MacEager::expr(e)
 }
 
-fn help_for_missing_env_var(var: &str) -> String {
+fn help_for_missing_env_var(var: &str) -> errors::EnvNotDefinedHelp {
     if var.starts_with("CARGO_")
         || var.starts_with("DEP_")
         || matches!(var, "OUT_DIR" | "OPT_LEVEL" | "PROFILE" | "HOST" | "TARGET")
     {
-        format!(
-            "Cargo sets build script variables at run time. Use `std::env::var(\"{var}\")` instead"
-        )
+        errors::EnvNotDefinedHelp::CargoVar
     } else {
-        format!("Use `std::env::var(\"{var}\")` to read the variable at run time")
+        errors::EnvNotDefinedHelp::Other
     }
 }
diff --git a/compiler/rustc_builtin_macros/src/errors.rs b/compiler/rustc_builtin_macros/src/errors.rs
new file mode 100644
index 00000000000..630f9b87bc3
--- /dev/null
+++ b/compiler/rustc_builtin_macros/src/errors.rs
@@ -0,0 +1,553 @@
+use rustc_errors::{
+    AddToDiagnostic, EmissionGuarantee, IntoDiagnostic, MultiSpan, SingleLabelManySpans,
+};
+use rustc_macros::{Diagnostic, Subdiagnostic};
+use rustc_span::{symbol::Ident, Span, Symbol};
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_requires_cfg_pattern)]
+pub(crate) struct RequiresCfgPattern {
+    #[primary_span]
+    #[label]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_expected_one_cfg_pattern)]
+pub(crate) struct OneCfgPattern {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_alloc_error_must_be_fn)]
+pub(crate) struct AllocErrorMustBeFn {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_assert_requires_boolean)]
+pub(crate) struct AssertRequiresBoolean {
+    #[primary_span]
+    #[label]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_assert_requires_expression)]
+pub(crate) struct AssertRequiresExpression {
+    #[primary_span]
+    pub(crate) span: Span,
+    #[suggestion(code = "", applicability = "maybe-incorrect")]
+    pub(crate) token: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_assert_missing_comma)]
+pub(crate) struct AssertMissingComma {
+    #[primary_span]
+    pub(crate) span: Span,
+    #[suggestion(code = ", ", applicability = "maybe-incorrect", style = "short")]
+    pub(crate) comma: Span,
+}
+
+#[derive(Diagnostic)]
+pub(crate) enum CfgAccessibleInvalid {
+    #[diag(builtin_macros_cfg_accessible_unspecified_path)]
+    UnspecifiedPath(#[primary_span] Span),
+    #[diag(builtin_macros_cfg_accessible_multiple_paths)]
+    MultiplePaths(#[primary_span] Span),
+    #[diag(builtin_macros_cfg_accessible_literal_path)]
+    LiteralPath(#[primary_span] Span),
+    #[diag(builtin_macros_cfg_accessible_has_args)]
+    HasArguments(#[primary_span] Span),
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_cfg_accessible_indeterminate)]
+pub(crate) struct CfgAccessibleIndeterminate {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_missing_literal)]
+#[note]
+pub(crate) struct ConcatMissingLiteral {
+    #[primary_span]
+    pub(crate) spans: Vec<Span>,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_bytestr)]
+pub(crate) struct ConcatBytestr {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_bytes_invalid)]
+pub(crate) struct ConcatBytesInvalid {
+    #[primary_span]
+    pub(crate) span: Span,
+    pub(crate) lit_kind: &'static str,
+    #[subdiagnostic]
+    pub(crate) sugg: Option<ConcatBytesInvalidSuggestion>,
+}
+
+#[derive(Subdiagnostic)]
+pub(crate) enum ConcatBytesInvalidSuggestion {
+    #[suggestion(
+        builtin_macros_byte_char,
+        code = "b{snippet}",
+        applicability = "machine-applicable"
+    )]
+    CharLit {
+        #[primary_span]
+        span: Span,
+        snippet: String,
+    },
+    #[suggestion(
+        builtin_macros_byte_str,
+        code = "b{snippet}",
+        applicability = "machine-applicable"
+    )]
+    StrLit {
+        #[primary_span]
+        span: Span,
+        snippet: String,
+    },
+    #[suggestion(
+        builtin_macros_number_array,
+        code = "[{snippet}]",
+        applicability = "machine-applicable"
+    )]
+    IntLit {
+        #[primary_span]
+        span: Span,
+        snippet: String,
+    },
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_bytes_oob)]
+pub(crate) struct ConcatBytesOob {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_bytes_non_u8)]
+pub(crate) struct ConcatBytesNonU8 {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_bytes_missing_literal)]
+#[note]
+pub(crate) struct ConcatBytesMissingLiteral {
+    #[primary_span]
+    pub(crate) spans: Vec<Span>,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_bytes_array)]
+pub(crate) struct ConcatBytesArray {
+    #[primary_span]
+    pub(crate) span: Span,
+    #[note]
+    #[help]
+    pub(crate) bytestr: bool,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_bytes_bad_repeat)]
+pub(crate) struct ConcatBytesBadRepeat {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_idents_missing_args)]
+pub(crate) struct ConcatIdentsMissingArgs {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_idents_missing_comma)]
+pub(crate) struct ConcatIdentsMissingComma {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_concat_idents_ident_args)]
+pub(crate) struct ConcatIdentsIdentArgs {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_bad_derive_target, code = "E0774")]
+pub(crate) struct BadDeriveTarget {
+    #[primary_span]
+    #[label]
+    pub(crate) span: Span,
+    #[label(builtin_macros_label2)]
+    pub(crate) item: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_unexpected_lit, code = "E0777")]
+pub(crate) struct BadDeriveLit {
+    #[primary_span]
+    #[label]
+    pub(crate) span: Span,
+    #[subdiagnostic]
+    pub help: BadDeriveLitHelp,
+}
+
+#[derive(Subdiagnostic)]
+pub(crate) enum BadDeriveLitHelp {
+    #[help(builtin_macros_str_lit)]
+    StrLit { sym: Symbol },
+    #[help(builtin_macros_other)]
+    Other,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_derive_path_args_list)]
+pub(crate) struct DerivePathArgsList {
+    #[suggestion(code = "", applicability = "machine-applicable")]
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_derive_path_args_value)]
+pub(crate) struct DerivePathArgsValue {
+    #[suggestion(code = "", applicability = "machine-applicable")]
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_no_default_variant)]
+#[help]
+pub(crate) struct NoDefaultVariant {
+    #[primary_span]
+    pub(crate) span: Span,
+    #[subdiagnostic]
+    pub(crate) suggs: Vec<NoDefaultVariantSugg>,
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(
+    builtin_macros_suggestion,
+    code = "#[default] {ident}",
+    applicability = "maybe-incorrect",
+    style = "tool-only"
+)]
+pub(crate) struct NoDefaultVariantSugg {
+    #[primary_span]
+    pub(crate) span: Span,
+    pub(crate) ident: Ident,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_multiple_defaults)]
+#[note]
+pub(crate) struct MultipleDefaults {
+    #[primary_span]
+    pub(crate) span: Span,
+    #[label]
+    pub(crate) first: Span,
+    #[label(builtin_macros_additional)]
+    pub additional: Vec<Span>,
+    #[subdiagnostic]
+    pub suggs: Vec<MultipleDefaultsSugg>,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(
+    builtin_macros_suggestion,
+    applicability = "maybe-incorrect",
+    style = "tool-only"
+)]
+pub(crate) struct MultipleDefaultsSugg {
+    #[suggestion_part(code = "")]
+    pub(crate) spans: Vec<Span>,
+    pub(crate) ident: Ident,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_non_unit_default)]
+#[help]
+pub(crate) struct NonUnitDefault {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_non_exhaustive_default)]
+#[help]
+pub(crate) struct NonExhaustiveDefault {
+    #[primary_span]
+    pub(crate) span: Span,
+    #[label]
+    pub(crate) non_exhaustive: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_multiple_default_attrs)]
+#[note]
+pub(crate) struct MultipleDefaultAttrs {
+    #[primary_span]
+    pub(crate) span: Span,
+    #[label]
+    pub(crate) first: Span,
+    #[label(builtin_macros_label_again)]
+    pub(crate) first_rest: Span,
+    #[help]
+    pub(crate) rest: MultiSpan,
+    pub(crate) only_one: bool,
+    #[subdiagnostic]
+    pub(crate) sugg: MultipleDefaultAttrsSugg,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(
+    builtin_macros_help,
+    applicability = "machine-applicable",
+    style = "tool-only"
+)]
+pub(crate) struct MultipleDefaultAttrsSugg {
+    #[suggestion_part(code = "")]
+    pub(crate) spans: Vec<Span>,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_default_arg)]
+pub(crate) struct DefaultHasArg {
+    #[primary_span]
+    #[suggestion(code = "#[default]", style = "hidden", applicability = "maybe-incorrect")]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_derive_macro_call)]
+pub(crate) struct DeriveMacroCall {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_cannot_derive_union)]
+pub(crate) struct DeriveUnion {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_env_takes_args)]
+pub(crate) struct EnvTakesArgs {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+//#[derive(Diagnostic)]
+//#[diag(builtin_macros_env_not_defined)]
+pub(crate) struct EnvNotDefined {
+    pub(crate) span: Span,
+    pub(crate) msg: Option<Symbol>,
+    pub(crate) var: Symbol,
+    pub(crate) help: Option<EnvNotDefinedHelp>,
+}
+
+// Hand-written implementation to support custom user messages
+impl<'a, G: EmissionGuarantee> IntoDiagnostic<'a, G> for EnvNotDefined {
+    #[track_caller]
+    fn into_diagnostic(
+        self,
+        handler: &'a rustc_errors::Handler,
+    ) -> rustc_errors::DiagnosticBuilder<'a, G> {
+        let mut diag = if let Some(msg) = self.msg {
+            handler.struct_diagnostic(msg.as_str())
+        } else {
+            handler.struct_diagnostic(crate::fluent_generated::builtin_macros_env_not_defined)
+        };
+        diag.set_arg("var", self.var);
+        diag.set_span(self.span);
+        if let Some(help) = self.help {
+            diag.subdiagnostic(help);
+        }
+        diag
+    }
+}
+
+#[derive(Subdiagnostic)]
+pub(crate) enum EnvNotDefinedHelp {
+    #[help(builtin_macros_cargo)]
+    CargoVar,
+    #[help(builtin_macros_other)]
+    Other,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_requires_string)]
+pub(crate) struct FormatRequiresString {
+    #[primary_span]
+    pub(crate) span: Span,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_duplicate_arg)]
+pub(crate) struct FormatDuplicateArg {
+    #[primary_span]
+    pub(crate) span: Span,
+    #[label(builtin_macros_label1)]
+    pub(crate) prev: Span,
+    #[label(builtin_macros_label2)]
+    pub(crate) duplicate: Span,
+    pub(crate) ident: Ident,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_positional_after_named)]
+pub(crate) struct PositionalAfterNamed {
+    #[primary_span]
+    #[label]
+    pub(crate) span: Span,
+    #[label(builtin_macros_named_args)]
+    pub(crate) args: Vec<Span>,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_string_invalid)]
+pub(crate) struct InvalidFormatString {
+    #[primary_span]
+    #[label]
+    pub(crate) span: Span,
+    pub(crate) desc: String,
+    pub(crate) label1: String,
+    #[subdiagnostic]
+    pub(crate) note_: Option<InvalidFormatStringNote>,
+    #[subdiagnostic]
+    pub(crate) label_: Option<InvalidFormatStringLabel>,
+    #[subdiagnostic]
+    pub(crate) sugg_: Option<InvalidFormatStringSuggestion>,
+}
+
+#[derive(Subdiagnostic)]
+#[note(builtin_macros_note)]
+pub(crate) struct InvalidFormatStringNote {
+    pub(crate) note: String,
+}
+
+#[derive(Subdiagnostic)]
+#[label(builtin_macros_second_label)]
+pub(crate) struct InvalidFormatStringLabel {
+    #[primary_span]
+    pub(crate) span: Span,
+    pub(crate) label: String,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(
+    builtin_macros_sugg,
+    style = "verbose",
+    applicability = "machine-applicable"
+)]
+pub(crate) struct InvalidFormatStringSuggestion {
+    #[suggestion_part(code = "{len}")]
+    pub(crate) captured: Span,
+    pub(crate) len: String,
+    #[suggestion_part(code = ", {arg}")]
+    pub(crate) span: Span,
+    pub(crate) arg: String,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_no_arg_named)]
+#[note]
+#[note(builtin_macros_note2)]
+pub(crate) struct FormatNoArgNamed {
+    #[primary_span]
+    pub(crate) span: Span,
+    pub(crate) name: Symbol,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_unknown_trait)]
+#[note]
+pub(crate) struct FormatUnknownTrait<'a> {
+    #[primary_span]
+    pub(crate) span: Span,
+    pub(crate) ty: &'a str,
+    #[subdiagnostic]
+    pub(crate) suggs: Vec<FormatUnknownTraitSugg>,
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(
+    builtin_macros_suggestion,
+    code = "{fmt}",
+    style = "tool-only",
+    applicability = "maybe-incorrect"
+)]
+pub struct FormatUnknownTraitSugg {
+    #[primary_span]
+    pub span: Span,
+    pub fmt: &'static str,
+    pub trait_name: &'static str,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_unused_arg)]
+pub(crate) struct FormatUnusedArg {
+    #[primary_span]
+    #[label(builtin_macros_format_unused_arg)]
+    pub(crate) span: Span,
+    pub(crate) named: bool,
+}
+
+// Allow the singular form to be a subdiagnostic of the multiple-unused
+// form of diagnostic.
+impl AddToDiagnostic for FormatUnusedArg {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, f: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        diag.set_arg("named", self.named);
+        let msg = f(diag, crate::fluent_generated::builtin_macros_format_unused_arg.into());
+        diag.span_label(self.span, msg);
+    }
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_unused_args)]
+pub(crate) struct FormatUnusedArgs {
+    #[primary_span]
+    pub(crate) unused: Vec<Span>,
+    #[label]
+    pub(crate) fmt: Span,
+    #[subdiagnostic]
+    pub(crate) unused_labels: Vec<FormatUnusedArg>,
+}
+
+#[derive(Diagnostic)]
+#[diag(builtin_macros_format_pos_mismatch)]
+pub(crate) struct FormatPositionalMismatch {
+    #[primary_span]
+    pub(crate) span: MultiSpan,
+    pub(crate) n: usize,
+    pub(crate) desc: String,
+    #[subdiagnostic]
+    pub(crate) highlight: SingleLabelManySpans,
+}
diff --git a/compiler/rustc_builtin_macros/src/format.rs b/compiler/rustc_builtin_macros/src/format.rs
index db2ef7fba4b..435a07d8ce7 100644
--- a/compiler/rustc_builtin_macros/src/format.rs
+++ b/compiler/rustc_builtin_macros/src/format.rs
@@ -7,7 +7,7 @@ use rustc_ast::{
     FormatDebugHex, FormatOptions, FormatPlaceholder, FormatSign, FormatTrait,
 };
 use rustc_data_structures::fx::FxHashSet;
-use rustc_errors::{pluralize, Applicability, MultiSpan, PResult};
+use rustc_errors::{Applicability, MultiSpan, PResult, SingleLabelManySpans};
 use rustc_expand::base::{self, *};
 use rustc_parse_format as parse;
 use rustc_span::symbol::{Ident, Symbol};
@@ -36,6 +36,8 @@ enum PositionUsedAs {
 }
 use PositionUsedAs::*;
 
+use crate::errors;
+
 struct MacroInput {
     fmtstr: P<Expr>,
     args: FormatArguments,
@@ -66,7 +68,7 @@ fn parse_args<'a>(ecx: &mut ExtCtxt<'a>, sp: Span, tts: TokenStream) -> PResult<
     let mut p = ecx.new_parser_from_tts(tts);
 
     if p.token == token::Eof {
-        return Err(ecx.struct_span_err(sp, "requires at least a format string argument"));
+        return Err(ecx.create_err(errors::FormatRequiresString { span: sp }));
     }
 
     let first_token = &p.token;
@@ -121,13 +123,12 @@ fn parse_args<'a>(ecx: &mut ExtCtxt<'a>, sp: Span, tts: TokenStream) -> PResult<
                 p.expect(&token::Eq)?;
                 let expr = p.parse_expr()?;
                 if let Some((_, prev)) = args.by_name(ident.name) {
-                    ecx.struct_span_err(
-                        ident.span,
-                        &format!("duplicate argument named `{}`", ident),
-                    )
-                    .span_label(prev.kind.ident().unwrap().span, "previously here")
-                    .span_label(ident.span, "duplicate argument")
-                    .emit();
+                    ecx.emit_err(errors::FormatDuplicateArg {
+                        span: ident.span,
+                        prev: prev.kind.ident().unwrap().span,
+                        duplicate: ident.span,
+                        ident,
+                    });
                     continue;
                 }
                 args.add(FormatArgument { kind: FormatArgumentKind::Named(ident), expr });
@@ -135,20 +136,21 @@ fn parse_args<'a>(ecx: &mut ExtCtxt<'a>, sp: Span, tts: TokenStream) -> PResult<
             _ => {
                 let expr = p.parse_expr()?;
                 if !args.named_args().is_empty() {
-                    let mut err = ecx.struct_span_err(
-                        expr.span,
-                        "positional arguments cannot follow named arguments",
-                    );
-                    err.span_label(
-                        expr.span,
-                        "positional arguments must be before named arguments",
-                    );
-                    for arg in args.named_args() {
-                        if let Some(name) = arg.kind.ident() {
-                            err.span_label(name.span.to(arg.expr.span), "named argument");
-                        }
-                    }
-                    err.emit();
+                    ecx.emit_err(errors::PositionalAfterNamed {
+                        span: expr.span,
+                        args: args
+                            .named_args()
+                            .iter()
+                            .filter_map(|a| {
+                                if let Some(ident) = a.kind.ident() {
+                                    Some((a, ident))
+                                } else {
+                                    None
+                                }
+                            })
+                            .map(|(arg, n)| n.span.to(arg.expr.span))
+                            .collect(),
+                    });
                 }
                 args.add(FormatArgument { kind: FormatArgumentKind::Normal, expr });
             }
@@ -234,13 +236,19 @@ fn make_format_args(
             // argument span here.
             fmt_span
         };
-        let mut e = ecx.struct_span_err(sp, &format!("invalid format string: {}", err.description));
-        e.span_label(sp, err.label + " in format string");
+        let mut e = errors::InvalidFormatString {
+            span: sp,
+            note_: None,
+            label_: None,
+            sugg_: None,
+            desc: err.description,
+            label1: err.label,
+        };
         if let Some(note) = err.note {
-            e.note(&note);
+            e.note_ = Some(errors::InvalidFormatStringNote { note });
         }
         if let Some((label, span)) = err.secondary_label && is_source_literal {
-            e.span_label(fmt_span.from_inner(InnerSpan::new(span.start, span.end)), label);
+            e.label_ = Some(errors::InvalidFormatStringLabel { span: fmt_span.from_inner(InnerSpan::new(span.start, span.end)), label } );
         }
         if err.should_be_replaced_with_positional_argument {
             let captured_arg_span =
@@ -250,17 +258,15 @@ fn make_format_args(
                     Some(arg) => arg.expr.span,
                     None => fmt_span,
                 };
-                e.multipart_suggestion_verbose(
-                    "consider using a positional formatting argument instead",
-                    vec![
-                        (captured_arg_span, args.unnamed_args().len().to_string()),
-                        (span.shrink_to_hi(), format!(", {}", arg)),
-                    ],
-                    Applicability::MachineApplicable,
-                );
+                e.sugg_ = Some(errors::InvalidFormatStringSuggestion {
+                    captured: captured_arg_span,
+                    len: args.unnamed_args().len().to_string(),
+                    span: span.shrink_to_hi(),
+                    arg,
+                });
             }
         }
-        e.emit();
+        ecx.emit_err(e);
         return Err(());
     }
 
@@ -318,10 +324,7 @@ fn make_format_args(
                     } else {
                         // For the moment capturing variables from format strings expanded from macros is
                         // disabled (see RFC #2795)
-                        ecx.struct_span_err(span, &format!("there is no argument named `{name}`"))
-                            .note(format!("did you intend to capture a variable `{name}` from the surrounding scope?"))
-                            .note("to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro")
-                            .emit();
+                        ecx.emit_err(errors::FormatNoArgNamed { span, name });
                         DummyResult::raw_expr(span, true)
                     };
                     Ok(args.add(FormatArgument { kind: FormatArgumentKind::Captured(ident), expr }))
@@ -475,12 +478,8 @@ fn make_format_args(
         .enumerate()
         .filter(|&(_, used)| !used)
         .map(|(i, _)| {
-            let msg = if let FormatArgumentKind::Named(_) = args.explicit_args()[i].kind {
-                "named argument never used"
-            } else {
-                "argument never used"
-            };
-            (args.explicit_args()[i].expr.span, msg)
+            let named = matches!(args.explicit_args()[i].kind, FormatArgumentKind::Named(_));
+            (args.explicit_args()[i].expr.span, named)
         })
         .collect::<Vec<_>>();
 
@@ -531,22 +530,8 @@ fn invalid_placeholder_type_error(
     fmt_span: Span,
 ) {
     let sp = ty_span.map(|sp| fmt_span.from_inner(InnerSpan::new(sp.start, sp.end)));
-    let mut err =
-        ecx.struct_span_err(sp.unwrap_or(fmt_span), &format!("unknown format trait `{}`", ty));
-    err.note(
-        "the only appropriate formatting traits are:\n\
-                                - ``, which uses the `Display` trait\n\
-                                - `?`, which uses the `Debug` trait\n\
-                                - `e`, which uses the `LowerExp` trait\n\
-                                - `E`, which uses the `UpperExp` trait\n\
-                                - `o`, which uses the `Octal` trait\n\
-                                - `p`, which uses the `Pointer` trait\n\
-                                - `b`, which uses the `Binary` trait\n\
-                                - `x`, which uses the `LowerHex` trait\n\
-                                - `X`, which uses the `UpperHex` trait",
-    );
-    if let Some(sp) = sp {
-        for (fmt, name) in &[
+    let suggs = if let Some(sp) = sp {
+        [
             ("", "Display"),
             ("?", "Debug"),
             ("e", "LowerExp"),
@@ -556,40 +541,38 @@ fn invalid_placeholder_type_error(
             ("b", "Binary"),
             ("x", "LowerHex"),
             ("X", "UpperHex"),
-        ] {
-            err.tool_only_span_suggestion(
-                sp,
-                &format!("use the `{}` trait", name),
-                *fmt,
-                Applicability::MaybeIncorrect,
-            );
-        }
-    }
-    err.emit();
+        ]
+        .into_iter()
+        .map(|(fmt, trait_name)| errors::FormatUnknownTraitSugg { span: sp, fmt, trait_name })
+        .collect()
+    } else {
+        vec![]
+    };
+    ecx.emit_err(errors::FormatUnknownTrait { span: sp.unwrap_or(fmt_span), ty, suggs });
 }
 
 fn report_missing_placeholders(
     ecx: &mut ExtCtxt<'_>,
-    unused: Vec<(Span, &str)>,
+    unused: Vec<(Span, bool)>,
     detect_foreign_fmt: bool,
     str_style: Option<usize>,
     fmt_str: &str,
     fmt_span: Span,
 ) {
-    let mut diag = if let &[(span, msg)] = &unused[..] {
-        let mut diag = ecx.struct_span_err(span, msg);
-        diag.span_label(span, msg);
-        diag
+    let mut diag = if let &[(span, named)] = &unused[..] {
+        //let mut diag = ecx.struct_span_err(span, msg);
+        //diag.span_label(span, msg);
+        //diag
+        ecx.create_err(errors::FormatUnusedArg { span, named })
     } else {
-        let mut diag = ecx.struct_span_err(
-            unused.iter().map(|&(sp, _)| sp).collect::<Vec<Span>>(),
-            "multiple unused formatting arguments",
-        );
-        diag.span_label(fmt_span, "multiple missing formatting specifiers");
-        for &(span, msg) in &unused {
-            diag.span_label(span, msg);
-        }
-        diag
+        let unused_labels =
+            unused.iter().map(|&(span, named)| errors::FormatUnusedArg { span, named }).collect();
+        let unused_spans = unused.iter().map(|&(span, _)| span).collect();
+        ecx.create_err(errors::FormatUnusedArgs {
+            fmt: fmt_span,
+            unused: unused_spans,
+            unused_labels,
+        })
     };
 
     // Used to ensure we only report translations for *one* kind of foreign format.
@@ -768,18 +751,16 @@ fn report_invalid_references(
         } else {
             MultiSpan::from_spans(spans)
         };
-        e = ecx.struct_span_err(
+        e = ecx.create_err(errors::FormatPositionalMismatch {
             span,
-            &format!(
-                "{} positional argument{} in format string, but {}",
-                num_placeholders,
-                pluralize!(num_placeholders),
-                num_args_desc,
-            ),
-        );
-        for arg in args.explicit_args() {
-            e.span_label(arg.expr.span, "");
-        }
+            n: num_placeholders,
+            desc: num_args_desc,
+            highlight: SingleLabelManySpans {
+                spans: args.explicit_args().iter().map(|arg| arg.expr.span).collect(),
+                label: "",
+                kind: rustc_errors::LabelKind::Label,
+            },
+        });
         // Point out `{:.*}` placeholders: those take an extra argument.
         let mut has_precision_star = false;
         for piece in template {
diff --git a/compiler/rustc_builtin_macros/src/lib.rs b/compiler/rustc_builtin_macros/src/lib.rs
index 7697b592e33..37fbd03a6a2 100644
--- a/compiler/rustc_builtin_macros/src/lib.rs
+++ b/compiler/rustc_builtin_macros/src/lib.rs
@@ -9,6 +9,7 @@
 #![feature(if_let_guard)]
 #![feature(is_sorted)]
 #![feature(let_chains)]
+#![feature(lint_reasons)]
 #![feature(proc_macro_internals)]
 #![feature(proc_macro_quote)]
 #![recursion_limit = "256"]
@@ -39,6 +40,7 @@ mod derive;
 mod deriving;
 mod edition_panic;
 mod env;
+mod errors;
 mod format;
 mod format_foreign;
 mod global_allocator;
diff --git a/compiler/rustc_errors/src/diagnostic.rs b/compiler/rustc_errors/src/diagnostic.rs
index 9ed8ab67431..cd0049b4558 100644
--- a/compiler/rustc_errors/src/diagnostic.rs
+++ b/compiler/rustc_errors/src/diagnostic.rs
@@ -880,6 +880,7 @@ impl Diagnostic {
     ///
     /// This is intended to be used for suggestions that are *very* obvious in what the changes
     /// need to be from the message, but we still want other tools to be able to apply them.
+    #[rustc_lint_diagnostics]
     pub fn tool_only_span_suggestion(
         &mut self,
         sp: Span,
diff --git a/compiler/rustc_errors/src/diagnostic_impls.rs b/compiler/rustc_errors/src/diagnostic_impls.rs
index e82bad67b21..65f8a61a30a 100644
--- a/compiler/rustc_errors/src/diagnostic_impls.rs
+++ b/compiler/rustc_errors/src/diagnostic_impls.rs
@@ -1,4 +1,4 @@
-use crate::fluent_generated as fluent;
+use crate::{fluent_generated as fluent, AddToDiagnostic};
 use crate::{DiagnosticArgValue, DiagnosticBuilder, Handler, IntoDiagnostic, IntoDiagnosticArg};
 use rustc_ast as ast;
 use rustc_ast_pretty::pprust;
@@ -6,6 +6,7 @@ use rustc_hir as hir;
 use rustc_lint_defs::Level;
 use rustc_span::edition::Edition;
 use rustc_span::symbol::{Ident, MacroRulesNormalizedIdent, Symbol};
+use rustc_span::Span;
 use rustc_target::abi::TargetDataLayoutErrors;
 use rustc_target::spec::{PanicStrategy, SplitDebuginfo, StackProtector, TargetTriple};
 use rustc_type_ir as type_ir;
@@ -276,3 +277,26 @@ impl IntoDiagnostic<'_, !> for TargetDataLayoutErrors<'_> {
         }
     }
 }
+
+/// Utility struct used to apply a single label while highlighting multiple spans
+pub struct SingleLabelManySpans {
+    pub spans: Vec<Span>,
+    pub label: &'static str,
+    pub kind: LabelKind,
+}
+impl AddToDiagnostic for SingleLabelManySpans {
+    fn add_to_diagnostic_with<F>(self, diag: &mut crate::Diagnostic, _: F) {
+        match self.kind {
+            LabelKind::Note => diag.span_note(self.spans, self.label),
+            LabelKind::Label => diag.span_labels(self.spans, self.label),
+            LabelKind::Help => diag.span_help(self.spans, self.label),
+        };
+    }
+}
+
+/// The kind of label to attach when using [`SingleLabelManySpans`]
+pub enum LabelKind {
+    Note,
+    Label,
+    Help,
+}
diff --git a/compiler/rustc_errors/src/lib.rs b/compiler/rustc_errors/src/lib.rs
index 9866a9bffe0..5b0d8096207 100644
--- a/compiler/rustc_errors/src/lib.rs
+++ b/compiler/rustc_errors/src/lib.rs
@@ -383,7 +383,9 @@ pub use diagnostic::{
     DiagnosticStyledString, IntoDiagnosticArg, SubDiagnostic,
 };
 pub use diagnostic_builder::{DiagnosticBuilder, EmissionGuarantee, Noted};
-pub use diagnostic_impls::{DiagnosticArgFromDisplay, DiagnosticSymbolList};
+pub use diagnostic_impls::{
+    DiagnosticArgFromDisplay, DiagnosticSymbolList, LabelKind, SingleLabelManySpans,
+};
 use std::backtrace::Backtrace;
 
 /// A handler deals with errors and other compiler output.
diff --git a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs
index 9b91627883a..181e65d8953 100644
--- a/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs
+++ b/compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs
@@ -392,14 +392,16 @@ impl<'a> DiagnosticDeriveVariantBuilder<'a> {
             }
             SubdiagnosticKind::Note | SubdiagnosticKind::Help | SubdiagnosticKind::Warn => {
                 let inner = info.ty.inner_type();
-                if type_matches_path(inner, &["rustc_span", "Span"]) {
+                if type_matches_path(inner, &["rustc_span", "Span"])
+                    || type_matches_path(inner, &["rustc_span", "MultiSpan"])
+                {
                     Ok(self.add_spanned_subdiagnostic(binding, &fn_ident, slug))
                 } else if type_is_unit(inner)
                     || (matches!(info.ty, FieldInnerTy::Plain(_)) && type_is_bool(inner))
                 {
                     Ok(self.add_subdiagnostic(&fn_ident, slug))
                 } else {
-                    report_type_error(attr, "`Span`, `bool` or `()`")?
+                    report_type_error(attr, "`Span`, `MultiSpan`, `bool` or `()`")?
                 }
             }
             SubdiagnosticKind::Suggestion {
diff --git a/tests/ui-fulldeps/session-diagnostic/diagnostic-derive.rs b/tests/ui-fulldeps/session-diagnostic/diagnostic-derive.rs
index 61724c11745..6cc7bab3726 100644
--- a/tests/ui-fulldeps/session-diagnostic/diagnostic-derive.rs
+++ b/tests/ui-fulldeps/session-diagnostic/diagnostic-derive.rs
@@ -518,7 +518,7 @@ struct BoolField {
     #[help]
     foo: bool,
     #[help(no_crate_help)]
-    //~^ ERROR the `#[help(...)]` attribute can only be applied to fields of type `Span`, `bool` or `()`
+    //~^ ERROR the `#[help(...)]` attribute can only be applied to fields of type
     // only allow plain 'bool' fields
     bar: Option<bool>,
 }
diff --git a/tests/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr b/tests/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr
index cd14c7496b3..a2f3bb5277b 100644
--- a/tests/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr
+++ b/tests/ui-fulldeps/session-diagnostic/diagnostic-derive.stderr
@@ -323,7 +323,7 @@ error: invalid applicability
 LL |     #[suggestion(no_crate_suggestion, code = "...", applicability = "batman")]
    |                                                                     ^^^^^^^^
 
-error: the `#[help(...)]` attribute can only be applied to fields of type `Span`, `bool` or `()`
+error: the `#[help(...)]` attribute can only be applied to fields of type `Span`, `MultiSpan`, `bool` or `()`
   --> $DIR/diagnostic-derive.rs:520:5
    |
 LL |     #[help(no_crate_help)]
diff --git a/tests/ui/extenv/extenv-no-args.rs b/tests/ui/extenv/extenv-no-args.rs
index 9f221ed10d7..2ff6d242b27 100644
--- a/tests/ui/extenv/extenv-no-args.rs
+++ b/tests/ui/extenv/extenv-no-args.rs
@@ -1 +1 @@
-fn main() { env!(); } //~ ERROR: env! takes 1 or 2 arguments
+fn main() { env!(); } //~ ERROR: `env!()` takes 1 or 2 arguments
diff --git a/tests/ui/extenv/extenv-no-args.stderr b/tests/ui/extenv/extenv-no-args.stderr
index 318ed635be0..70b85932c23 100644
--- a/tests/ui/extenv/extenv-no-args.stderr
+++ b/tests/ui/extenv/extenv-no-args.stderr
@@ -1,4 +1,4 @@
-error: env! takes 1 or 2 arguments
+error: `env!()` takes 1 or 2 arguments
   --> $DIR/extenv-no-args.rs:1:13
    |
 LL | fn main() { env!(); }
diff --git a/tests/ui/extenv/extenv-too-many-args.rs b/tests/ui/extenv/extenv-too-many-args.rs
index 1adbee583db..ffad1c51303 100644
--- a/tests/ui/extenv/extenv-too-many-args.rs
+++ b/tests/ui/extenv/extenv-too-many-args.rs
@@ -1 +1 @@
-fn main() { env!("one", "two", "three"); } //~ ERROR: env! takes 1 or 2 arguments
+fn main() { env!("one", "two", "three"); } //~ ERROR: `env!()` takes 1 or 2 arguments
diff --git a/tests/ui/extenv/extenv-too-many-args.stderr b/tests/ui/extenv/extenv-too-many-args.stderr
index 54150a3328f..47cf810b70d 100644
--- a/tests/ui/extenv/extenv-too-many-args.stderr
+++ b/tests/ui/extenv/extenv-too-many-args.stderr
@@ -1,4 +1,4 @@
-error: env! takes 1 or 2 arguments
+error: `env!()` takes 1 or 2 arguments
   --> $DIR/extenv-too-many-args.rs:1:13
    |
 LL | fn main() { env!("one", "two", "three"); }
diff --git a/tests/ui/extenv/issue-55897.stderr b/tests/ui/extenv/issue-55897.stderr
index 5752a965e35..401db827813 100644
--- a/tests/ui/extenv/issue-55897.stderr
+++ b/tests/ui/extenv/issue-55897.stderr
@@ -4,7 +4,7 @@ error: environment variable `NON_EXISTENT` not defined at compile time
 LL |     include!(concat!(env!("NON_EXISTENT"), "/data.rs"));
    |                      ^^^^^^^^^^^^^^^^^^^^
    |
-   = help: Use `std::env::var("NON_EXISTENT")` to read the variable at run time
+   = help: use `std::env::var("NON_EXISTENT")` to read the variable at run time
    = note: this error originates in the macro `env` (in Nightly builds, run with -Z macro-backtrace for more info)
 
 error: suffixes on string literals are invalid
diff --git a/tests/ui/issues/issue-50403.rs b/tests/ui/issues/issue-50403.rs
index 012057fc280..ab22aff26d9 100644
--- a/tests/ui/issues/issue-50403.rs
+++ b/tests/ui/issues/issue-50403.rs
@@ -1,5 +1,5 @@
 #![feature(concat_idents)]
 
 fn main() {
-    let x = concat_idents!(); //~ ERROR concat_idents! takes 1 or more arguments
+    let x = concat_idents!(); //~ ERROR `concat_idents!()` takes 1 or more arguments
 }
diff --git a/tests/ui/issues/issue-50403.stderr b/tests/ui/issues/issue-50403.stderr
index a3a2ed044db..d50befa5e32 100644
--- a/tests/ui/issues/issue-50403.stderr
+++ b/tests/ui/issues/issue-50403.stderr
@@ -1,4 +1,4 @@
-error: concat_idents! takes 1 or more arguments
+error: `concat_idents!()` takes 1 or more arguments
   --> $DIR/issue-50403.rs:4:13
    |
 LL |     let x = concat_idents!();
diff --git a/tests/ui/macros/concat-bytes-error.stderr b/tests/ui/macros/concat-bytes-error.stderr
index d6cd1a3d178..3f2c64922e3 100644
--- a/tests/ui/macros/concat-bytes-error.stderr
+++ b/tests/ui/macros/concat-bytes-error.stderr
@@ -4,7 +4,7 @@ error: expected a byte literal
 LL |     concat_bytes!(pie);
    |                   ^^^
    |
-   = note: only byte literals (like `b"foo"`, `b's'`, and `[3, 4, 5]`) can be passed to `concat_bytes!()`
+   = note: only byte literals (like `b"foo"`, `b's'` and `[3, 4, 5]`) can be passed to `concat_bytes!()`
 
 error: expected a byte literal
   --> $DIR/concat-bytes-error.rs:5:19
@@ -12,7 +12,7 @@ error: expected a byte literal
 LL |     concat_bytes!(pie, pie);
    |                   ^^^  ^^^
    |
-   = note: only byte literals (like `b"foo"`, `b's'`, and `[3, 4, 5]`) can be passed to `concat_bytes!()`
+   = note: only byte literals (like `b"foo"`, `b's'` and `[3, 4, 5]`) can be passed to `concat_bytes!()`
 
 error: cannot concatenate string literals
   --> $DIR/concat-bytes-error.rs:6:19
@@ -98,7 +98,7 @@ error: expected a byte literal
 LL |         -33,
    |         ^^^
    |
-   = note: only byte literals (like `b"foo"`, `b's'`, and `[3, 4, 5]`) can be passed to `concat_bytes!()`
+   = note: only byte literals (like `b"foo"`, `b's'` and `[3, 4, 5]`) can be passed to `concat_bytes!()`
 
 error: cannot concatenate doubly nested array
   --> $DIR/concat-bytes-error.rs:35:9
@@ -151,7 +151,7 @@ error: expected a byte literal
 LL |     concat_bytes!([pie; 2]);
    |                    ^^^
    |
-   = note: only byte literals (like `b"foo"`, `b's'`, and `[3, 4, 5]`) can be passed to `concat_bytes!()`
+   = note: only byte literals (like `b"foo"`, `b's'` and `[3, 4, 5]`) can be passed to `concat_bytes!()`
 
 error: cannot concatenate float literals
   --> $DIR/concat-bytes-error.rs:46:20
diff --git a/tests/ui/macros/macros-nonfatal-errors.stderr b/tests/ui/macros/macros-nonfatal-errors.stderr
index 93fbc9c8a44..ca373ea6cd9 100644
--- a/tests/ui/macros/macros-nonfatal-errors.stderr
+++ b/tests/ui/macros/macros-nonfatal-errors.stderr
@@ -3,36 +3,48 @@ error: the `#[default]` attribute may only be used on unit enum variants
    |
 LL |     #[default]
    |     ^^^^^^^^^^
+   |
+   = help: consider a manual implementation of `Default`
 
 error: the `#[default]` attribute may only be used on unit enum variants
   --> $DIR/macros-nonfatal-errors.rs:18:36
    |
 LL | struct DefaultInnerAttrTupleStruct(#[default] ());
    |                                    ^^^^^^^^^^
+   |
+   = help: consider a manual implementation of `Default`
 
 error: the `#[default]` attribute may only be used on unit enum variants
   --> $DIR/macros-nonfatal-errors.rs:22:1
    |
 LL | #[default]
    | ^^^^^^^^^^
+   |
+   = help: consider a manual implementation of `Default`
 
 error: the `#[default]` attribute may only be used on unit enum variants
   --> $DIR/macros-nonfatal-errors.rs:26:1
    |
 LL | #[default]
    | ^^^^^^^^^^
+   |
+   = help: consider a manual implementation of `Default`
 
 error: the `#[default]` attribute may only be used on unit enum variants
   --> $DIR/macros-nonfatal-errors.rs:36:11
    |
 LL |     Foo = #[default] 0,
    |           ^^^^^^^^^^
+   |
+   = help: consider a manual implementation of `Default`
 
 error: the `#[default]` attribute may only be used on unit enum variants
   --> $DIR/macros-nonfatal-errors.rs:37:14
    |
 LL |     Bar([u8; #[default] 1]),
    |              ^^^^^^^^^^
+   |
+   = help: consider a manual implementation of `Default`
 
 error: no default declared
   --> $DIR/macros-nonfatal-errors.rs:42:10
@@ -132,7 +144,7 @@ error: asm template must be a string literal
 LL |     asm!(invalid);
    |          ^^^^^^^
 
-error: concat_idents! requires ident args
+error: `concat_idents!()` requires ident args
   --> $DIR/macros-nonfatal-errors.rs:101:5
    |
 LL |     concat_idents!("not", "idents");
@@ -150,7 +162,7 @@ error: expected string literal
 LL |     env!(invalid);
    |          ^^^^^^^
 
-error: env! takes 1 or 2 arguments
+error: `env!()` takes 1 or 2 arguments
   --> $DIR/macros-nonfatal-errors.rs:105:5
    |
 LL |     env!(foo, abr, baz);
@@ -162,7 +174,7 @@ error: environment variable `RUST_HOPEFULLY_THIS_DOESNT_EXIST` not defined at co
 LL |     env!("RUST_HOPEFULLY_THIS_DOESNT_EXIST");
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
-   = help: Use `std::env::var("RUST_HOPEFULLY_THIS_DOESNT_EXIST")` to read the variable at run time
+   = help: use `std::env::var("RUST_HOPEFULLY_THIS_DOESNT_EXIST")` to read the variable at run time
    = note: this error originates in the macro `env` (in Nightly builds, run with -Z macro-backtrace for more info)
 
 error: format argument must be a string literal