about summary refs log tree commit diff
path: root/compiler/rustc_lint/src
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/rustc_lint/src')
-rw-r--r--compiler/rustc_lint/src/array_into_iter.rs50
-rw-r--r--compiler/rustc_lint/src/builtin.rs727
-rw-r--r--compiler/rustc_lint/src/context.rs3
-rw-r--r--compiler/rustc_lint/src/deref_into_dyn_supertrait.rs30
-rw-r--r--compiler/rustc_lint/src/early.rs1
-rw-r--r--compiler/rustc_lint/src/enum_intrinsics_non_enums.rs17
-rw-r--r--compiler/rustc_lint/src/errors.rs5
-rw-r--r--compiler/rustc_lint/src/expect.rs40
-rw-r--r--compiler/rustc_lint/src/for_loops_over_fallibles.rs73
-rw-r--r--compiler/rustc_lint/src/hidden_unicode_codepoints.rs67
-rw-r--r--compiler/rustc_lint/src/internal.rs109
-rw-r--r--compiler/rustc_lint/src/let_underscore.rs47
-rw-r--r--compiler/rustc_lint/src/levels.rs187
-rw-r--r--compiler/rustc_lint/src/lib.rs3
-rw-r--r--compiler/rustc_lint/src/lints.rs1479
-rw-r--r--compiler/rustc_lint/src/methods.rs12
-rw-r--r--compiler/rustc_lint/src/non_ascii_idents.rs56
-rw-r--r--compiler/rustc_lint/src/non_fmt_panic.rs47
-rw-r--r--compiler/rustc_lint/src/nonstandard_style.rs129
-rw-r--r--compiler/rustc_lint/src/noop_method_call.rs13
-rw-r--r--compiler/rustc_lint/src/pass_by_value.rs17
-rw-r--r--compiler/rustc_lint/src/redundant_semicolon.rs15
-rw-r--r--compiler/rustc_lint/src/traits.rs25
-rw-r--r--compiler/rustc_lint/src/types.rs333
-rw-r--r--compiler/rustc_lint/src/unused.rs169
25 files changed, 2259 insertions, 1395 deletions
diff --git a/compiler/rustc_lint/src/array_into_iter.rs b/compiler/rustc_lint/src/array_into_iter.rs
index abebc533cc1..3593f141df6 100644
--- a/compiler/rustc_lint/src/array_into_iter.rs
+++ b/compiler/rustc_lint/src/array_into_iter.rs
@@ -1,5 +1,5 @@
+use crate::lints::{ArrayIntoIterDiag, ArrayIntoIterDiagSub};
 use crate::{LateContext, LateLintPass, LintContext};
-use rustc_errors::{fluent, Applicability};
 use rustc_hir as hir;
 use rustc_middle::ty;
 use rustc_middle::ty::adjustment::{Adjust, Adjustment};
@@ -118,41 +118,23 @@ impl<'tcx> LateLintPass<'tcx> for ArrayIntoIter {
                 // to an array or to a slice.
                 _ => bug!("array type coerced to something other than array or slice"),
             };
-            cx.struct_span_lint(
+            let sub = if self.for_expr_span == expr.span {
+                Some(ArrayIntoIterDiagSub::RemoveIntoIter {
+                    span: receiver_arg.span.shrink_to_hi().to(expr.span.shrink_to_hi()),
+                })
+            } else if receiver_ty.is_array() {
+                Some(ArrayIntoIterDiagSub::UseExplicitIntoIter {
+                    start_span: expr.span.shrink_to_lo(),
+                    end_span: receiver_arg.span.shrink_to_hi().to(expr.span.shrink_to_hi()),
+                })
+            } else {
+                None
+            };
+            cx.emit_spanned_lint(
                 ARRAY_INTO_ITER,
                 call.ident.span,
-                fluent::lint_array_into_iter,
-                |diag| {
-                    diag.set_arg("target", target);
-                    diag.span_suggestion(
-                        call.ident.span,
-                        fluent::use_iter_suggestion,
-                        "iter",
-                        Applicability::MachineApplicable,
-                    );
-                    if self.for_expr_span == expr.span {
-                        diag.span_suggestion(
-                            receiver_arg.span.shrink_to_hi().to(expr.span.shrink_to_hi()),
-                            fluent::remove_into_iter_suggestion,
-                            "",
-                            Applicability::MaybeIncorrect,
-                        );
-                    } else if receiver_ty.is_array() {
-                        diag.multipart_suggestion(
-                            fluent::use_explicit_into_iter_suggestion,
-                            vec![
-                                (expr.span.shrink_to_lo(), "IntoIterator::into_iter(".into()),
-                                (
-                                    receiver_arg.span.shrink_to_hi().to(expr.span.shrink_to_hi()),
-                                    ")".into(),
-                                ),
-                            ],
-                            Applicability::MaybeIncorrect,
-                        );
-                    }
-                    diag
-                },
-            )
+                ArrayIntoIterDiag { target, suggestion: call.ident.span, sub },
+            );
         }
     }
 }
diff --git a/compiler/rustc_lint/src/builtin.rs b/compiler/rustc_lint/src/builtin.rs
index d58168ff377..6f445426df7 100644
--- a/compiler/rustc_lint/src/builtin.rs
+++ b/compiler/rustc_lint/src/builtin.rs
@@ -22,6 +22,23 @@
 
 use crate::{
     errors::BuiltinEllpisisInclusiveRangePatterns,
+    lints::{
+        BuiltinAnonymousParams, BuiltinBoxPointers, BuiltinClashingExtern,
+        BuiltinClashingExternSub, BuiltinConstNoMangle, BuiltinDeprecatedAttrLink,
+        BuiltinDeprecatedAttrLinkSuggestion, BuiltinDeprecatedAttrUsed, BuiltinDerefNullptr,
+        BuiltinEllipsisInclusiveRangePatternsLint, BuiltinExplicitOutlives,
+        BuiltinExplicitOutlivesSuggestion, BuiltinIncompleteFeatures,
+        BuiltinIncompleteFeaturesHelp, BuiltinIncompleteFeaturesNote, BuiltinKeywordIdents,
+        BuiltinMissingCopyImpl, BuiltinMissingDebugImpl, BuiltinMissingDoc,
+        BuiltinMutablesTransmutes, BuiltinNoMangleGeneric, BuiltinNonShorthandFieldPatterns,
+        BuiltinSpecialModuleNameUsed, BuiltinTrivialBounds, BuiltinTypeAliasGenericBounds,
+        BuiltinTypeAliasGenericBoundsSuggestion, BuiltinTypeAliasWhereClause,
+        BuiltinUnexpectedCliConfigName, BuiltinUnexpectedCliConfigValue,
+        BuiltinUngatedAsyncFnTrackCaller, BuiltinUnnameableTestItems, BuiltinUnpermittedTypeInit,
+        BuiltinUnpermittedTypeInitSub, BuiltinUnreachablePub, BuiltinUnsafe,
+        BuiltinUnstableFeatures, BuiltinUnusedDocComment, BuiltinUnusedDocCommentSub,
+        BuiltinWhileTrue, SuggestChangingAssocTypes,
+    },
     types::{transparent_newtype_field, CItemKind},
     EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext,
 };
@@ -33,10 +50,7 @@ use rustc_ast::{self as ast, *};
 use rustc_ast_pretty::pprust::{self, expr_to_string};
 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
 use rustc_data_structures::stack::ensure_sufficient_stack;
-use rustc_errors::{
-    fluent, Applicability, DelayDm, Diagnostic, DiagnosticBuilder, DiagnosticMessage,
-    DiagnosticStyledString, MultiSpan,
-};
+use rustc_errors::{fluent, Applicability, DecorateLint, MultiSpan};
 use rustc_feature::{deprecated_attributes, AttributeGate, BuiltinAttribute, GateIssue, Stability};
 use rustc_hir as hir;
 use rustc_hir::def::{DefKind, Res};
@@ -110,25 +124,17 @@ impl EarlyLintPass for WhileTrue {
             && !cond.span.from_expansion()
         {
             let condition_span = e.span.with_hi(cond.span.hi());
-            cx.struct_span_lint(
-                            WHILE_TRUE,
-                            condition_span,
-                fluent::lint_builtin_while_true,
-                            |lint| {
-                    lint.span_suggestion_short(
-                        condition_span,
-                        fluent::suggestion,
-                        format!(
+            let replace = format!(
                             "{}loop",
                             label.map_or_else(String::new, |label| format!(
                                 "{}: ",
                                 label.ident,
                             ))
-                        ),
-                        Applicability::MachineApplicable,
-                    )
-                },
-            )
+                        );
+            cx.emit_spanned_lint(WHILE_TRUE, condition_span, BuiltinWhileTrue {
+                suggestion: condition_span,
+                replace,
+            });
         }
     }
 }
@@ -164,12 +170,7 @@ impl BoxPointers {
         for leaf in ty.walk() {
             if let GenericArgKind::Type(leaf_ty) = leaf.unpack() {
                 if leaf_ty.is_box() {
-                    cx.struct_span_lint(
-                        BOX_POINTERS,
-                        span,
-                        fluent::lint_builtin_box_pointers,
-                        |lint| lint.set_arg("ty", ty),
-                    );
+                    cx.emit_spanned_lint(BOX_POINTERS, span, BuiltinBoxPointers { ty });
                 }
             }
         }
@@ -267,19 +268,13 @@ impl<'tcx> LateLintPass<'tcx> for NonShorthandFieldPatterns {
                     if cx.tcx.find_field_index(ident, &variant)
                         == Some(cx.typeck_results().field_index(fieldpat.hir_id))
                     {
-                        cx.struct_span_lint(
+                        cx.emit_spanned_lint(
                             NON_SHORTHAND_FIELD_PATTERNS,
                             fieldpat.span,
-                            fluent::lint_builtin_non_shorthand_field_patterns,
-                            |lint| {
-                                let suggested_ident =
-                                    format!("{}{}", binding_annot.prefix_str(), ident);
-                                lint.set_arg("ident", ident).span_suggestion(
-                                    fieldpat.span,
-                                    fluent::suggestion,
-                                    suggested_ident,
-                                    Applicability::MachineApplicable,
-                                )
+                            BuiltinNonShorthandFieldPatterns {
+                                ident,
+                                suggestion: fieldpat.span,
+                                prefix: binding_annot.prefix_str(),
                             },
                         );
                     }
@@ -321,48 +316,21 @@ impl UnsafeCode {
         &self,
         cx: &EarlyContext<'_>,
         span: Span,
-        msg: impl Into<DiagnosticMessage>,
-        decorate: impl for<'a, 'b> FnOnce(
-            &'b mut DiagnosticBuilder<'a, ()>,
-        ) -> &'b mut DiagnosticBuilder<'a, ()>,
+        decorate: impl for<'a> DecorateLint<'a, ()>,
     ) {
         // This comes from a macro that has `#[allow_internal_unsafe]`.
         if span.allows_unsafe() {
             return;
         }
 
-        cx.struct_span_lint(UNSAFE_CODE, span, msg, decorate);
-    }
-
-    fn report_overridden_symbol_name(
-        &self,
-        cx: &EarlyContext<'_>,
-        span: Span,
-        msg: DiagnosticMessage,
-    ) {
-        self.report_unsafe(cx, span, msg, |lint| {
-            lint.note(fluent::lint_builtin_overridden_symbol_name)
-        })
-    }
-
-    fn report_overridden_symbol_section(
-        &self,
-        cx: &EarlyContext<'_>,
-        span: Span,
-        msg: DiagnosticMessage,
-    ) {
-        self.report_unsafe(cx, span, msg, |lint| {
-            lint.note(fluent::lint_builtin_overridden_symbol_section)
-        })
+        cx.emit_spanned_lint(UNSAFE_CODE, span, decorate);
     }
 }
 
 impl EarlyLintPass for UnsafeCode {
     fn check_attribute(&mut self, cx: &EarlyContext<'_>, attr: &ast::Attribute) {
         if attr.has_name(sym::allow_internal_unsafe) {
-            self.report_unsafe(cx, attr.span, fluent::lint_builtin_allow_internal_unsafe, |lint| {
-                lint
-            });
+            self.report_unsafe(cx, attr.span, BuiltinUnsafe::AllowInternalUnsafe);
         }
     }
 
@@ -371,7 +339,7 @@ impl EarlyLintPass for UnsafeCode {
         if let ast::ExprKind::Block(ref blk, _) = e.kind {
             // Don't warn about generated blocks; that'll just pollute the output.
             if blk.rules == ast::BlockCheckMode::Unsafe(ast::UserProvided) {
-                self.report_unsafe(cx, blk.span, fluent::lint_builtin_unsafe_block, |lint| lint);
+                self.report_unsafe(cx, blk.span, BuiltinUnsafe::UnsafeBlock);
             }
         }
     }
@@ -379,62 +347,38 @@ impl EarlyLintPass for UnsafeCode {
     fn check_item(&mut self, cx: &EarlyContext<'_>, it: &ast::Item) {
         match it.kind {
             ast::ItemKind::Trait(box ast::Trait { unsafety: ast::Unsafe::Yes(_), .. }) => {
-                self.report_unsafe(cx, it.span, fluent::lint_builtin_unsafe_trait, |lint| lint)
+                self.report_unsafe(cx, it.span, BuiltinUnsafe::UnsafeTrait);
             }
 
             ast::ItemKind::Impl(box ast::Impl { unsafety: ast::Unsafe::Yes(_), .. }) => {
-                self.report_unsafe(cx, it.span, fluent::lint_builtin_unsafe_impl, |lint| lint)
+                self.report_unsafe(cx, it.span, BuiltinUnsafe::UnsafeImpl);
             }
 
             ast::ItemKind::Fn(..) => {
                 if let Some(attr) = cx.sess().find_by_name(&it.attrs, sym::no_mangle) {
-                    self.report_overridden_symbol_name(
-                        cx,
-                        attr.span,
-                        fluent::lint_builtin_no_mangle_fn,
-                    );
+                    self.report_unsafe(cx, attr.span, BuiltinUnsafe::NoMangleFn);
                 }
 
                 if let Some(attr) = cx.sess().find_by_name(&it.attrs, sym::export_name) {
-                    self.report_overridden_symbol_name(
-                        cx,
-                        attr.span,
-                        fluent::lint_builtin_export_name_fn,
-                    );
+                    self.report_unsafe(cx, attr.span, BuiltinUnsafe::ExportNameFn);
                 }
 
                 if let Some(attr) = cx.sess().find_by_name(&it.attrs, sym::link_section) {
-                    self.report_overridden_symbol_section(
-                        cx,
-                        attr.span,
-                        fluent::lint_builtin_link_section_fn,
-                    );
+                    self.report_unsafe(cx, attr.span, BuiltinUnsafe::LinkSectionFn);
                 }
             }
 
             ast::ItemKind::Static(..) => {
                 if let Some(attr) = cx.sess().find_by_name(&it.attrs, sym::no_mangle) {
-                    self.report_overridden_symbol_name(
-                        cx,
-                        attr.span,
-                        fluent::lint_builtin_no_mangle_static,
-                    );
+                    self.report_unsafe(cx, attr.span, BuiltinUnsafe::NoMangleStatic);
                 }
 
                 if let Some(attr) = cx.sess().find_by_name(&it.attrs, sym::export_name) {
-                    self.report_overridden_symbol_name(
-                        cx,
-                        attr.span,
-                        fluent::lint_builtin_export_name_static,
-                    );
+                    self.report_unsafe(cx, attr.span, BuiltinUnsafe::ExportNameStatic);
                 }
 
                 if let Some(attr) = cx.sess().find_by_name(&it.attrs, sym::link_section) {
-                    self.report_overridden_symbol_section(
-                        cx,
-                        attr.span,
-                        fluent::lint_builtin_link_section_static,
-                    );
+                    self.report_unsafe(cx, attr.span, BuiltinUnsafe::LinkSectionStatic);
                 }
             }
 
@@ -445,18 +389,10 @@ impl EarlyLintPass for UnsafeCode {
     fn check_impl_item(&mut self, cx: &EarlyContext<'_>, it: &ast::AssocItem) {
         if let ast::AssocItemKind::Fn(..) = it.kind {
             if let Some(attr) = cx.sess().find_by_name(&it.attrs, sym::no_mangle) {
-                self.report_overridden_symbol_name(
-                    cx,
-                    attr.span,
-                    fluent::lint_builtin_no_mangle_method,
-                );
+                self.report_unsafe(cx, attr.span, BuiltinUnsafe::NoMangleMethod);
             }
             if let Some(attr) = cx.sess().find_by_name(&it.attrs, sym::export_name) {
-                self.report_overridden_symbol_name(
-                    cx,
-                    attr.span,
-                    fluent::lint_builtin_export_name_method,
-                );
+                self.report_unsafe(cx, attr.span, BuiltinUnsafe::ExportNameMethod);
             }
         }
     }
@@ -471,13 +407,13 @@ impl EarlyLintPass for UnsafeCode {
             body,
         ) = fk
         {
-            let msg = match ctxt {
+            let decorator = match ctxt {
                 FnCtxt::Foreign => return,
-                FnCtxt::Free => fluent::lint_builtin_decl_unsafe_fn,
-                FnCtxt::Assoc(_) if body.is_none() => fluent::lint_builtin_decl_unsafe_method,
-                FnCtxt::Assoc(_) => fluent::lint_builtin_impl_unsafe_method,
+                FnCtxt::Free => BuiltinUnsafe::DeclUnsafeFn,
+                FnCtxt::Assoc(_) if body.is_none() => BuiltinUnsafe::DeclUnsafeMethod,
+                FnCtxt::Assoc(_) => BuiltinUnsafe::ImplUnsafeMethod,
             };
-            self.report_unsafe(cx, span, msg, |lint| lint);
+            self.report_unsafe(cx, span, decorator);
         }
     }
 }
@@ -578,11 +514,10 @@ impl MissingDoc {
         let attrs = cx.tcx.hir().attrs(cx.tcx.hir().local_def_id_to_hir_id(def_id));
         let has_doc = attrs.iter().any(has_doc);
         if !has_doc {
-            cx.struct_span_lint(
+            cx.emit_spanned_lint(
                 MISSING_DOCS,
                 cx.tcx.def_span(def_id),
-                fluent::lint_builtin_missing_doc,
-                |lint| lint.set_arg("article", article).set_arg("desc", desc),
+                BuiltinMissingDoc { article, desc },
             );
         }
     }
@@ -799,12 +734,7 @@ impl<'tcx> LateLintPass<'tcx> for MissingCopyImplementations {
         )
         .is_ok()
         {
-            cx.struct_span_lint(
-                MISSING_COPY_IMPLEMENTATIONS,
-                item.span,
-                fluent::lint_builtin_missing_copy_impl,
-                |lint| lint,
-            )
+            cx.emit_spanned_lint(MISSING_COPY_IMPLEMENTATIONS, item.span, BuiltinMissingCopyImpl);
         }
     }
 }
@@ -878,11 +808,10 @@ impl<'tcx> LateLintPass<'tcx> for MissingDebugImplementations {
         }
 
         if !self.impling_types.as_ref().unwrap().contains(&item.owner_id.def_id) {
-            cx.struct_span_lint(
+            cx.emit_spanned_lint(
                 MISSING_DEBUG_IMPLEMENTATIONS,
                 item.span,
-                fluent::lint_builtin_missing_debug_impl,
-                |lint| lint.set_arg("debug", cx.tcx.def_path_str(debug)),
+                BuiltinMissingDebugImpl { tcx: cx.tcx, def_id: debug },
             );
         }
     }
@@ -958,19 +887,11 @@ impl EarlyLintPass for AnonymousParameters {
                         } else {
                             ("<type>", Applicability::HasPlaceholders)
                         };
-                        cx.struct_span_lint(
+                        cx.emit_spanned_lint(
                             ANONYMOUS_PARAMETERS,
                             arg.pat.span,
-                            fluent::lint_builtin_anonymous_params,
-                            |lint| {
-                                lint.span_suggestion(
-                                    arg.pat.span,
-                                    fluent::suggestion,
-                                    format!("_: {}", ty_snip),
-                                    appl,
-                                )
-                            },
-                        )
+                            BuiltinAnonymousParams { suggestion: (arg.pat.span, appl), ty_snip },
+                        );
                     }
                 }
             }
@@ -1005,42 +926,30 @@ impl EarlyLintPass for DeprecatedAttr {
                     _,
                 ) = gate
                 {
-                    // FIXME(davidtwco) translatable deprecated attr
-                    cx.struct_span_lint(
+                    let suggestion = match suggestion {
+                        Some(msg) => {
+                            BuiltinDeprecatedAttrLinkSuggestion::Msg { suggestion: attr.span, msg }
+                        }
+                        None => {
+                            BuiltinDeprecatedAttrLinkSuggestion::Default { suggestion: attr.span }
+                        }
+                    };
+                    cx.emit_spanned_lint(
                         DEPRECATED,
                         attr.span,
-                        fluent::lint_builtin_deprecated_attr_link,
-                        |lint| {
-                            lint.set_arg("name", name)
-                                .set_arg("reason", reason)
-                                .set_arg("link", link)
-                                .span_suggestion_short(
-                                    attr.span,
-                                    suggestion.map(|s| s.into()).unwrap_or(
-                                        fluent::lint_builtin_deprecated_attr_default_suggestion,
-                                    ),
-                                    "",
-                                    Applicability::MachineApplicable,
-                                )
-                        },
+                        BuiltinDeprecatedAttrLink { name, reason, link, suggestion },
                     );
                 }
                 return;
             }
         }
         if attr.has_name(sym::no_start) || attr.has_name(sym::crate_id) {
-            cx.struct_span_lint(
+            cx.emit_spanned_lint(
                 DEPRECATED,
                 attr.span,
-                fluent::lint_builtin_deprecated_attr_used,
-                |lint| {
-                    lint.set_arg("name", pprust::path_to_string(&attr.get_normal_item().path))
-                        .span_suggestion_short(
-                            attr.span,
-                            fluent::lint_builtin_deprecated_attr_default_suggestion,
-                            "",
-                            Applicability::MachineApplicable,
-                        )
+                BuiltinDeprecatedAttrUsed {
+                    name: pprust::path_to_string(&attr.get_normal_item().path),
+                    suggestion: attr.span,
                 },
             );
         }
@@ -1069,20 +978,18 @@ fn warn_if_doc(cx: &EarlyContext<'_>, node_span: Span, node_kind: &str, attrs: &
         let span = sugared_span.take().unwrap_or(attr.span);
 
         if is_doc_comment || attr.has_name(sym::doc) {
-            cx.struct_span_lint(
+            let sub = match attr.kind {
+                AttrKind::DocComment(CommentKind::Line, _) | AttrKind::Normal(..) => {
+                    BuiltinUnusedDocCommentSub::PlainHelp
+                }
+                AttrKind::DocComment(CommentKind::Block, _) => {
+                    BuiltinUnusedDocCommentSub::BlockHelp
+                }
+            };
+            cx.emit_spanned_lint(
                 UNUSED_DOC_COMMENTS,
                 span,
-                fluent::lint_builtin_unused_doc_comment,
-                |lint| {
-                    lint.set_arg("kind", node_kind).span_label(node_span, fluent::label).help(
-                        match attr.kind {
-                            AttrKind::DocComment(CommentKind::Line, _) | AttrKind::Normal(..) => {
-                                fluent::plain_help
-                            }
-                            AttrKind::DocComment(CommentKind::Block, _) => fluent::block_help,
-                        },
-                    )
-                },
+                BuiltinUnusedDocComment { kind: node_kind, label: node_span, sub },
             );
         }
     }
@@ -1197,20 +1104,10 @@ impl<'tcx> LateLintPass<'tcx> for InvalidNoMangleItems {
                 match param.kind {
                     GenericParamKind::Lifetime { .. } => {}
                     GenericParamKind::Type { .. } | GenericParamKind::Const { .. } => {
-                        cx.struct_span_lint(
+                        cx.emit_spanned_lint(
                             NO_MANGLE_GENERIC_ITEMS,
                             span,
-                            fluent::lint_builtin_no_mangle_generic,
-                            |lint| {
-                                lint.span_suggestion_short(
-                                    no_mangle_attr.span,
-                                    fluent::suggestion,
-                                    "",
-                                    // Use of `#[no_mangle]` suggests FFI intent; correct
-                                    // fix may be to monomorphize source by hand
-                                    Applicability::MaybeIncorrect,
-                                )
-                            },
+                            BuiltinNoMangleGeneric { suggestion: no_mangle_attr.span },
                         );
                         break;
                     }
@@ -1225,30 +1122,23 @@ impl<'tcx> LateLintPass<'tcx> for InvalidNoMangleItems {
             }
             hir::ItemKind::Const(..) => {
                 if cx.sess().contains_name(attrs, sym::no_mangle) {
+                    // account for "pub const" (#45562)
+                    let start = cx
+                        .tcx
+                        .sess
+                        .source_map()
+                        .span_to_snippet(it.span)
+                        .map(|snippet| snippet.find("const").unwrap_or(0))
+                        .unwrap_or(0) as u32;
+                    // `const` is 5 chars
+                    let suggestion = it.span.with_hi(BytePos(it.span.lo().0 + start + 5));
+
                     // Const items do not refer to a particular location in memory, and therefore
                     // don't have anything to attach a symbol to
-                    cx.struct_span_lint(
+                    cx.emit_spanned_lint(
                         NO_MANGLE_CONST_ITEMS,
                         it.span,
-                        fluent::lint_builtin_const_no_mangle,
-                        |lint| {
-                            // account for "pub const" (#45562)
-                            let start = cx
-                                .tcx
-                                .sess
-                                .source_map()
-                                .span_to_snippet(it.span)
-                                .map(|snippet| snippet.find("const").unwrap_or(0))
-                                .unwrap_or(0) as u32;
-                            // `const` is 5 chars
-                            let const_span = it.span.with_hi(BytePos(it.span.lo().0 + start + 5));
-                            lint.span_suggestion(
-                                const_span,
-                                fluent::suggestion,
-                                "pub static",
-                                Applicability::MachineApplicable,
-                            )
-                        },
+                        BuiltinConstNoMangle { suggestion },
                     );
                 }
             }
@@ -1309,12 +1199,7 @@ impl<'tcx> LateLintPass<'tcx> for MutableTransmutes {
             get_transmute_from_to(cx, expr).map(|(ty1, ty2)| (ty1.kind(), ty2.kind()))
         {
             if from_mutbl < to_mutbl {
-                cx.struct_span_lint(
-                    MUTABLE_TRANSMUTES,
-                    expr.span,
-                    fluent::lint_builtin_mutable_transmutes,
-                    |lint| lint,
-                );
+                cx.emit_spanned_lint(MUTABLE_TRANSMUTES, expr.span, BuiltinMutablesTransmutes);
             }
         }
 
@@ -1362,12 +1247,7 @@ impl<'tcx> LateLintPass<'tcx> for UnstableFeatures {
         if attr.has_name(sym::feature) {
             if let Some(items) = attr.meta_item_list() {
                 for item in items {
-                    cx.struct_span_lint(
-                        UNSTABLE_FEATURES,
-                        item.span(),
-                        fluent::lint_builtin_unstable_features,
-                        |lint| lint,
-                    );
+                    cx.emit_spanned_lint(UNSTABLE_FEATURES, item.span(), BuiltinUnstableFeatures);
                 }
             }
         }
@@ -1422,20 +1302,10 @@ impl<'tcx> LateLintPass<'tcx> for UngatedAsyncFnTrackCaller {
             // Now, check if the function has the `#[track_caller]` attribute
             && let Some(attr) = attrs.iter().find(|attr| attr.has_name(sym::track_caller))
             {
-                cx.struct_span_lint(
-                    UNGATED_ASYNC_FN_TRACK_CALLER,
-                    attr.span,
-                    fluent::lint_ungated_async_fn_track_caller,
-                    |lint| {
-                        lint.span_label(span, fluent::label);
-                        rustc_session::parse::add_feature_diagnostics(
-                            lint,
-                            &cx.tcx.sess.parse_sess,
-                            sym::closure_track_caller,
-                        );
-                        lint
-                    },
-                );
+                cx.emit_spanned_lint(UNGATED_ASYNC_FN_TRACK_CALLER, attr.span, BuiltinUngatedAsyncFnTrackCaller {
+                    label: span,
+                    parse_sess: &cx.tcx.sess.parse_sess,
+                });
             }
     }
 }
@@ -1493,18 +1363,13 @@ impl UnreachablePub {
                 applicability = Applicability::MaybeIncorrect;
             }
             let def_span = cx.tcx.def_span(def_id);
-            cx.struct_span_lint(
+            cx.emit_spanned_lint(
                 UNREACHABLE_PUB,
                 def_span,
-                fluent::lint_builtin_unreachable_pub,
-                |lint| {
-                    lint.set_arg("what", what);
-
-                    lint.span_suggestion(vis_span, fluent::suggestion, "pub(crate)", applicability);
-                    if exportable {
-                        lint.help(fluent::help);
-                    }
-                    lint
+                BuiltinUnreachablePub {
+                    what,
+                    suggestion: (vis_span, applicability),
+                    help: exportable.then_some(()),
                 },
             );
         }
@@ -1569,7 +1434,7 @@ declare_lint_pass!(
 );
 
 impl TypeAliasBounds {
-    fn is_type_variable_assoc(qpath: &hir::QPath<'_>) -> bool {
+    pub(crate) fn is_type_variable_assoc(qpath: &hir::QPath<'_>) -> bool {
         match *qpath {
             hir::QPath::TypeRelative(ref ty, _) => {
                 // If this is a type variable, we found a `T::Assoc`.
@@ -1583,29 +1448,6 @@ impl TypeAliasBounds {
             hir::QPath::Resolved(..) | hir::QPath::LangItem(..) => false,
         }
     }
-
-    fn suggest_changing_assoc_types(ty: &hir::Ty<'_>, err: &mut Diagnostic) {
-        // Access to associates types should use `<T as Bound>::Assoc`, which does not need a
-        // bound.  Let's see if this type does that.
-
-        // We use a HIR visitor to walk the type.
-        use rustc_hir::intravisit::{self, Visitor};
-        struct WalkAssocTypes<'a> {
-            err: &'a mut Diagnostic,
-        }
-        impl Visitor<'_> for WalkAssocTypes<'_> {
-            fn visit_qpath(&mut self, qpath: &hir::QPath<'_>, id: hir::HirId, span: Span) {
-                if TypeAliasBounds::is_type_variable_assoc(qpath) {
-                    self.err.span_help(span, fluent::lint_builtin_type_alias_bounds_help);
-                }
-                intravisit::walk_qpath(self, qpath, id)
-            }
-        }
-
-        // Let's go for a walk!
-        let mut visitor = WalkAssocTypes { err };
-        visitor.visit_ty(ty);
-    }
 }
 
 impl<'tcx> LateLintPass<'tcx> for TypeAliasBounds {
@@ -1639,35 +1481,31 @@ impl<'tcx> LateLintPass<'tcx> for TypeAliasBounds {
 
         let mut suggested_changing_assoc_types = false;
         if !where_spans.is_empty() {
-            cx.lint(TYPE_ALIAS_BOUNDS, fluent::lint_builtin_type_alias_where_clause, |lint| {
-                lint.set_span(where_spans);
-                lint.span_suggestion(
-                    type_alias_generics.where_clause_span,
-                    fluent::suggestion,
-                    "",
-                    Applicability::MachineApplicable,
-                );
-                if !suggested_changing_assoc_types {
-                    TypeAliasBounds::suggest_changing_assoc_types(ty, lint);
-                    suggested_changing_assoc_types = true;
-                }
-                lint
+            let sub = (!suggested_changing_assoc_types).then(|| {
+                suggested_changing_assoc_types = true;
+                SuggestChangingAssocTypes { ty }
             });
+            cx.emit_spanned_lint(
+                TYPE_ALIAS_BOUNDS,
+                where_spans,
+                BuiltinTypeAliasWhereClause {
+                    suggestion: type_alias_generics.where_clause_span,
+                    sub,
+                },
+            );
         }
 
         if !inline_spans.is_empty() {
-            cx.lint(TYPE_ALIAS_BOUNDS, fluent::lint_builtin_type_alias_generic_bounds, |lint| {
-                lint.set_span(inline_spans);
-                lint.multipart_suggestion(
-                    fluent::suggestion,
-                    inline_sugg,
-                    Applicability::MachineApplicable,
-                );
-                if !suggested_changing_assoc_types {
-                    TypeAliasBounds::suggest_changing_assoc_types(ty, lint);
-                }
-                lint
+            let suggestion = BuiltinTypeAliasGenericBoundsSuggestion { suggestions: inline_sugg };
+            let sub = (!suggested_changing_assoc_types).then(|| {
+                suggested_changing_assoc_types = true;
+                SuggestChangingAssocTypes { ty }
             });
+            cx.emit_spanned_lint(
+                TYPE_ALIAS_BOUNDS,
+                inline_spans,
+                BuiltinTypeAliasGenericBounds { suggestion, sub },
+            );
         }
     }
 }
@@ -1767,14 +1605,10 @@ impl<'tcx> LateLintPass<'tcx> for TrivialConstraints {
                     TypeWellFormedFromEnv(..) => continue,
                 };
                 if predicate.is_global() {
-                    cx.struct_span_lint(
+                    cx.emit_spanned_lint(
                         TRIVIAL_BOUNDS,
                         span,
-                        fluent::lint_builtin_trivial_bounds,
-                        |lint| {
-                            lint.set_arg("predicate_kind_name", predicate_kind_name)
-                                .set_arg("predicate", predicate)
-                        },
+                        BuiltinTrivialBounds { predicate_kind_name, predicate },
                     );
                 }
             }
@@ -1875,8 +1709,6 @@ impl EarlyLintPass for EllipsisInclusiveRangePatterns {
         };
 
         if let Some((start, end, join)) = endpoints {
-            let msg = fluent::lint_builtin_ellipsis_inclusive_range_patterns;
-            let suggestion = fluent::suggestion;
             if parenthesise {
                 self.node_id = Some(pat.id);
                 let end = expr_to_string(&end);
@@ -1891,14 +1723,14 @@ impl EarlyLintPass for EllipsisInclusiveRangePatterns {
                         replace,
                     });
                 } else {
-                    cx.struct_span_lint(ELLIPSIS_INCLUSIVE_RANGE_PATTERNS, pat.span, msg, |lint| {
-                        lint.span_suggestion(
-                            pat.span,
-                            suggestion,
+                    cx.emit_spanned_lint(
+                        ELLIPSIS_INCLUSIVE_RANGE_PATTERNS,
+                        pat.span,
+                        BuiltinEllipsisInclusiveRangePatternsLint::Parenthesise {
+                            suggestion: pat.span,
                             replace,
-                            Applicability::MachineApplicable,
-                        )
-                    });
+                        },
+                    );
                 }
             } else {
                 let replace = "..=";
@@ -1909,14 +1741,13 @@ impl EarlyLintPass for EllipsisInclusiveRangePatterns {
                         replace: replace.to_string(),
                     });
                 } else {
-                    cx.struct_span_lint(ELLIPSIS_INCLUSIVE_RANGE_PATTERNS, join, msg, |lint| {
-                        lint.span_suggestion_short(
-                            join,
-                            suggestion,
-                            replace,
-                            Applicability::MachineApplicable,
-                        )
-                    });
+                    cx.emit_spanned_lint(
+                        ELLIPSIS_INCLUSIVE_RANGE_PATTERNS,
+                        join,
+                        BuiltinEllipsisInclusiveRangePatternsLint::NonParenthesise {
+                            suggestion: join,
+                        },
+                    );
                 }
             };
         }
@@ -1996,12 +1827,7 @@ impl<'tcx> LateLintPass<'tcx> for UnnameableTestItems {
 
         let attrs = cx.tcx.hir().attrs(it.hir_id());
         if let Some(attr) = cx.sess().find_by_name(attrs, sym::rustc_test_marker) {
-            cx.struct_span_lint(
-                UNNAMEABLE_TEST_ITEMS,
-                attr.span,
-                fluent::lint_builtin_unnameable_test_items,
-                |lint| lint,
-            );
+            cx.emit_spanned_lint(UNNAMEABLE_TEST_ITEMS, attr.span, BuiltinUnnameableTestItems);
         }
     }
 
@@ -2117,18 +1943,10 @@ impl KeywordIdents {
             return;
         }
 
-        cx.struct_span_lint(
+        cx.emit_spanned_lint(
             KEYWORD_IDENTS,
             ident.span,
-            fluent::lint_builtin_keyword_idents,
-            |lint| {
-                lint.set_arg("kw", ident).set_arg("next", next_edition).span_suggestion(
-                    ident.span,
-                    fluent::suggestion,
-                    format!("r#{}", ident),
-                    Applicability::MachineApplicable,
-                )
-            },
+            BuiltinKeywordIdents { kw: ident, next: next_edition, suggestion: ident.span },
         );
     }
 }
@@ -2405,16 +2223,15 @@ impl<'tcx> LateLintPass<'tcx> for ExplicitOutlivesRequirements {
                     Applicability::MaybeIncorrect
                 };
 
-                cx.struct_span_lint(
+                cx.emit_spanned_lint(
                     EXPLICIT_OUTLIVES_REQUIREMENTS,
                     lint_spans.clone(),
-                    fluent::lint_builtin_explicit_outlives,
-                    |lint| {
-                        lint.set_arg("count", bound_count).multipart_suggestion(
-                            fluent::suggestion,
-                            lint_spans.into_iter().map(|span| (span, String::new())).collect(),
+                    BuiltinExplicitOutlives {
+                        count: bound_count,
+                        suggestion: BuiltinExplicitOutlivesSuggestion {
+                            spans: lint_spans,
                             applicability,
-                        )
+                        },
                     },
                 );
             }
@@ -2463,24 +2280,18 @@ impl EarlyLintPass for IncompleteFeatures {
             .chain(features.declared_lib_features.iter().map(|(name, span)| (name, span)))
             .filter(|(&name, _)| features.incomplete(name))
             .for_each(|(&name, &span)| {
-                cx.struct_span_lint(
+                let note = rustc_feature::find_feature_issue(name, GateIssue::Language)
+                    .map(|n| BuiltinIncompleteFeaturesNote { n });
+                let help = if HAS_MIN_FEATURES.contains(&name) {
+                    Some(BuiltinIncompleteFeaturesHelp)
+                } else {
+                    None
+                };
+                cx.emit_spanned_lint(
                     INCOMPLETE_FEATURES,
                     span,
-                    fluent::lint_builtin_incomplete_features,
-                    |lint| {
-                        lint.set_arg("name", name);
-                        if let Some(n) =
-                            rustc_feature::find_feature_issue(name, GateIssue::Language)
-                        {
-                            lint.set_arg("n", n);
-                            lint.note(fluent::note);
-                        }
-                        if HAS_MIN_FEATURES.contains(&name) {
-                            lint.help(fluent::help);
-                        }
-                        lint
-                    },
-                )
+                    BuiltinIncompleteFeatures { name, note, help },
+                );
             });
     }
 }
@@ -2525,6 +2336,36 @@ declare_lint! {
 
 declare_lint_pass!(InvalidValue => [INVALID_VALUE]);
 
+/// Information about why a type cannot be initialized this way.
+pub struct InitError {
+    pub(crate) message: String,
+    /// Spans from struct fields and similar that can be obtained from just the type.
+    pub(crate) span: Option<Span>,
+    /// Used to report a trace through adts.
+    pub(crate) nested: Option<Box<InitError>>,
+}
+impl InitError {
+    fn spanned(self, span: Span) -> InitError {
+        Self { span: Some(span), ..self }
+    }
+
+    fn nested(self, nested: impl Into<Option<InitError>>) -> InitError {
+        assert!(self.nested.is_none());
+        Self { nested: nested.into().map(Box::new), ..self }
+    }
+}
+
+impl<'a> From<&'a str> for InitError {
+    fn from(s: &'a str) -> Self {
+        s.to_owned().into()
+    }
+}
+impl From<String> for InitError {
+    fn from(message: String) -> Self {
+        Self { message, span: None, nested: None }
+    }
+}
+
 impl<'tcx> LateLintPass<'tcx> for InvalidValue {
     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &hir::Expr<'_>) {
         #[derive(Debug, Copy, Clone, PartialEq)]
@@ -2533,36 +2374,6 @@ impl<'tcx> LateLintPass<'tcx> for InvalidValue {
             Uninit,
         }
 
-        /// Information about why a type cannot be initialized this way.
-        struct InitError {
-            message: String,
-            /// Spans from struct fields and similar that can be obtained from just the type.
-            span: Option<Span>,
-            /// Used to report a trace through adts.
-            nested: Option<Box<InitError>>,
-        }
-        impl InitError {
-            fn spanned(self, span: Span) -> InitError {
-                Self { span: Some(span), ..self }
-            }
-
-            fn nested(self, nested: impl Into<Option<InitError>>) -> InitError {
-                assert!(self.nested.is_none());
-                Self { nested: nested.into().map(Box::new), ..self }
-            }
-        }
-
-        impl<'a> From<&'a str> for InitError {
-            fn from(s: &'a str) -> Self {
-                s.to_owned().into()
-            }
-        }
-        impl From<String> for InitError {
-            fn from(message: String) -> Self {
-                Self { message, span: None, nested: None }
-            }
-        }
-
         /// Test if this constant is all-0.
         fn is_zero(expr: &hir::Expr<'_>) -> bool {
             use hir::ExprKind::*;
@@ -2786,46 +2597,16 @@ impl<'tcx> LateLintPass<'tcx> for InvalidValue {
             // using zeroed or uninitialized memory.
             // We are extremely conservative with what we warn about.
             let conjured_ty = cx.typeck_results().expr_ty(expr);
-            if let Some(mut err) = with_no_trimmed_paths!(ty_find_init_error(cx, conjured_ty, init))
-            {
-                // FIXME(davidtwco): make translatable
-                cx.struct_span_lint(
+            if let Some(err) = with_no_trimmed_paths!(ty_find_init_error(cx, conjured_ty, init)) {
+                let msg = match init {
+                    InitKind::Zeroed => fluent::lint_builtin_unpermitted_type_init_zeroed,
+                    InitKind::Uninit => fluent::lint_builtin_unpermitted_type_init_unint,
+                };
+                let sub = BuiltinUnpermittedTypeInitSub { err };
+                cx.emit_spanned_lint(
                     INVALID_VALUE,
                     expr.span,
-                    DelayDm(|| {
-                        format!(
-                            "the type `{}` does not permit {}",
-                            conjured_ty,
-                            match init {
-                                InitKind::Zeroed => "zero-initialization",
-                                InitKind::Uninit => "being left uninitialized",
-                            },
-                        )
-                    }),
-                    |lint| {
-                        lint.span_label(
-                            expr.span,
-                            "this code causes undefined behavior when executed",
-                        );
-                        lint.span_label(
-                            expr.span,
-                            "help: use `MaybeUninit<T>` instead, \
-                            and only call `assume_init` after initialization is done",
-                        );
-                        loop {
-                            if let Some(span) = err.span {
-                                lint.span_note(span, &err.message);
-                            } else {
-                                lint.note(&err.message);
-                            }
-                            if let Some(e) = err.nested {
-                                err = *e;
-                            } else {
-                                break;
-                            }
-                        }
-                        lint
-                    },
+                    BuiltinUnpermittedTypeInit { msg, ty: conjured_ty, label: expr.span, sub },
                 );
             }
         }
@@ -3171,31 +2952,39 @@ impl<'tcx> LateLintPass<'tcx> for ClashingExternDeclarations {
                             SymbolName::Normal(_) => fi.span,
                             SymbolName::Link(_, annot_span) => fi.span.to(annot_span),
                         };
-                    // Finally, emit the diagnostic.
 
-                    let msg = if orig.get_name() == this_fi.ident.name {
-                        fluent::lint_builtin_clashing_extern_same_name
+                    // Finally, emit the diagnostic.
+                    let this = this_fi.ident.name;
+                    let orig = orig.get_name();
+                    let previous_decl_label = get_relevant_span(orig_fi);
+                    let mismatch_label = get_relevant_span(this_fi);
+                    let sub = BuiltinClashingExternSub {
+                        tcx,
+                        expected: existing_decl_ty,
+                        found: this_decl_ty,
+                    };
+                    let decorator = if orig == this {
+                        BuiltinClashingExtern::SameName {
+                            this,
+                            orig,
+                            previous_decl_label,
+                            mismatch_label,
+                            sub,
+                        }
                     } else {
-                        fluent::lint_builtin_clashing_extern_diff_name
+                        BuiltinClashingExtern::DiffName {
+                            this,
+                            orig,
+                            previous_decl_label,
+                            mismatch_label,
+                            sub,
+                        }
                     };
-                    tcx.struct_span_lint_hir(
+                    tcx.emit_spanned_lint(
                         CLASHING_EXTERN_DECLARATIONS,
                         this_fi.hir_id(),
                         get_relevant_span(this_fi),
-                        msg,
-                        |lint| {
-                            let mut expected_str = DiagnosticStyledString::new();
-                            expected_str.push(existing_decl_ty.fn_sig(tcx).to_string(), false);
-                            let mut found_str = DiagnosticStyledString::new();
-                            found_str.push(this_decl_ty.fn_sig(tcx).to_string(), true);
-
-                            lint.set_arg("this_fi", this_fi.ident.name)
-                                .set_arg("orig", orig.get_name())
-                                .span_label(get_relevant_span(orig_fi), fluent::previous_decl_label)
-                                .span_label(get_relevant_span(this_fi), fluent::mismatch_label)
-                                // FIXME(davidtwco): translatable expected/found
-                                .note_expected_found(&"", expected_str, &"", found_str)
-                        },
+                        decorator,
                     );
                 }
             }
@@ -3275,11 +3064,10 @@ impl<'tcx> LateLintPass<'tcx> for DerefNullPtr {
 
         if let rustc_hir::ExprKind::Unary(rustc_hir::UnOp::Deref, expr_deref) = expr.kind {
             if is_null_ptr(cx, expr_deref) {
-                cx.struct_span_lint(
+                cx.emit_spanned_lint(
                     DEREF_NULLPTR,
                     expr.span,
-                    fluent::lint_builtin_deref_nullptr,
-                    |lint| lint.span_label(expr.span, fluent::label),
+                    BuiltinDerefNullptr { label: expr.span },
                 );
             }
         }
@@ -3324,6 +3112,7 @@ declare_lint! {
 declare_lint_pass!(NamedAsmLabels => [NAMED_ASM_LABELS]);
 
 impl<'tcx> LateLintPass<'tcx> for NamedAsmLabels {
+    #[allow(rustc::diagnostic_outside_of_impl)]
     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
         if let hir::Expr {
             kind: hir::ExprKind::InlineAsm(hir::InlineAsm { template_strs, .. }),
@@ -3464,16 +3253,17 @@ impl EarlyLintPass for SpecialModuleName {
                 }
 
                 match item.ident.name.as_str() {
-                    "lib" => cx.struct_span_lint(SPECIAL_MODULE_NAME, item.span, "found module declaration for lib.rs", |lint| {
-                        lint
-                            .note("lib.rs is the root of this crate's library target")
-                            .help("to refer to it from other targets, use the library's name as the path")
-                    }),
-                    "main" => cx.struct_span_lint(SPECIAL_MODULE_NAME, item.span, "found module declaration for main.rs", |lint| {
-                        lint
-                            .note("a binary crate cannot be used as library")
-                    }),
-                    _ => continue
+                    "lib" => cx.emit_spanned_lint(
+                        SPECIAL_MODULE_NAME,
+                        item.span,
+                        BuiltinSpecialModuleNameUsed::Lib,
+                    ),
+                    "main" => cx.emit_spanned_lint(
+                        SPECIAL_MODULE_NAME,
+                        item.span,
+                        BuiltinSpecialModuleNameUsed::Main,
+                    ),
+                    _ => continue,
                 }
             }
         }
@@ -3489,31 +3279,16 @@ impl EarlyLintPass for UnexpectedCfgs {
         let cfg = &cx.sess().parse_sess.config;
         let check_cfg = &cx.sess().parse_sess.check_config;
         for &(name, value) in cfg {
-            if let Some(names_valid) = &check_cfg.names_valid {
-                if !names_valid.contains(&name) {
-                    cx.lookup(
-                        UNEXPECTED_CFGS,
-                        None::<MultiSpan>,
-                        fluent::lint_builtin_unexpected_cli_config_name,
-                        |diag| diag.help(fluent::help).set_arg("name", name),
-                    );
-                }
+            if let Some(names_valid) = &check_cfg.names_valid && !names_valid.contains(&name){
+                cx.emit_lint(UNEXPECTED_CFGS, BuiltinUnexpectedCliConfigName {
+                    name,
+                });
             }
-            if let Some(value) = value {
-                if let Some(values) = &check_cfg.values_valid.get(&name) {
-                    if !values.contains(&value) {
-                        cx.lookup(
-                            UNEXPECTED_CFGS,
-                            None::<MultiSpan>,
-                            fluent::lint_builtin_unexpected_cli_config_value,
-                            |diag| {
-                                diag.help(fluent::help)
-                                    .set_arg("name", name)
-                                    .set_arg("value", value)
-                            },
-                        );
-                    }
-                }
+            if let Some(value) = value && let Some(values) = check_cfg.values_valid.get(&name) && !values.contains(&value) {
+                cx.emit_lint(
+                    UNEXPECTED_CFGS,
+                    BuiltinUnexpectedCliConfigValue { name, value },
+                );
             }
         }
     }
diff --git a/compiler/rustc_lint/src/context.rs b/compiler/rustc_lint/src/context.rs
index a16bb7f1a5f..c9b9a622571 100644
--- a/compiler/rustc_lint/src/context.rs
+++ b/compiler/rustc_lint/src/context.rs
@@ -965,6 +965,7 @@ pub trait LintContext: Sized {
     /// Note that this function should only be called for [`LintExpectationId`]s
     /// retrieved from the current lint pass. Buffered or manually created ids can
     /// cause ICEs.
+    #[rustc_lint_diagnostics]
     fn fulfill_expectation(&self, expectation: LintExpectationId) {
         // We need to make sure that submitted expectation ids are correctly fulfilled suppressed
         // and stored between compilation sessions. To not manually do these steps, we simply create
@@ -1011,6 +1012,7 @@ impl<'tcx> LintContext for LateContext<'tcx> {
         &*self.lint_store
     }
 
+    #[rustc_lint_diagnostics]
     fn lookup<S: Into<MultiSpan>>(
         &self,
         lint: &'static Lint,
@@ -1045,6 +1047,7 @@ impl LintContext for EarlyContext<'_> {
         self.builder.lint_store()
     }
 
+    #[rustc_lint_diagnostics]
     fn lookup<S: Into<MultiSpan>>(
         &self,
         lint: &'static Lint,
diff --git a/compiler/rustc_lint/src/deref_into_dyn_supertrait.rs b/compiler/rustc_lint/src/deref_into_dyn_supertrait.rs
index 1d29a234a3c..dff5a645c17 100644
--- a/compiler/rustc_lint/src/deref_into_dyn_supertrait.rs
+++ b/compiler/rustc_lint/src/deref_into_dyn_supertrait.rs
@@ -1,6 +1,8 @@
-use crate::{LateContext, LateLintPass, LintContext};
+use crate::{
+    lints::{SupertraitAsDerefTarget, SupertraitAsDerefTargetLabel},
+    LateContext, LateLintPass, LintContext,
+};
 
-use rustc_errors::DelayDm;
 use rustc_hir as hir;
 use rustc_middle::{traits::util::supertraits, ty};
 use rustc_span::sym;
@@ -71,22 +73,14 @@ impl<'tcx> LateLintPass<'tcx> for DerefIntoDynSupertrait {
             && supertraits(cx.tcx, t_principal.with_self_ty(cx.tcx, cx.tcx.types.trait_object_dummy_self))
                 .any(|sup| sup.map_bound(|x| ty::ExistentialTraitRef::erase_self_ty(cx.tcx, x)) == target_principal)
         {
-            cx.struct_span_lint(
-                DEREF_INTO_DYN_SUPERTRAIT,
-                cx.tcx.def_span(item.owner_id.def_id),
-                DelayDm(|| {
-                    format!(
-                        "`{t}` implements `Deref` with supertrait `{target_principal}` as target"
-                    )
-                }),
-                |lint| {
-                    if let Some(target_span) = impl_.items.iter().find_map(|i| (i.ident.name == sym::Target).then_some(i.span)) {
-                        lint.span_label(target_span, "target type is set here");
-                    }
-
-                    lint
-                },
-            )
+            let label = impl_.items.iter().find_map(|i| (i.ident.name == sym::Target).then_some(i.span)).map(|label| SupertraitAsDerefTargetLabel {
+                label,
+            });
+            cx.emit_spanned_lint(DEREF_INTO_DYN_SUPERTRAIT, cx.tcx.def_span(item.owner_id.def_id), SupertraitAsDerefTarget {
+                t,
+                target_principal: target_principal.to_string(),
+                label,
+            });
         }
     }
 }
diff --git a/compiler/rustc_lint/src/early.rs b/compiler/rustc_lint/src/early.rs
index d757471dcee..f9b2df49592 100644
--- a/compiler/rustc_lint/src/early.rs
+++ b/compiler/rustc_lint/src/early.rs
@@ -40,6 +40,7 @@ pub struct EarlyContextAndPass<'a, T: EarlyLintPass> {
 impl<'a, T: EarlyLintPass> EarlyContextAndPass<'a, T> {
     // This always-inlined function is for the hot call site.
     #[inline(always)]
+    #[allow(rustc::diagnostic_outside_of_impl)]
     fn inlined_check_id(&mut self, id: ast::NodeId) {
         for early_lint in self.context.buffered.take(id) {
             let BufferedEarlyLint { span, msg, node_id: _, lint_id, diagnostic } = early_lint;
diff --git a/compiler/rustc_lint/src/enum_intrinsics_non_enums.rs b/compiler/rustc_lint/src/enum_intrinsics_non_enums.rs
index f9d7466228a..73bd4173270 100644
--- a/compiler/rustc_lint/src/enum_intrinsics_non_enums.rs
+++ b/compiler/rustc_lint/src/enum_intrinsics_non_enums.rs
@@ -1,5 +1,8 @@
-use crate::{context::LintContext, LateContext, LateLintPass};
-use rustc_errors::fluent;
+use crate::{
+    context::LintContext,
+    lints::{EnumIntrinsicsMemDiscriminate, EnumIntrinsicsMemVariant},
+    LateContext, LateLintPass,
+};
 use rustc_hir as hir;
 use rustc_middle::ty::{visit::TypeVisitable, Ty};
 use rustc_span::{symbol::sym, Span};
@@ -50,11 +53,10 @@ fn enforce_mem_discriminant(
 ) {
     let ty_param = cx.typeck_results().node_substs(func_expr.hir_id).type_at(0);
     if is_non_enum(ty_param) {
-        cx.struct_span_lint(
+        cx.emit_spanned_lint(
             ENUM_INTRINSICS_NON_ENUMS,
             expr_span,
-            fluent::lint_enum_intrinsics_mem_discriminant,
-            |lint| lint.set_arg("ty_param", ty_param).span_note(args_span, fluent::note),
+            EnumIntrinsicsMemDiscriminate { ty_param, note: args_span },
         );
     }
 }
@@ -62,11 +64,10 @@ fn enforce_mem_discriminant(
 fn enforce_mem_variant_count(cx: &LateContext<'_>, func_expr: &hir::Expr<'_>, span: Span) {
     let ty_param = cx.typeck_results().node_substs(func_expr.hir_id).type_at(0);
     if is_non_enum(ty_param) {
-        cx.struct_span_lint(
+        cx.emit_spanned_lint(
             ENUM_INTRINSICS_NON_ENUMS,
             span,
-            fluent::lint_enum_intrinsics_mem_variant,
-            |lint| lint.set_arg("ty_param", ty_param).note(fluent::note),
+            EnumIntrinsicsMemVariant { ty_param },
         );
     }
 }
diff --git a/compiler/rustc_lint/src/errors.rs b/compiler/rustc_lint/src/errors.rs
index 1a769893f55..f3ae2609186 100644
--- a/compiler/rustc_lint/src/errors.rs
+++ b/compiler/rustc_lint/src/errors.rs
@@ -8,12 +8,12 @@ use rustc_span::{Span, Symbol};
 
 #[derive(Diagnostic)]
 #[diag(lint_overruled_attribute, code = "E0453")]
-pub struct OverruledAttribute {
+pub struct OverruledAttribute<'a> {
     #[primary_span]
     pub span: Span,
     #[label]
     pub overruled: Span,
-    pub lint_level: String,
+    pub lint_level: &'a str,
     pub lint_source: Symbol,
     #[subdiagnostic]
     pub sub: OverruledAttributeSub,
@@ -38,6 +38,7 @@ impl AddToDiagnostic for OverruledAttributeSub {
             OverruledAttributeSub::NodeSource { span, reason } => {
                 diag.span_label(span, fluent::lint_node_source);
                 if let Some(rationale) = reason {
+                    #[allow(rustc::untranslatable_diagnostic)]
                     diag.note(rationale.as_str());
                 }
             }
diff --git a/compiler/rustc_lint/src/expect.rs b/compiler/rustc_lint/src/expect.rs
index bf00ade5f0b..e9eb14ea188 100644
--- a/compiler/rustc_lint/src/expect.rs
+++ b/compiler/rustc_lint/src/expect.rs
@@ -1,8 +1,7 @@
-use crate::builtin;
-use rustc_errors::fluent;
-use rustc_hir::HirId;
+use crate::lints::{Expectation, ExpectationNote};
 use rustc_middle::ty::query::Providers;
-use rustc_middle::{lint::LintExpectation, ty::TyCtxt};
+use rustc_middle::ty::TyCtxt;
+use rustc_session::lint::builtin::UNFULFILLED_LINT_EXPECTATIONS;
 use rustc_session::lint::LintExpectationId;
 use rustc_span::symbol::sym;
 use rustc_span::Symbol;
@@ -28,34 +27,17 @@ fn check_expectations(tcx: TyCtxt<'_>, tool_filter: Option<Symbol>) {
             if !fulfilled_expectations.contains(&id)
                 && tool_filter.map_or(true, |filter| expectation.lint_tool == Some(filter))
             {
-                emit_unfulfilled_expectation_lint(tcx, *hir_id, expectation);
+                let rationale = expectation.reason.map(|rationale| ExpectationNote { rationale });
+                let note = expectation.is_unfulfilled_lint_expectations.then_some(());
+                tcx.emit_spanned_lint(
+                    UNFULFILLED_LINT_EXPECTATIONS,
+                    *hir_id,
+                    expectation.emission_span,
+                    Expectation { rationale, note },
+                );
             }
         } else {
             unreachable!("at this stage all `LintExpectationId`s are stable");
         }
     }
 }
-
-fn emit_unfulfilled_expectation_lint(
-    tcx: TyCtxt<'_>,
-    hir_id: HirId,
-    expectation: &LintExpectation,
-) {
-    tcx.struct_span_lint_hir(
-        builtin::UNFULFILLED_LINT_EXPECTATIONS,
-        hir_id,
-        expectation.emission_span,
-        fluent::lint_expectation,
-        |lint| {
-            if let Some(rationale) = expectation.reason {
-                lint.note(rationale.as_str());
-            }
-
-            if expectation.is_unfulfilled_lint_expectations {
-                lint.note(fluent::note);
-            }
-
-            lint
-        },
-    );
-}
diff --git a/compiler/rustc_lint/src/for_loops_over_fallibles.rs b/compiler/rustc_lint/src/for_loops_over_fallibles.rs
index 182734fa9fc..5219992ee94 100644
--- a/compiler/rustc_lint/src/for_loops_over_fallibles.rs
+++ b/compiler/rustc_lint/src/for_loops_over_fallibles.rs
@@ -1,7 +1,12 @@
-use crate::{LateContext, LateLintPass, LintContext};
+use crate::{
+    lints::{
+        ForLoopsOverFalliblesDiag, ForLoopsOverFalliblesLoopSub, ForLoopsOverFalliblesQuestionMark,
+        ForLoopsOverFalliblesSuggestion,
+    },
+    LateContext, LateLintPass, LintContext,
+};
 
 use hir::{Expr, Pat};
-use rustc_errors::{Applicability, DelayDm};
 use rustc_hir as hir;
 use rustc_infer::{infer::TyCtxtInferExt, traits::ObligationCause};
 use rustc_middle::ty::{self, List};
@@ -53,53 +58,29 @@ impl<'tcx> LateLintPass<'tcx> for ForLoopsOverFallibles {
             _ => return,
         };
 
-        let msg = DelayDm(|| {
-            format!(
-                "for loop over {article} `{ty}`. This is more readably written as an `if let` statement",
-            )
-        });
-
-        cx.struct_span_lint(FOR_LOOPS_OVER_FALLIBLES, arg.span, msg, |lint| {
-            if let Some(recv) = extract_iterator_next_call(cx, arg)
+        let sub =  if let Some(recv) = extract_iterator_next_call(cx, arg)
             && let Ok(recv_snip) = cx.sess().source_map().span_to_snippet(recv.span)
             {
-                lint.span_suggestion(
-                    recv.span.between(arg.span.shrink_to_hi()),
-                    format!("to iterate over `{recv_snip}` remove the call to `next`"),
-                    ".by_ref()",
-                    Applicability::MaybeIncorrect
-                );
+                ForLoopsOverFalliblesLoopSub::RemoveNext { suggestion: recv.span.between(arg.span.shrink_to_hi()), recv_snip }
             } else {
-                lint.multipart_suggestion_verbose(
-                    "to check pattern in a loop use `while let`",
-                    vec![
-                        // NB can't use `until` here because `expr.span` and `pat.span` have different syntax contexts
-                        (expr.span.with_hi(pat.span.lo()), format!("while let {var}(")),
-                        (pat.span.between(arg.span), ") = ".to_string()),
-                    ],
-                    Applicability::MaybeIncorrect
-                );
-            }
-
-            if suggest_question_mark(cx, adt, substs, expr.span) {
-                lint.span_suggestion(
-                    arg.span.shrink_to_hi(),
-                    "consider unwrapping the `Result` with `?` to iterate over its contents",
-                    "?",
-                    Applicability::MaybeIncorrect,
-                );
-            }
-
-            lint.multipart_suggestion_verbose(
-                "consider using `if let` to clear intent",
-                vec![
-                    // NB can't use `until` here because `expr.span` and `pat.span` have different syntax contexts
-                    (expr.span.with_hi(pat.span.lo()), format!("if let {var}(")),
-                    (pat.span.between(arg.span), ") = ".to_string()),
-                ],
-                Applicability::MaybeIncorrect,
-            )
-        })
+                ForLoopsOverFalliblesLoopSub::UseWhileLet { start_span: expr.span.with_hi(pat.span.lo()), end_span: pat.span.between(arg.span), var }
+            } ;
+        let question_mark = if suggest_question_mark(cx, adt, substs, expr.span) {
+            Some(ForLoopsOverFalliblesQuestionMark { suggestion: arg.span.shrink_to_hi() })
+        } else {
+            None
+        };
+        let suggestion = ForLoopsOverFalliblesSuggestion {
+            var,
+            start_span: expr.span.with_hi(pat.span.lo()),
+            end_span: pat.span.between(arg.span),
+        };
+
+        cx.emit_spanned_lint(
+            FOR_LOOPS_OVER_FALLIBLES,
+            arg.span,
+            ForLoopsOverFalliblesDiag { article, ty, sub, question_mark, suggestion },
+        );
     }
 }
 
diff --git a/compiler/rustc_lint/src/hidden_unicode_codepoints.rs b/compiler/rustc_lint/src/hidden_unicode_codepoints.rs
index dc2f5c0e296..7c1af6bee1d 100644
--- a/compiler/rustc_lint/src/hidden_unicode_codepoints.rs
+++ b/compiler/rustc_lint/src/hidden_unicode_codepoints.rs
@@ -1,7 +1,12 @@
-use crate::{EarlyContext, EarlyLintPass, LintContext};
+use crate::{
+    lints::{
+        HiddenUnicodeCodepointsDiag, HiddenUnicodeCodepointsDiagLabels,
+        HiddenUnicodeCodepointsDiagSub,
+    },
+    EarlyContext, EarlyLintPass, LintContext,
+};
 use ast::util::unicode::{contains_text_flow_control_chars, TEXT_FLOW_CONTROL_CHARS};
 use rustc_ast as ast;
-use rustc_errors::{fluent, Applicability, SuggestionStyle};
 use rustc_span::{BytePos, Span, Symbol};
 
 declare_lint! {
@@ -60,55 +65,19 @@ impl HiddenUnicodeCodepoints {
             })
             .collect();
 
-        cx.struct_span_lint(
+        let count = spans.len();
+        let labels = point_at_inner_spans
+            .then_some(HiddenUnicodeCodepointsDiagLabels { spans: spans.clone() });
+        let sub = if point_at_inner_spans && !spans.is_empty() {
+            HiddenUnicodeCodepointsDiagSub::Escape { spans }
+        } else {
+            HiddenUnicodeCodepointsDiagSub::NoEscape { spans }
+        };
+
+        cx.emit_spanned_lint(
             TEXT_DIRECTION_CODEPOINT_IN_LITERAL,
             span,
-            fluent::lint_hidden_unicode_codepoints,
-            |lint| {
-                lint.set_arg("label", label);
-                lint.set_arg("count", spans.len());
-                lint.span_label(span, fluent::label);
-                lint.note(fluent::note);
-                if point_at_inner_spans {
-                    for (c, span) in &spans {
-                        lint.span_label(*span, format!("{:?}", c));
-                    }
-                }
-                if point_at_inner_spans && !spans.is_empty() {
-                    lint.multipart_suggestion_with_style(
-                        fluent::suggestion_remove,
-                        spans.iter().map(|(_, span)| (*span, "".to_string())).collect(),
-                        Applicability::MachineApplicable,
-                        SuggestionStyle::HideCodeAlways,
-                    );
-                    lint.multipart_suggestion(
-                        fluent::suggestion_escape,
-                        spans
-                            .into_iter()
-                            .map(|(c, span)| {
-                                let c = format!("{:?}", c);
-                                (span, c[1..c.len() - 1].to_string())
-                            })
-                            .collect(),
-                        Applicability::MachineApplicable,
-                    );
-                } else {
-                    // FIXME: in other suggestions we've reversed the inner spans of doc comments. We
-                    // should do the same here to provide the same good suggestions as we do for
-                    // literals above.
-                    lint.set_arg(
-                        "escaped",
-                        spans
-                            .into_iter()
-                            .map(|(c, _)| format!("{:?}", c))
-                            .collect::<Vec<String>>()
-                            .join(", "),
-                    );
-                    lint.note(fluent::suggestion_remove);
-                    lint.note(fluent::no_suggestion_note_escape);
-                }
-                lint
-            },
+            HiddenUnicodeCodepointsDiag { label, count, span_label: span, labels, sub },
         );
     }
 }
diff --git a/compiler/rustc_lint/src/internal.rs b/compiler/rustc_lint/src/internal.rs
index 48902cd0569..5eb54cc0034 100644
--- a/compiler/rustc_lint/src/internal.rs
+++ b/compiler/rustc_lint/src/internal.rs
@@ -1,9 +1,12 @@
 //! Some lints that are only useful in the compiler or crates that use compiler internals, such as
 //! Clippy.
 
+use crate::lints::{
+    BadOptAccessDiag, DefaultHashTypesDiag, DiagOutOfImpl, LintPassByHand, NonExistantDocKeyword,
+    QueryInstability, TyQualified, TykindDiag, TykindKind, UntranslatableDiag,
+};
 use crate::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};
 use rustc_ast as ast;
-use rustc_errors::{fluent, Applicability};
 use rustc_hir::def::Res;
 use rustc_hir::{def_id::DefId, Expr, ExprKind, GenericArg, PatKind, Path, PathSegment, QPath};
 use rustc_hir::{HirId, Impl, Item, ItemKind, Node, Pat, Ty, TyKind};
@@ -29,20 +32,15 @@ impl LateLintPass<'_> for DefaultHashTypes {
             // don't lint imports, only actual usages
             return;
         }
-        let replace = match cx.tcx.get_diagnostic_name(def_id) {
+        let preferred = match cx.tcx.get_diagnostic_name(def_id) {
             Some(sym::HashMap) => "FxHashMap",
             Some(sym::HashSet) => "FxHashSet",
             _ => return,
         };
-        cx.struct_span_lint(
+        cx.emit_spanned_lint(
             DEFAULT_HASH_TYPES,
             path.span,
-            fluent::lint_default_hash_types,
-            |lint| {
-                lint.set_arg("preferred", replace)
-                    .set_arg("used", cx.tcx.item_name(def_id))
-                    .note(fluent::note)
-            },
+            DefaultHashTypesDiag { preferred, used: cx.tcx.item_name(def_id) },
         );
     }
 }
@@ -83,12 +81,11 @@ impl LateLintPass<'_> for QueryStability {
         if let Ok(Some(instance)) = ty::Instance::resolve(cx.tcx, cx.param_env, def_id, substs) {
             let def_id = instance.def_id();
             if cx.tcx.has_attr(def_id, sym::rustc_lint_query_instability) {
-                cx.struct_span_lint(
+                cx.emit_spanned_lint(
                     POTENTIAL_QUERY_INSTABILITY,
                     span,
-                    fluent::lint_query_instability,
-                    |lint| lint.set_arg("query", cx.tcx.item_name(def_id)).note(fluent::note),
-                )
+                    QueryInstability { query: cx.tcx.item_name(def_id) },
+                );
             }
         }
     }
@@ -126,14 +123,8 @@ impl<'tcx> LateLintPass<'tcx> for TyTyKind {
             let span = path.span.with_hi(
                 segment.args.map_or(segment.ident.span, |a| a.span_ext).hi()
             );
-            cx.struct_span_lint(USAGE_OF_TY_TYKIND, path.span, fluent::lint_tykind_kind, |lint| {
-                lint
-                    .span_suggestion(
-                        span,
-                        fluent::suggestion,
-                        "ty",
-                        Applicability::MaybeIncorrect, // ty maybe needs an import
-                    )
+            cx.emit_spanned_lint(USAGE_OF_TY_TYKIND, path.span, TykindKind {
+                suggestion: span,
             });
         }
     }
@@ -190,39 +181,17 @@ impl<'tcx> LateLintPass<'tcx> for TyTyKind {
 
                     match span {
                         Some(span) => {
-                            cx.struct_span_lint(
-                                USAGE_OF_TY_TYKIND,
-                                path.span,
-                                fluent::lint_tykind_kind,
-                                |lint| lint.span_suggestion(
-                                    span,
-                                    fluent::suggestion,
-                                    "ty",
-                                    Applicability::MaybeIncorrect, // ty maybe needs an import
-                                )
-                            )
+                            cx.emit_spanned_lint(USAGE_OF_TY_TYKIND, path.span, TykindKind {
+                                suggestion: span,
+                            });
                         },
-                        None => cx.struct_span_lint(
-                            USAGE_OF_TY_TYKIND,
-                            path.span,
-                            fluent::lint_tykind,
-                            |lint| lint.help(fluent::help)
-                        )
-                    }
-                } else if !ty.span.from_expansion() && let Some(t) = is_ty_or_ty_ctxt(cx, &path) {
-                    if path.segments.len() > 1 {
-                        cx.struct_span_lint(USAGE_OF_QUALIFIED_TY, path.span, fluent::lint_ty_qualified, |lint| {
-                            lint
-                                .set_arg("ty", t.clone())
-                                .span_suggestion(
-                                    path.span,
-                                    fluent::suggestion,
-                                    t,
-                                    // The import probably needs to be changed
-                                    Applicability::MaybeIncorrect,
-                                )
-                        })
+                        None => cx.emit_spanned_lint(USAGE_OF_TY_TYKIND, path.span, TykindDiag),
                     }
+                } else if !ty.span.from_expansion() && path.segments.len() > 1 && let Some(t) = is_ty_or_ty_ctxt(cx, &path) {
+                    cx.emit_spanned_lint(USAGE_OF_QUALIFIED_TY, path.span, TyQualified {
+                        ty: t.clone(),
+                        suggestion: path.span,
+                    });
                 }
             }
             _ => {}
@@ -303,12 +272,11 @@ impl EarlyLintPass for LintPassImpl {
                         && call_site.ctxt().outer_expn_data().kind
                             != ExpnKind::Macro(MacroKind::Bang, sym::declare_lint_pass)
                     {
-                        cx.struct_span_lint(
+                        cx.emit_spanned_lint(
                             LINT_PASS_IMPL_WITHOUT_MACRO,
                             lint_pass.path.span,
-                            fluent::lint_lintpass_by_hand,
-                            |lint| lint.help(fluent::help),
-                        )
+                            LintPassByHand,
+                        );
                     }
                 }
             }
@@ -338,17 +306,16 @@ impl<'tcx> LateLintPass<'tcx> for ExistingDocKeyword {
             if let Some(list) = attr.meta_item_list() {
                 for nested in list {
                     if nested.has_name(sym::keyword) {
-                        let v = nested
+                        let keyword = nested
                             .value_str()
                             .expect("#[doc(keyword = \"...\")] expected a value!");
-                        if is_doc_keyword(v) {
+                        if is_doc_keyword(keyword) {
                             return;
                         }
-                        cx.struct_span_lint(
+                        cx.emit_spanned_lint(
                             EXISTING_DOC_KEYWORD,
                             attr.span,
-                            fluent::lint_non_existant_doc_keyword,
-                            |lint| lint.set_arg("keyword", v).help(fluent::help),
+                            NonExistantDocKeyword { keyword },
                         );
                     }
                 }
@@ -407,12 +374,7 @@ impl LateLintPass<'_> for Diagnostics {
         }
         debug!(?found_impl);
         if !found_parent_with_attr && !found_impl {
-            cx.struct_span_lint(
-                DIAGNOSTIC_OUTSIDE_OF_IMPL,
-                span,
-                fluent::lint_diag_out_of_impl,
-                |lint| lint,
-            )
+            cx.emit_spanned_lint(DIAGNOSTIC_OUTSIDE_OF_IMPL, span, DiagOutOfImpl);
         }
 
         let mut found_diagnostic_message = false;
@@ -428,12 +390,7 @@ impl LateLintPass<'_> for Diagnostics {
         }
         debug!(?found_diagnostic_message);
         if !found_parent_with_attr && !found_diagnostic_message {
-            cx.struct_span_lint(
-                UNTRANSLATABLE_DIAGNOSTIC,
-                span,
-                fluent::lint_untranslatable_diag,
-                |lint| lint,
-            )
+            cx.emit_spanned_lint(UNTRANSLATABLE_DIAGNOSTIC, span, UntranslatableDiag);
         }
     }
 }
@@ -465,9 +422,9 @@ impl LateLintPass<'_> for BadOptAccess {
                 let Some(lit) = item.lit()  &&
                 let ast::LitKind::Str(val, _) = lit.kind
             {
-                cx.struct_span_lint(BAD_OPT_ACCESS, expr.span, val.as_str(), |lint|
-                    lint
-                );
+                cx.emit_spanned_lint(BAD_OPT_ACCESS, expr.span, BadOptAccessDiag {
+                    msg: val.as_str(),
+                });
             }
         }
     }
diff --git a/compiler/rustc_lint/src/let_underscore.rs b/compiler/rustc_lint/src/let_underscore.rs
index 04d844d21dc..b83a9665fc0 100644
--- a/compiler/rustc_lint/src/let_underscore.rs
+++ b/compiler/rustc_lint/src/let_underscore.rs
@@ -1,5 +1,8 @@
-use crate::{LateContext, LateLintPass, LintContext};
-use rustc_errors::{Applicability, DiagnosticBuilder, MultiSpan};
+use crate::{
+    lints::{NonBindingLet, NonBindingLetSub},
+    LateContext, LateLintPass, LintContext,
+};
+use rustc_errors::MultiSpan;
 use rustc_hir as hir;
 use rustc_middle::ty;
 use rustc_span::Symbol;
@@ -119,6 +122,11 @@ impl<'tcx> LateLintPass<'tcx> for LetUnderscore {
                 _ => false,
             };
 
+            let sub = NonBindingLetSub {
+                suggestion: local.pat.span,
+                multi_suggestion_start: local.span.until(init.span),
+                multi_suggestion_end: init.span.shrink_to_hi(),
+            };
             if is_sync_lock {
                 let mut span = MultiSpan::from_spans(vec![local.pat.span, init.span]);
                 span.push_span_label(
@@ -129,41 +137,14 @@ impl<'tcx> LateLintPass<'tcx> for LetUnderscore {
                     init.span,
                     "this binding will immediately drop the value assigned to it".to_string(),
                 );
-                cx.struct_span_lint(
-                    LET_UNDERSCORE_LOCK,
-                    span,
-                    "non-binding let on a synchronization lock",
-                    |lint| build_lint(lint, local, init.span),
-                )
+                cx.emit_spanned_lint(LET_UNDERSCORE_LOCK, span, NonBindingLet::SyncLock { sub });
             } else {
-                cx.struct_span_lint(
+                cx.emit_spanned_lint(
                     LET_UNDERSCORE_DROP,
                     local.span,
-                    "non-binding let on a type that implements `Drop`",
-                    |lint| build_lint(lint, local, init.span),
-                )
+                    NonBindingLet::DropType { sub },
+                );
             }
         }
     }
 }
-
-fn build_lint<'a, 'b>(
-    lint: &'a mut DiagnosticBuilder<'b, ()>,
-    local: &hir::Local<'_>,
-    init_span: rustc_span::Span,
-) -> &'a mut DiagnosticBuilder<'b, ()> {
-    lint.span_suggestion_verbose(
-        local.pat.span,
-        "consider binding to an unused variable to avoid immediately dropping the value",
-        "_unused",
-        Applicability::MachineApplicable,
-    )
-    .multipart_suggestion(
-        "consider immediately dropping the value",
-        vec![
-            (local.span.until(init_span), "drop(".to_string()),
-            (init_span.shrink_to_hi(), ")".to_string()),
-        ],
-        Applicability::MachineApplicable,
-    )
-}
diff --git a/compiler/rustc_lint/src/levels.rs b/compiler/rustc_lint/src/levels.rs
index e9d3d44a3f9..09dfb1022d8 100644
--- a/compiler/rustc_lint/src/levels.rs
+++ b/compiler/rustc_lint/src/levels.rs
@@ -1,9 +1,13 @@
 use crate::context::{CheckLintNameResult, LintStore};
 use crate::late::unerased_lint_store;
+use crate::lints::{
+    DeprecatedLintName, IgnoredUnlessCrateSpecified, OverruledAtributeLint, RenamedOrRemovedLint,
+    RenamedOrRemovedLintSuggestion, UnknownLint, UnknownLintSuggestion,
+};
 use rustc_ast as ast;
 use rustc_ast_pretty::pprust;
 use rustc_data_structures::fx::FxHashMap;
-use rustc_errors::{Applicability, Diagnostic, DiagnosticBuilder, DiagnosticMessage, MultiSpan};
+use rustc_errors::{fluent, DecorateLint, DiagnosticBuilder, DiagnosticMessage, MultiSpan};
 use rustc_hir as hir;
 use rustc_hir::intravisit::{self, Visitor};
 use rustc_hir::HirId;
@@ -15,6 +19,7 @@ use rustc_middle::lint::{
 };
 use rustc_middle::ty::query::Providers;
 use rustc_middle::ty::{RegisteredTools, TyCtxt};
+use rustc_session::lint::builtin::{RENAMED_AND_REMOVED_LINTS, UNKNOWN_LINTS, UNUSED_ATTRIBUTES};
 use rustc_session::lint::{
     builtin::{self, FORBIDDEN_LINT_GROUPS, SINGLE_USE_LIFETIMES, UNFULFILLED_LINT_EXPECTATIONS},
     Level, Lint, LintExpectationId, LintId,
@@ -583,57 +588,32 @@ impl<'s, P: LintLevelsProvider> LintLevelsBuilder<'s, P> {
                     old_src,
                     id_name
                 );
-
-                let decorate_diag = |diag: &mut Diagnostic| {
-                    diag.span_label(src.span(), "overruled by previous forbid");
-                    match old_src {
-                        LintLevelSource::Default => {
-                            diag.note(&format!(
-                                "`forbid` lint level is the default for {}",
-                                id.to_string()
-                            ));
-                        }
-                        LintLevelSource::Node { span, reason, .. } => {
-                            diag.span_label(span, "`forbid` level set here");
-                            if let Some(rationale) = reason {
-                                diag.note(rationale.as_str());
-                            }
-                        }
-                        LintLevelSource::CommandLine(_, _) => {
-                            diag.note("`forbid` lint level was set on command line");
-                        }
+                let sub = match old_src {
+                    LintLevelSource::Default => {
+                        OverruledAttributeSub::DefaultSource { id: id.to_string() }
                     }
+                    LintLevelSource::Node { span, reason, .. } => {
+                        OverruledAttributeSub::NodeSource { span, reason }
+                    }
+                    LintLevelSource::CommandLine(_, _) => OverruledAttributeSub::CommandLineSource,
                 };
                 if !fcw_warning {
                     self.sess.emit_err(OverruledAttribute {
                         span: src.span(),
                         overruled: src.span(),
-                        lint_level: level.as_str().to_string(),
+                        lint_level: level.as_str(),
                         lint_source: src.name(),
-                        sub: match old_src {
-                            LintLevelSource::Default => {
-                                OverruledAttributeSub::DefaultSource { id: id.to_string() }
-                            }
-                            LintLevelSource::Node { span, reason, .. } => {
-                                OverruledAttributeSub::NodeSource { span, reason }
-                            }
-                            LintLevelSource::CommandLine(_, _) => {
-                                OverruledAttributeSub::CommandLineSource
-                            }
-                        },
+                        sub,
                     });
                 } else {
-                    self.struct_lint(
+                    self.emit_spanned_lint(
                         FORBIDDEN_LINT_GROUPS,
-                        Some(src.span().into()),
-                        format!(
-                            "{}({}) incompatible with previous forbid",
-                            level.as_str(),
-                            src.name(),
-                        ),
-                        |lint| {
-                            decorate_diag(lint);
-                            lint
+                        src.span().into(),
+                        OverruledAtributeLint {
+                            overruled: src.span(),
+                            lint_level: level.as_str(),
+                            lint_source: src.name(),
+                            sub,
                         },
                     );
                 }
@@ -858,25 +838,13 @@ impl<'s, P: LintLevelsProvider> LintLevelsBuilder<'s, P> {
                             }
                             Err((Some(ids), ref new_lint_name)) => {
                                 let lint = builtin::RENAMED_AND_REMOVED_LINTS;
-                                let (lvl, src) = self.provider.get_lint_level(lint, &sess);
-                                struct_lint_level(
-                                    self.sess,
+                                self.emit_spanned_lint(
                                     lint,
-                                    lvl,
-                                    src,
-                                    Some(sp.into()),
-                                    format!(
-                                        "lint name `{}` is deprecated \
-                                         and may not have an effect in the future.",
-                                        name
-                                    ),
-                                    |lint| {
-                                        lint.span_suggestion(
-                                            sp,
-                                            "change it to",
-                                            new_lint_name,
-                                            Applicability::MachineApplicable,
-                                        )
+                                    sp.into(),
+                                    DeprecatedLintName {
+                                        name,
+                                        suggestion: sp,
+                                        replace: &new_lint_name,
                                     },
                                 );
 
@@ -917,54 +885,29 @@ impl<'s, P: LintLevelsProvider> LintLevelsBuilder<'s, P> {
                     _ if !self.warn_about_weird_lints => {}
 
                     CheckLintNameResult::Warning(msg, renamed) => {
-                        let lint = builtin::RENAMED_AND_REMOVED_LINTS;
-                        let (renamed_lint_level, src) = self.provider.get_lint_level(lint, &sess);
-                        struct_lint_level(
-                            self.sess,
-                            lint,
-                            renamed_lint_level,
-                            src,
-                            Some(sp.into()),
-                            msg,
-                            |lint| {
-                                if let Some(new_name) = &renamed {
-                                    lint.span_suggestion(
-                                        sp,
-                                        "use the new name",
-                                        new_name,
-                                        Applicability::MachineApplicable,
-                                    );
-                                }
-                                lint
-                            },
+                        let suggestion =
+                            renamed.as_ref().map(|replace| RenamedOrRemovedLintSuggestion {
+                                suggestion: sp,
+                                replace: replace.as_str(),
+                            });
+                        self.emit_spanned_lint(
+                            RENAMED_AND_REMOVED_LINTS,
+                            sp.into(),
+                            RenamedOrRemovedLint { msg, suggestion },
                         );
                     }
                     CheckLintNameResult::NoLint(suggestion) => {
-                        let lint = builtin::UNKNOWN_LINTS;
-                        let (level, src) = self.provider.get_lint_level(lint, self.sess);
                         let name = if let Some(tool_ident) = tool_ident {
                             format!("{}::{}", tool_ident.name, name)
                         } else {
                             name.to_string()
                         };
-                        struct_lint_level(
-                            self.sess,
-                            lint,
-                            level,
-                            src,
-                            Some(sp.into()),
-                            format!("unknown lint: `{}`", name),
-                            |lint| {
-                                if let Some(suggestion) = suggestion {
-                                    lint.span_suggestion(
-                                        sp,
-                                        "did you mean",
-                                        suggestion,
-                                        Applicability::MaybeIncorrect,
-                                    );
-                                }
-                                lint
-                            },
+                        let suggestion = suggestion
+                            .map(|replace| UnknownLintSuggestion { suggestion: sp, replace });
+                        self.emit_spanned_lint(
+                            UNKNOWN_LINTS,
+                            sp.into(),
+                            UnknownLint { name, suggestion },
                         );
                     }
                 }
@@ -1010,20 +953,10 @@ impl<'s, P: LintLevelsProvider> LintLevelsBuilder<'s, P> {
                     continue
                 };
 
-                let lint = builtin::UNUSED_ATTRIBUTES;
-                let (lint_level, lint_src) = self.provider.get_lint_level(lint, &self.sess);
-                struct_lint_level(
-                    self.sess,
-                    lint,
-                    lint_level,
-                    lint_src,
-                    Some(lint_attr_span.into()),
-                    format!(
-                        "{}({}) is ignored unless specified at crate level",
-                        level.as_str(),
-                        lint_attr_name
-                    ),
-                    |lint| lint,
+                self.emit_spanned_lint(
+                    UNUSED_ATTRIBUTES,
+                    lint_attr_span.into(),
+                    IgnoredUnlessCrateSpecified { level: level.as_str(), name: lint_attr_name },
                 );
                 // don't set a separate error for every lint in the group
                 break;
@@ -1047,11 +980,10 @@ impl<'s, P: LintLevelsProvider> LintLevelsBuilder<'s, P> {
                     level,
                     src,
                     Some(span.into()),
-                    format!("unknown lint: `{}`", lint_id.lint.name_lower()),
+                    fluent::lint_unknown_gated_lint,
                     |lint| {
-                        lint.note(
-                            &format!("the `{}` lint is unstable", lint_id.lint.name_lower(),),
-                        );
+                        lint.set_arg("name", lint_id.lint.name_lower());
+                        lint.note(fluent::note);
                         add_feature_diagnostics(lint, &self.sess.parse_sess, feature);
                         lint
                     },
@@ -1086,6 +1018,25 @@ impl<'s, P: LintLevelsProvider> LintLevelsBuilder<'s, P> {
         let (level, src) = self.lint_level(lint);
         struct_lint_level(self.sess, lint, level, src, span, msg, decorate)
     }
+
+    pub fn emit_spanned_lint(
+        &self,
+        lint: &'static Lint,
+        span: MultiSpan,
+        decorate: impl for<'a> DecorateLint<'a, ()>,
+    ) {
+        let (level, src) = self.lint_level(lint);
+        struct_lint_level(self.sess, lint, level, src, Some(span), decorate.msg(), |lint| {
+            decorate.decorate_lint(lint)
+        });
+    }
+
+    pub fn emit_lint(&self, lint: &'static Lint, decorate: impl for<'a> DecorateLint<'a, ()>) {
+        let (level, src) = self.lint_level(lint);
+        struct_lint_level(self.sess, lint, level, src, None, decorate.msg(), |lint| {
+            decorate.decorate_lint(lint)
+        });
+    }
 }
 
 pub(crate) fn provide(providers: &mut Providers) {
diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs
index 1275d6f223c..3d818154cb9 100644
--- a/compiler/rustc_lint/src/lib.rs
+++ b/compiler/rustc_lint/src/lib.rs
@@ -38,6 +38,8 @@
 #![feature(never_type)]
 #![feature(rustc_attrs)]
 #![recursion_limit = "256"]
+#![deny(rustc::untranslatable_diagnostic)]
+#![deny(rustc::diagnostic_outside_of_impl)]
 
 #[macro_use]
 extern crate rustc_middle;
@@ -60,6 +62,7 @@ mod internal;
 mod late;
 mod let_underscore;
 mod levels;
+mod lints;
 mod methods;
 mod non_ascii_idents;
 mod non_fmt_panic;
diff --git a/compiler/rustc_lint/src/lints.rs b/compiler/rustc_lint/src/lints.rs
new file mode 100644
index 00000000000..c3782a49689
--- /dev/null
+++ b/compiler/rustc_lint/src/lints.rs
@@ -0,0 +1,1479 @@
+#![allow(rustc::untranslatable_diagnostic)]
+#![allow(rustc::diagnostic_outside_of_impl)]
+use std::num::NonZeroU32;
+
+use rustc_errors::{
+    fluent, AddToDiagnostic, Applicability, DecorateLint, DiagnosticMessage,
+    DiagnosticStyledString, SuggestionStyle,
+};
+use rustc_hir::def_id::DefId;
+use rustc_macros::{LintDiagnostic, Subdiagnostic};
+use rustc_middle::ty::{Predicate, Ty, TyCtxt};
+use rustc_session::parse::ParseSess;
+use rustc_span::{edition::Edition, sym, symbol::Ident, Span, Symbol};
+
+use crate::{
+    builtin::InitError, builtin::TypeAliasBounds, errors::OverruledAttributeSub, LateContext,
+};
+
+// array_into_iter.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_array_into_iter)]
+pub struct ArrayIntoIterDiag<'a> {
+    pub target: &'a str,
+    #[suggestion(use_iter_suggestion, code = "iter", applicability = "machine-applicable")]
+    pub suggestion: Span,
+    #[subdiagnostic]
+    pub sub: Option<ArrayIntoIterDiagSub>,
+}
+
+#[derive(Subdiagnostic)]
+pub enum ArrayIntoIterDiagSub {
+    #[suggestion(remove_into_iter_suggestion, code = "", applicability = "maybe-incorrect")]
+    RemoveIntoIter {
+        #[primary_span]
+        span: Span,
+    },
+    #[multipart_suggestion(use_explicit_into_iter_suggestion, applicability = "maybe-incorrect")]
+    UseExplicitIntoIter {
+        #[suggestion_part(code = "IntoIterator::into_iter(")]
+        start_span: Span,
+        #[suggestion_part(code = ")")]
+        end_span: Span,
+    },
+}
+
+// builtin.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_while_true)]
+pub struct BuiltinWhileTrue {
+    #[suggestion(style = "short", code = "{replace}", applicability = "machine-applicable")]
+    pub suggestion: Span,
+    pub replace: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_box_pointers)]
+pub struct BuiltinBoxPointers<'a> {
+    pub ty: Ty<'a>,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_non_shorthand_field_patterns)]
+pub struct BuiltinNonShorthandFieldPatterns {
+    pub ident: Ident,
+    #[suggestion(code = "{prefix}{ident}", applicability = "machine-applicable")]
+    pub suggestion: Span,
+    pub prefix: &'static str,
+}
+
+#[derive(LintDiagnostic)]
+pub enum BuiltinUnsafe {
+    #[diag(lint_builtin_allow_internal_unsafe)]
+    AllowInternalUnsafe,
+    #[diag(lint_builtin_unsafe_block)]
+    UnsafeBlock,
+    #[diag(lint_builtin_unsafe_trait)]
+    UnsafeTrait,
+    #[diag(lint_builtin_unsafe_impl)]
+    UnsafeImpl,
+    #[diag(lint_builtin_no_mangle_fn)]
+    #[note(lint_builtin_overridden_symbol_name)]
+    NoMangleFn,
+    #[diag(lint_builtin_export_name_fn)]
+    #[note(lint_builtin_overridden_symbol_name)]
+    ExportNameFn,
+    #[diag(lint_builtin_link_section_fn)]
+    #[note(lint_builtin_overridden_symbol_section)]
+    LinkSectionFn,
+    #[diag(lint_builtin_no_mangle_static)]
+    #[note(lint_builtin_overridden_symbol_name)]
+    NoMangleStatic,
+    #[diag(lint_builtin_export_name_static)]
+    #[note(lint_builtin_overridden_symbol_name)]
+    ExportNameStatic,
+    #[diag(lint_builtin_link_section_static)]
+    #[note(lint_builtin_overridden_symbol_section)]
+    LinkSectionStatic,
+    #[diag(lint_builtin_no_mangle_method)]
+    #[note(lint_builtin_overridden_symbol_name)]
+    NoMangleMethod,
+    #[diag(lint_builtin_export_name_method)]
+    #[note(lint_builtin_overridden_symbol_name)]
+    ExportNameMethod,
+    #[diag(lint_builtin_decl_unsafe_fn)]
+    DeclUnsafeFn,
+    #[diag(lint_builtin_decl_unsafe_method)]
+    DeclUnsafeMethod,
+    #[diag(lint_builtin_impl_unsafe_method)]
+    ImplUnsafeMethod,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_missing_doc)]
+pub struct BuiltinMissingDoc<'a> {
+    pub article: &'a str,
+    pub desc: &'a str,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_missing_copy_impl)]
+pub struct BuiltinMissingCopyImpl;
+
+pub struct BuiltinMissingDebugImpl<'a> {
+    pub tcx: TyCtxt<'a>,
+    pub def_id: DefId,
+}
+
+// Needed for def_path_str
+impl<'a> DecorateLint<'a, ()> for BuiltinMissingDebugImpl<'_> {
+    fn decorate_lint<'b>(
+        self,
+        diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
+    ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
+        diag.set_arg("debug", self.tcx.def_path_str(self.def_id));
+        diag
+    }
+
+    fn msg(&self) -> DiagnosticMessage {
+        fluent::lint_builtin_missing_debug_impl
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_anonymous_params)]
+pub struct BuiltinAnonymousParams<'a> {
+    #[suggestion(code = "_: {ty_snip}")]
+    pub suggestion: (Span, Applicability),
+    pub ty_snip: &'a str,
+}
+
+// FIXME(davidtwco) translatable deprecated attr
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_deprecated_attr_link)]
+pub struct BuiltinDeprecatedAttrLink<'a> {
+    pub name: Symbol,
+    pub reason: &'a str,
+    pub link: &'a str,
+    #[subdiagnostic]
+    pub suggestion: BuiltinDeprecatedAttrLinkSuggestion<'a>,
+}
+
+#[derive(Subdiagnostic)]
+pub enum BuiltinDeprecatedAttrLinkSuggestion<'a> {
+    #[suggestion(msg_suggestion, code = "", applicability = "machine-applicable")]
+    Msg {
+        #[primary_span]
+        suggestion: Span,
+        msg: &'a str,
+    },
+    #[suggestion(default_suggestion, code = "", applicability = "machine-applicable")]
+    Default {
+        #[primary_span]
+        suggestion: Span,
+    },
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_deprecated_attr_used)]
+pub struct BuiltinDeprecatedAttrUsed {
+    pub name: String,
+    #[suggestion(
+        lint_builtin_deprecated_attr_default_suggestion,
+        style = "short",
+        code = "",
+        applicability = "machine-applicable"
+    )]
+    pub suggestion: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_unused_doc_comment)]
+pub struct BuiltinUnusedDocComment<'a> {
+    pub kind: &'a str,
+    #[label]
+    pub label: Span,
+    #[subdiagnostic]
+    pub sub: BuiltinUnusedDocCommentSub,
+}
+
+#[derive(Subdiagnostic)]
+pub enum BuiltinUnusedDocCommentSub {
+    #[help(plain_help)]
+    PlainHelp,
+    #[help(block_help)]
+    BlockHelp,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_no_mangle_generic)]
+pub struct BuiltinNoMangleGeneric {
+    // Use of `#[no_mangle]` suggests FFI intent; correct
+    // fix may be to monomorphize source by hand
+    #[suggestion(style = "short", code = "", applicability = "maybe-incorrect")]
+    pub suggestion: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_const_no_mangle)]
+pub struct BuiltinConstNoMangle {
+    #[suggestion(code = "pub static", applicability = "machine-applicable")]
+    pub suggestion: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_mutable_transmutes)]
+pub struct BuiltinMutablesTransmutes;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_unstable_features)]
+pub struct BuiltinUnstableFeatures;
+
+// lint_ungated_async_fn_track_caller
+pub struct BuiltinUngatedAsyncFnTrackCaller<'a> {
+    pub label: Span,
+    pub parse_sess: &'a ParseSess,
+}
+
+impl<'a> DecorateLint<'a, ()> for BuiltinUngatedAsyncFnTrackCaller<'_> {
+    fn decorate_lint<'b>(
+        self,
+        diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
+    ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
+        diag.span_label(self.label, fluent::label);
+        rustc_session::parse::add_feature_diagnostics(
+            diag,
+            &self.parse_sess,
+            sym::closure_track_caller,
+        );
+        diag
+    }
+
+    fn msg(&self) -> DiagnosticMessage {
+        fluent::lint_ungated_async_fn_track_caller
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_unreachable_pub)]
+pub struct BuiltinUnreachablePub<'a> {
+    pub what: &'a str,
+    #[suggestion(code = "pub(crate)")]
+    pub suggestion: (Span, Applicability),
+    #[help]
+    pub help: Option<()>,
+}
+
+pub struct SuggestChangingAssocTypes<'a, 'b> {
+    pub ty: &'a rustc_hir::Ty<'b>,
+}
+
+impl AddToDiagnostic for SuggestChangingAssocTypes<'_, '_> {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        // Access to associates types should use `<T as Bound>::Assoc`, which does not need a
+        // bound.  Let's see if this type does that.
+
+        // We use a HIR visitor to walk the type.
+        use rustc_hir::intravisit::{self, Visitor};
+        struct WalkAssocTypes<'a> {
+            err: &'a mut rustc_errors::Diagnostic,
+        }
+        impl Visitor<'_> for WalkAssocTypes<'_> {
+            fn visit_qpath(
+                &mut self,
+                qpath: &rustc_hir::QPath<'_>,
+                id: rustc_hir::HirId,
+                span: Span,
+            ) {
+                if TypeAliasBounds::is_type_variable_assoc(qpath) {
+                    self.err.span_help(span, fluent::lint_builtin_type_alias_bounds_help);
+                }
+                intravisit::walk_qpath(self, qpath, id)
+            }
+        }
+
+        // Let's go for a walk!
+        let mut visitor = WalkAssocTypes { err: diag };
+        visitor.visit_ty(self.ty);
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_type_alias_where_clause)]
+pub struct BuiltinTypeAliasWhereClause<'a, 'b> {
+    #[suggestion(code = "", applicability = "machine-applicable")]
+    pub suggestion: Span,
+    #[subdiagnostic]
+    pub sub: Option<SuggestChangingAssocTypes<'a, 'b>>,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_type_alias_generic_bounds)]
+pub struct BuiltinTypeAliasGenericBounds<'a, 'b> {
+    #[subdiagnostic]
+    pub suggestion: BuiltinTypeAliasGenericBoundsSuggestion,
+    #[subdiagnostic]
+    pub sub: Option<SuggestChangingAssocTypes<'a, 'b>>,
+}
+
+pub struct BuiltinTypeAliasGenericBoundsSuggestion {
+    pub suggestions: Vec<(Span, String)>,
+}
+
+impl AddToDiagnostic for BuiltinTypeAliasGenericBoundsSuggestion {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        diag.multipart_suggestion(
+            fluent::suggestion,
+            self.suggestions,
+            Applicability::MachineApplicable,
+        );
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_trivial_bounds)]
+pub struct BuiltinTrivialBounds<'a> {
+    pub predicate_kind_name: &'a str,
+    pub predicate: Predicate<'a>,
+}
+
+#[derive(LintDiagnostic)]
+pub enum BuiltinEllipsisInclusiveRangePatternsLint {
+    #[diag(lint_builtin_ellipsis_inclusive_range_patterns)]
+    Parenthesise {
+        #[suggestion(code = "{replace}", applicability = "machine-applicable")]
+        suggestion: Span,
+        replace: String,
+    },
+    #[diag(lint_builtin_ellipsis_inclusive_range_patterns)]
+    NonParenthesise {
+        #[suggestion(style = "short", code = "..=", applicability = "machine-applicable")]
+        suggestion: Span,
+    },
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_unnameable_test_items)]
+pub struct BuiltinUnnameableTestItems;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_keyword_idents)]
+pub struct BuiltinKeywordIdents {
+    pub kw: Ident,
+    pub next: Edition,
+    #[suggestion(code = "r#{kw}", applicability = "machine-applicable")]
+    pub suggestion: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_explicit_outlives)]
+pub struct BuiltinExplicitOutlives {
+    pub count: usize,
+    #[subdiagnostic]
+    pub suggestion: BuiltinExplicitOutlivesSuggestion,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(suggestion)]
+pub struct BuiltinExplicitOutlivesSuggestion {
+    #[suggestion_part(code = "")]
+    pub spans: Vec<Span>,
+    #[applicability]
+    pub applicability: Applicability,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_incomplete_features)]
+pub struct BuiltinIncompleteFeatures {
+    pub name: Symbol,
+    #[subdiagnostic]
+    pub note: Option<BuiltinIncompleteFeaturesNote>,
+    #[subdiagnostic]
+    pub help: Option<BuiltinIncompleteFeaturesHelp>,
+}
+
+#[derive(Subdiagnostic)]
+#[help(help)]
+pub struct BuiltinIncompleteFeaturesHelp;
+
+#[derive(Subdiagnostic)]
+#[note(note)]
+pub struct BuiltinIncompleteFeaturesNote {
+    pub n: NonZeroU32,
+}
+
+pub struct BuiltinUnpermittedTypeInit<'a> {
+    pub msg: DiagnosticMessage,
+    pub ty: Ty<'a>,
+    pub label: Span,
+    pub sub: BuiltinUnpermittedTypeInitSub,
+}
+
+impl<'a> DecorateLint<'a, ()> for BuiltinUnpermittedTypeInit<'_> {
+    fn decorate_lint<'b>(
+        self,
+        diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
+    ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
+        diag.set_arg("ty", self.ty);
+        diag.span_label(self.label, fluent::lint_builtin_unpermitted_type_init_label);
+        diag.span_label(self.label, fluent::lint_builtin_unpermitted_type_init_label_suggestion);
+        self.sub.add_to_diagnostic(diag);
+        diag
+    }
+
+    fn msg(&self) -> rustc_errors::DiagnosticMessage {
+        self.msg.clone()
+    }
+}
+
+// FIXME(davidtwco): make translatable
+pub struct BuiltinUnpermittedTypeInitSub {
+    pub err: InitError,
+}
+
+impl AddToDiagnostic for BuiltinUnpermittedTypeInitSub {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        let mut err = self.err;
+        loop {
+            if let Some(span) = err.span {
+                diag.span_note(span, err.message);
+            } else {
+                diag.note(err.message);
+            }
+            if let Some(e) = err.nested {
+                err = *e;
+            } else {
+                break;
+            }
+        }
+    }
+}
+
+#[derive(LintDiagnostic)]
+pub enum BuiltinClashingExtern<'a> {
+    #[diag(lint_builtin_clashing_extern_same_name)]
+    SameName {
+        this: Symbol,
+        orig: Symbol,
+        #[label(previous_decl_label)]
+        previous_decl_label: Span,
+        #[label(mismatch_label)]
+        mismatch_label: Span,
+        #[subdiagnostic]
+        sub: BuiltinClashingExternSub<'a>,
+    },
+    #[diag(lint_builtin_clashing_extern_diff_name)]
+    DiffName {
+        this: Symbol,
+        orig: Symbol,
+        #[label(previous_decl_label)]
+        previous_decl_label: Span,
+        #[label(mismatch_label)]
+        mismatch_label: Span,
+        #[subdiagnostic]
+        sub: BuiltinClashingExternSub<'a>,
+    },
+}
+
+// FIXME(davidtwco): translatable expected/found
+pub struct BuiltinClashingExternSub<'a> {
+    pub tcx: TyCtxt<'a>,
+    pub expected: Ty<'a>,
+    pub found: Ty<'a>,
+}
+
+impl AddToDiagnostic for BuiltinClashingExternSub<'_> {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        let mut expected_str = DiagnosticStyledString::new();
+        expected_str.push(self.expected.fn_sig(self.tcx).to_string(), false);
+        let mut found_str = DiagnosticStyledString::new();
+        found_str.push(self.found.fn_sig(self.tcx).to_string(), true);
+        diag.note_expected_found(&"", expected_str, &"", found_str);
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_deref_nullptr)]
+pub struct BuiltinDerefNullptr {
+    #[label]
+    pub label: Span,
+}
+
+// FIXME: migrate fluent::lint::builtin_asm_labels
+
+#[derive(LintDiagnostic)]
+pub enum BuiltinSpecialModuleNameUsed {
+    #[diag(lint_builtin_special_module_name_used_lib)]
+    #[note]
+    #[help]
+    Lib,
+    #[diag(lint_builtin_special_module_name_used_main)]
+    #[note]
+    Main,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_unexpected_cli_config_name)]
+#[help]
+pub struct BuiltinUnexpectedCliConfigName {
+    pub name: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_builtin_unexpected_cli_config_value)]
+#[help]
+pub struct BuiltinUnexpectedCliConfigValue {
+    pub name: Symbol,
+    pub value: Symbol,
+}
+
+// deref_into_dyn_supertrait.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_supertrait_as_deref_target)]
+pub struct SupertraitAsDerefTarget<'a> {
+    pub t: Ty<'a>,
+    pub target_principal: String,
+    // pub target_principal: Binder<'a, ExistentialTraitRef<'b>>,
+    #[subdiagnostic]
+    pub label: Option<SupertraitAsDerefTargetLabel>,
+}
+
+#[derive(Subdiagnostic)]
+#[label(label)]
+pub struct SupertraitAsDerefTargetLabel {
+    #[primary_span]
+    pub label: Span,
+}
+
+// enum_intrinsics_non_enums.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_enum_intrinsics_mem_discriminant)]
+pub struct EnumIntrinsicsMemDiscriminate<'a> {
+    pub ty_param: Ty<'a>,
+    #[note]
+    pub note: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_enum_intrinsics_mem_variant)]
+#[note]
+pub struct EnumIntrinsicsMemVariant<'a> {
+    pub ty_param: Ty<'a>,
+}
+
+// expect.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_expectation)]
+pub struct Expectation {
+    #[subdiagnostic]
+    pub rationale: Option<ExpectationNote>,
+    #[note]
+    pub note: Option<()>,
+}
+
+#[derive(Subdiagnostic)]
+#[note(rationale)]
+pub struct ExpectationNote {
+    pub rationale: Symbol,
+}
+
+// for_loops_over_fallibles.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_for_loops_over_fallibles)]
+pub struct ForLoopsOverFalliblesDiag<'a> {
+    pub article: &'static str,
+    pub ty: &'static str,
+    #[subdiagnostic]
+    pub sub: ForLoopsOverFalliblesLoopSub<'a>,
+    #[subdiagnostic]
+    pub question_mark: Option<ForLoopsOverFalliblesQuestionMark>,
+    #[subdiagnostic]
+    pub suggestion: ForLoopsOverFalliblesSuggestion<'a>,
+}
+
+#[derive(Subdiagnostic)]
+pub enum ForLoopsOverFalliblesLoopSub<'a> {
+    #[suggestion(remove_next, code = ".by_ref()", applicability = "maybe-incorrect")]
+    RemoveNext {
+        #[primary_span]
+        suggestion: Span,
+        recv_snip: String,
+    },
+    #[multipart_suggestion(use_while_let, applicability = "maybe-incorrect")]
+    UseWhileLet {
+        #[suggestion_part(code = "while let {var}(")]
+        start_span: Span,
+        #[suggestion_part(code = ") = ")]
+        end_span: Span,
+        var: &'a str,
+    },
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(use_question_mark, code = "?", applicability = "maybe-incorrect")]
+pub struct ForLoopsOverFalliblesQuestionMark {
+    #[primary_span]
+    pub suggestion: Span,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(suggestion, applicability = "maybe-incorrect")]
+pub struct ForLoopsOverFalliblesSuggestion<'a> {
+    pub var: &'a str,
+    #[suggestion_part(code = "if let {var}(")]
+    pub start_span: Span,
+    #[suggestion_part(code = ") = ")]
+    pub end_span: Span,
+}
+
+// hidden_unicode_codepoints.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_hidden_unicode_codepoints)]
+#[note]
+pub struct HiddenUnicodeCodepointsDiag<'a> {
+    pub label: &'a str,
+    pub count: usize,
+    #[label]
+    pub span_label: Span,
+    #[subdiagnostic]
+    pub labels: Option<HiddenUnicodeCodepointsDiagLabels>,
+    #[subdiagnostic]
+    pub sub: HiddenUnicodeCodepointsDiagSub,
+}
+
+pub struct HiddenUnicodeCodepointsDiagLabels {
+    pub spans: Vec<(char, Span)>,
+}
+
+impl AddToDiagnostic for HiddenUnicodeCodepointsDiagLabels {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        for (c, span) in self.spans {
+            diag.span_label(span, format!("{:?}", c));
+        }
+    }
+}
+
+pub enum HiddenUnicodeCodepointsDiagSub {
+    Escape { spans: Vec<(char, Span)> },
+    NoEscape { spans: Vec<(char, Span)> },
+}
+
+// Used because of multiple multipart_suggestion and note
+impl AddToDiagnostic for HiddenUnicodeCodepointsDiagSub {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        match self {
+            HiddenUnicodeCodepointsDiagSub::Escape { spans } => {
+                diag.multipart_suggestion_with_style(
+                    fluent::suggestion_remove,
+                    spans.iter().map(|(_, span)| (*span, "".to_string())).collect(),
+                    Applicability::MachineApplicable,
+                    SuggestionStyle::HideCodeAlways,
+                );
+                diag.multipart_suggestion(
+                    fluent::suggestion_escape,
+                    spans
+                        .into_iter()
+                        .map(|(c, span)| {
+                            let c = format!("{:?}", c);
+                            (span, c[1..c.len() - 1].to_string())
+                        })
+                        .collect(),
+                    Applicability::MachineApplicable,
+                );
+            }
+            HiddenUnicodeCodepointsDiagSub::NoEscape { spans } => {
+                // FIXME: in other suggestions we've reversed the inner spans of doc comments. We
+                // should do the same here to provide the same good suggestions as we do for
+                // literals above.
+                diag.set_arg(
+                    "escaped",
+                    spans
+                        .into_iter()
+                        .map(|(c, _)| format!("{:?}", c))
+                        .collect::<Vec<String>>()
+                        .join(", "),
+                );
+                diag.note(fluent::suggestion_remove);
+                diag.note(fluent::no_suggestion_note_escape);
+            }
+        }
+    }
+}
+
+// internal.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_default_hash_types)]
+#[note]
+pub struct DefaultHashTypesDiag<'a> {
+    pub preferred: &'a str,
+    pub used: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_query_instability)]
+#[note]
+pub struct QueryInstability {
+    pub query: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_tykind_kind)]
+pub struct TykindKind {
+    #[suggestion(code = "ty", applicability = "maybe-incorrect")]
+    pub suggestion: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_tykind)]
+#[help]
+pub struct TykindDiag;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_ty_qualified)]
+pub struct TyQualified {
+    pub ty: String,
+    #[suggestion(code = "{ty}", applicability = "maybe-incorrect")]
+    pub suggestion: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_lintpass_by_hand)]
+#[help]
+pub struct LintPassByHand;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_non_existant_doc_keyword)]
+#[help]
+pub struct NonExistantDocKeyword {
+    pub keyword: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_diag_out_of_impl)]
+pub struct DiagOutOfImpl;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_untranslatable_diag)]
+pub struct UntranslatableDiag;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_bad_opt_access)]
+pub struct BadOptAccessDiag<'a> {
+    pub msg: &'a str,
+}
+
+// let_underscore.rs
+#[derive(LintDiagnostic)]
+pub enum NonBindingLet {
+    #[diag(lint_non_binding_let_on_sync_lock)]
+    SyncLock {
+        #[subdiagnostic]
+        sub: NonBindingLetSub,
+    },
+    #[diag(lint_non_binding_let_on_drop_type)]
+    DropType {
+        #[subdiagnostic]
+        sub: NonBindingLetSub,
+    },
+}
+
+pub struct NonBindingLetSub {
+    pub suggestion: Span,
+    pub multi_suggestion_start: Span,
+    pub multi_suggestion_end: Span,
+}
+
+impl AddToDiagnostic for NonBindingLetSub {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        diag.span_suggestion_verbose(
+            self.suggestion,
+            fluent::lint_non_binding_let_suggestion,
+            "_unused",
+            Applicability::MachineApplicable,
+        );
+        diag.multipart_suggestion(
+            fluent::lint_non_binding_let_multi_suggestion,
+            vec![
+                (self.multi_suggestion_start, "drop(".to_string()),
+                (self.multi_suggestion_end, ")".to_string()),
+            ],
+            Applicability::MachineApplicable,
+        );
+    }
+}
+
+// levels.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_overruled_attribute)]
+pub struct OverruledAtributeLint<'a> {
+    #[label]
+    pub overruled: Span,
+    pub lint_level: &'a str,
+    pub lint_source: Symbol,
+    #[subdiagnostic]
+    pub sub: OverruledAttributeSub,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_deprecated_lint_name)]
+pub struct DeprecatedLintName<'a> {
+    pub name: String,
+    #[suggestion(code = "{replace}", applicability = "machine-applicable")]
+    pub suggestion: Span,
+    pub replace: &'a str,
+}
+
+// FIXME: Non-translatable msg
+#[derive(LintDiagnostic)]
+#[diag(lint_renamed_or_removed_lint)]
+pub struct RenamedOrRemovedLint<'a> {
+    pub msg: &'a str,
+    #[subdiagnostic]
+    pub suggestion: Option<RenamedOrRemovedLintSuggestion<'a>>,
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(suggestion, code = "{replace}", applicability = "machine-applicable")]
+pub struct RenamedOrRemovedLintSuggestion<'a> {
+    #[primary_span]
+    pub suggestion: Span,
+    pub replace: &'a str,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unknown_lint)]
+pub struct UnknownLint {
+    pub name: String,
+    #[subdiagnostic]
+    pub suggestion: Option<UnknownLintSuggestion>,
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(suggestion, code = "{replace}", applicability = "maybe-incorrect")]
+pub struct UnknownLintSuggestion {
+    #[primary_span]
+    pub suggestion: Span,
+    pub replace: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_ignored_unless_crate_specified)]
+pub struct IgnoredUnlessCrateSpecified<'a> {
+    pub level: &'a str,
+    pub name: Symbol,
+}
+
+// methods.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_cstring_ptr)]
+#[note]
+#[help]
+pub struct CStringPtr {
+    #[label(as_ptr_label)]
+    pub as_ptr: Span,
+    #[label(unwrap_label)]
+    pub unwrap: Span,
+}
+
+// non_ascii_idents.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_identifier_non_ascii_char)]
+pub struct IdentifierNonAsciiChar;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_identifier_uncommon_codepoints)]
+pub struct IdentifierUncommonCodepoints;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_confusable_identifier_pair)]
+pub struct ConfusableIdentifierPair {
+    pub existing_sym: Symbol,
+    pub sym: Symbol,
+    #[label]
+    pub label: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_mixed_script_confusables)]
+#[note(includes_note)]
+#[note]
+pub struct MixedScriptConfusables {
+    pub set: String,
+    pub includes: String,
+}
+
+// non_fmt_panic.rs
+pub struct NonFmtPanicUnused {
+    pub count: usize,
+    pub suggestion: Option<Span>,
+}
+
+// Used because of two suggestions based on one Option<Span>
+impl<'a> DecorateLint<'a, ()> for NonFmtPanicUnused {
+    fn decorate_lint<'b>(
+        self,
+        diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
+    ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
+        diag.set_arg("count", self.count);
+        diag.note(fluent::note);
+        if let Some(span) = self.suggestion {
+            diag.span_suggestion(
+                span.shrink_to_hi(),
+                fluent::add_args_suggestion,
+                ", ...",
+                Applicability::HasPlaceholders,
+            );
+            diag.span_suggestion(
+                span.shrink_to_lo(),
+                fluent::add_fmt_suggestion,
+                "\"{}\", ",
+                Applicability::MachineApplicable,
+            );
+        }
+        diag
+    }
+
+    fn msg(&self) -> rustc_errors::DiagnosticMessage {
+        fluent::lint_non_fmt_panic_unused
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_non_fmt_panic_braces)]
+#[note]
+pub struct NonFmtPanicBraces {
+    pub count: usize,
+    #[suggestion(code = "\"{{}}\", ", applicability = "machine-applicable")]
+    pub suggestion: Option<Span>,
+}
+
+// nonstandard_style.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_non_camel_case_type)]
+pub struct NonCamelCaseType<'a> {
+    pub sort: &'a str,
+    pub name: &'a str,
+    #[subdiagnostic]
+    pub sub: NonCamelCaseTypeSub,
+}
+
+#[derive(Subdiagnostic)]
+pub enum NonCamelCaseTypeSub {
+    #[label(label)]
+    Label {
+        #[primary_span]
+        span: Span,
+    },
+    #[suggestion(suggestion, code = "{replace}", applicability = "maybe-incorrect")]
+    Suggestion {
+        #[primary_span]
+        span: Span,
+        replace: String,
+    },
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_non_snake_case)]
+pub struct NonSnakeCaseDiag<'a> {
+    pub sort: &'a str,
+    pub name: &'a str,
+    pub sc: String,
+    #[subdiagnostic]
+    pub sub: NonSnakeCaseDiagSub,
+}
+
+pub enum NonSnakeCaseDiagSub {
+    Label { span: Span },
+    Help,
+    RenameOrConvertSuggestion { span: Span, suggestion: Ident },
+    ConvertSuggestion { span: Span, suggestion: String },
+    SuggestionAndNote { span: Span },
+}
+
+impl AddToDiagnostic for NonSnakeCaseDiagSub {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        match self {
+            NonSnakeCaseDiagSub::Label { span } => {
+                diag.span_label(span, fluent::label);
+            }
+            NonSnakeCaseDiagSub::Help => {
+                diag.help(fluent::help);
+            }
+            NonSnakeCaseDiagSub::ConvertSuggestion { span, suggestion } => {
+                diag.span_suggestion(
+                    span,
+                    fluent::convert_suggestion,
+                    suggestion,
+                    Applicability::MaybeIncorrect,
+                );
+            }
+            NonSnakeCaseDiagSub::RenameOrConvertSuggestion { span, suggestion } => {
+                diag.span_suggestion(
+                    span,
+                    fluent::rename_or_convert_suggestion,
+                    suggestion,
+                    Applicability::MaybeIncorrect,
+                );
+            }
+            NonSnakeCaseDiagSub::SuggestionAndNote { span } => {
+                diag.note(fluent::cannot_convert_note);
+                diag.span_suggestion(
+                    span,
+                    fluent::rename_suggestion,
+                    "",
+                    Applicability::MaybeIncorrect,
+                );
+            }
+        }
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_non_upper_case_global)]
+pub struct NonUpperCaseGlobal<'a> {
+    pub sort: &'a str,
+    pub name: &'a str,
+    #[subdiagnostic]
+    pub sub: NonUpperCaseGlobalSub,
+}
+
+#[derive(Subdiagnostic)]
+pub enum NonUpperCaseGlobalSub {
+    #[label(label)]
+    Label {
+        #[primary_span]
+        span: Span,
+    },
+    #[suggestion(suggestion, code = "{replace}", applicability = "maybe-incorrect")]
+    Suggestion {
+        #[primary_span]
+        span: Span,
+        replace: String,
+    },
+}
+
+// noop_method_call.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_noop_method_call)]
+#[note]
+pub struct NoopMethodCallDiag<'a> {
+    pub method: Symbol,
+    pub receiver_ty: Ty<'a>,
+    #[label]
+    pub label: Span,
+}
+
+// pass_by_value.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_pass_by_value)]
+pub struct PassByValueDiag {
+    pub ty: String,
+    #[suggestion(code = "{ty}", applicability = "maybe-incorrect")]
+    pub suggestion: Span,
+}
+
+// redundant_semicolon.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_redundant_semicolons)]
+pub struct RedundantSemicolonsDiag {
+    pub multiple: bool,
+    #[suggestion(code = "", applicability = "maybe-incorrect")]
+    pub suggestion: Span,
+}
+
+// traits.rs
+pub struct DropTraitConstraintsDiag<'a> {
+    pub predicate: Predicate<'a>,
+    pub tcx: TyCtxt<'a>,
+    pub def_id: DefId,
+}
+
+// Needed for def_path_str
+impl<'a> DecorateLint<'a, ()> for DropTraitConstraintsDiag<'_> {
+    fn decorate_lint<'b>(
+        self,
+        diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
+    ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
+        diag.set_arg("predicate", self.predicate);
+        diag.set_arg("needs_drop", self.tcx.def_path_str(self.def_id))
+    }
+
+    fn msg(&self) -> rustc_errors::DiagnosticMessage {
+        fluent::lint_drop_trait_constraints
+    }
+}
+
+pub struct DropGlue<'a> {
+    pub tcx: TyCtxt<'a>,
+    pub def_id: DefId,
+}
+
+// Needed for def_path_str
+impl<'a> DecorateLint<'a, ()> for DropGlue<'_> {
+    fn decorate_lint<'b>(
+        self,
+        diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
+    ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
+        diag.set_arg("needs_drop", self.tcx.def_path_str(self.def_id))
+    }
+
+    fn msg(&self) -> rustc_errors::DiagnosticMessage {
+        fluent::lint_drop_glue
+    }
+}
+
+// types.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_range_endpoint_out_of_range)]
+pub struct RangeEndpointOutOfRange<'a> {
+    pub ty: &'a str,
+    #[suggestion(code = "{start}..={literal}{suffix}", applicability = "machine-applicable")]
+    pub suggestion: Span,
+    pub start: String,
+    pub literal: u128,
+    pub suffix: &'a str,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_overflowing_bin_hex)]
+pub struct OverflowingBinHex<'a> {
+    pub ty: &'a str,
+    pub lit: String,
+    pub dec: u128,
+    pub actually: String,
+    #[subdiagnostic]
+    pub sign: OverflowingBinHexSign,
+    #[subdiagnostic]
+    pub sub: Option<OverflowingBinHexSub<'a>>,
+}
+
+pub enum OverflowingBinHexSign {
+    Positive,
+    Negative,
+}
+
+impl AddToDiagnostic for OverflowingBinHexSign {
+    fn add_to_diagnostic_with<F>(self, diag: &mut rustc_errors::Diagnostic, _: F)
+    where
+        F: Fn(
+            &mut rustc_errors::Diagnostic,
+            rustc_errors::SubdiagnosticMessage,
+        ) -> rustc_errors::SubdiagnosticMessage,
+    {
+        match self {
+            OverflowingBinHexSign::Positive => {
+                diag.note(fluent::positive_note);
+            }
+            OverflowingBinHexSign::Negative => {
+                diag.note(fluent::negative_note);
+                diag.note(fluent::negative_becomes_note);
+            }
+        }
+    }
+}
+
+#[derive(Subdiagnostic)]
+pub enum OverflowingBinHexSub<'a> {
+    #[suggestion(
+        suggestion,
+        code = "{sans_suffix}{suggestion_ty}",
+        applicability = "machine-applicable"
+    )]
+    Suggestion {
+        #[primary_span]
+        span: Span,
+        suggestion_ty: &'a str,
+        sans_suffix: &'a str,
+    },
+    #[help(help)]
+    Help { suggestion_ty: &'a str },
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_overflowing_int)]
+#[note]
+pub struct OverflowingInt<'a> {
+    pub ty: &'a str,
+    pub lit: String,
+    pub min: i128,
+    pub max: u128,
+    #[subdiagnostic]
+    pub help: Option<OverflowingIntHelp<'a>>,
+}
+
+#[derive(Subdiagnostic)]
+#[help(help)]
+pub struct OverflowingIntHelp<'a> {
+    pub suggestion_ty: &'a str,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_only_cast_u8_to_char)]
+pub struct OnlyCastu8ToChar {
+    #[suggestion(code = "'\\u{{{literal:X}}}'", applicability = "machine-applicable")]
+    pub span: Span,
+    pub literal: u128,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_overflowing_uint)]
+#[note]
+pub struct OverflowingUInt<'a> {
+    pub ty: &'a str,
+    pub lit: String,
+    pub min: u128,
+    pub max: u128,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_overflowing_literal)]
+#[note]
+pub struct OverflowingLiteral<'a> {
+    pub ty: &'a str,
+    pub lit: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_comparisons)]
+pub struct UnusedComparisons;
+
+pub struct ImproperCTypes<'a> {
+    pub ty: Ty<'a>,
+    pub desc: &'a str,
+    pub label: Span,
+    pub help: Option<DiagnosticMessage>,
+    pub note: DiagnosticMessage,
+    pub span_note: Option<Span>,
+}
+
+// Used because of the complexity of Option<DiagnosticMessage>, DiagnosticMessage, and Option<Span>
+impl<'a> DecorateLint<'a, ()> for ImproperCTypes<'_> {
+    fn decorate_lint<'b>(
+        self,
+        diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
+    ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
+        diag.set_arg("ty", self.ty);
+        diag.set_arg("desc", self.desc);
+        diag.span_label(self.label, fluent::label);
+        if let Some(help) = self.help {
+            diag.help(help);
+        }
+        diag.note(self.note);
+        if let Some(note) = self.span_note {
+            diag.span_note(note, fluent::note);
+        }
+        diag
+    }
+
+    fn msg(&self) -> rustc_errors::DiagnosticMessage {
+        fluent::lint_improper_ctypes
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_variant_size_differences)]
+pub struct VariantSizeDifferencesDiag {
+    pub largest: u64,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_atomic_ordering_load)]
+#[help]
+pub struct AtomicOrderingLoad;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_atomic_ordering_store)]
+#[help]
+pub struct AtomicOrderingStore;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_atomic_ordering_fence)]
+#[help]
+pub struct AtomicOrderingFence;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_atomic_ordering_invalid)]
+#[help]
+pub struct InvalidAtomicOrderingDiag {
+    pub method: Symbol,
+    #[label]
+    pub fail_order_arg_span: Span,
+}
+
+// unused.rs
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_op)]
+pub struct UnusedOp<'a> {
+    pub op: &'a str,
+    #[label]
+    pub label: Span,
+    #[suggestion(style = "verbose", code = "let _ = ", applicability = "machine-applicable")]
+    pub suggestion: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_result)]
+pub struct UnusedResult<'a> {
+    pub ty: Ty<'a>,
+}
+
+// FIXME(davidtwco): this isn't properly translatable becauses of the
+// pre/post strings
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_closure)]
+#[note]
+pub struct UnusedClosure<'a> {
+    pub count: usize,
+    pub pre: &'a str,
+    pub post: &'a str,
+}
+
+// FIXME(davidtwco): this isn't properly translatable becauses of the
+// pre/post strings
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_generator)]
+#[note]
+pub struct UnusedGenerator<'a> {
+    pub count: usize,
+    pub pre: &'a str,
+    pub post: &'a str,
+}
+
+// FIXME(davidtwco): this isn't properly translatable becauses of the pre/post
+// strings
+pub struct UnusedDef<'a, 'b> {
+    pub pre: &'a str,
+    pub post: &'a str,
+    pub cx: &'a LateContext<'b>,
+    pub def_id: DefId,
+    pub note: Option<Symbol>,
+}
+
+// Needed because of def_path_str
+impl<'a> DecorateLint<'a, ()> for UnusedDef<'_, '_> {
+    fn decorate_lint<'b>(
+        self,
+        diag: &'b mut rustc_errors::DiagnosticBuilder<'a, ()>,
+    ) -> &'b mut rustc_errors::DiagnosticBuilder<'a, ()> {
+        diag.set_arg("pre", self.pre);
+        diag.set_arg("post", self.post);
+        diag.set_arg("def", self.cx.tcx.def_path_str(self.def_id));
+        // check for #[must_use = "..."]
+        if let Some(note) = self.note {
+            diag.note(note.as_str());
+        }
+        diag
+    }
+
+    fn msg(&self) -> rustc_errors::DiagnosticMessage {
+        fluent::lint_unused_def
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_path_statement_drop)]
+pub struct PathStatementDrop {
+    #[subdiagnostic]
+    pub sub: PathStatementDropSub,
+}
+
+#[derive(Subdiagnostic)]
+pub enum PathStatementDropSub {
+    #[suggestion(suggestion, code = "drop({snippet});", applicability = "machine-applicable")]
+    Suggestion {
+        #[primary_span]
+        span: Span,
+        snippet: String,
+    },
+    #[help(help)]
+    Help {
+        #[primary_span]
+        span: Span,
+    },
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_path_statement_no_effect)]
+pub struct PathStatementNoEffect;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_delim)]
+pub struct UnusedDelim<'a> {
+    pub delim: &'static str,
+    pub item: &'a str,
+    #[subdiagnostic]
+    pub suggestion: Option<UnusedDelimSuggestion>,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(suggestion, applicability = "machine-applicable")]
+pub struct UnusedDelimSuggestion {
+    #[suggestion_part(code = "{start_replace}")]
+    pub start_span: Span,
+    pub start_replace: &'static str,
+    #[suggestion_part(code = "{end_replace}")]
+    pub end_span: Span,
+    pub end_replace: &'static str,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_import_braces)]
+pub struct UnusedImportBracesDiag {
+    pub node: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_allocation)]
+pub struct UnusedAllocationDiag;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_allocation_mut)]
+pub struct UnusedAllocationMutDiag;
diff --git a/compiler/rustc_lint/src/methods.rs b/compiler/rustc_lint/src/methods.rs
index e2d7d5b49f6..3045fc1a476 100644
--- a/compiler/rustc_lint/src/methods.rs
+++ b/compiler/rustc_lint/src/methods.rs
@@ -1,7 +1,7 @@
+use crate::lints::CStringPtr;
 use crate::LateContext;
 use crate::LateLintPass;
 use crate::LintContext;
-use rustc_errors::fluent;
 use rustc_hir::{Expr, ExprKind, PathSegment};
 use rustc_middle::ty;
 use rustc_span::{symbol::sym, ExpnKind, Span};
@@ -90,16 +90,10 @@ fn lint_cstring_as_ptr(
         if cx.tcx.is_diagnostic_item(sym::Result, def.did()) {
             if let ty::Adt(adt, _) = substs.type_at(0).kind() {
                 if cx.tcx.is_diagnostic_item(sym::cstring_type, adt.did()) {
-                    cx.struct_span_lint(
+                    cx.emit_spanned_lint(
                         TEMPORARY_CSTRING_AS_PTR,
                         as_ptr_span,
-                        fluent::lint_cstring_ptr,
-                        |diag| {
-                            diag.span_label(as_ptr_span, fluent::as_ptr_label)
-                                .span_label(unwrap.span, fluent::unwrap_label)
-                                .note(fluent::note)
-                                .help(fluent::help)
-                        },
+                        CStringPtr { as_ptr: as_ptr_span, unwrap: unwrap.span },
                     );
                 }
             }
diff --git a/compiler/rustc_lint/src/non_ascii_idents.rs b/compiler/rustc_lint/src/non_ascii_idents.rs
index dea9506acb2..f130a98185d 100644
--- a/compiler/rustc_lint/src/non_ascii_idents.rs
+++ b/compiler/rustc_lint/src/non_ascii_idents.rs
@@ -1,7 +1,10 @@
+use crate::lints::{
+    ConfusableIdentifierPair, IdentifierNonAsciiChar, IdentifierUncommonCodepoints,
+    MixedScriptConfusables,
+};
 use crate::{EarlyContext, EarlyLintPass, LintContext};
 use rustc_ast as ast;
 use rustc_data_structures::fx::FxHashMap;
-use rustc_errors::fluent;
 use rustc_span::symbol::Symbol;
 
 declare_lint! {
@@ -180,21 +183,11 @@ impl EarlyLintPass for NonAsciiIdents {
                 continue;
             }
             has_non_ascii_idents = true;
-            cx.struct_span_lint(
-                NON_ASCII_IDENTS,
-                sp,
-                fluent::lint_identifier_non_ascii_char,
-                |lint| lint,
-            );
+            cx.emit_spanned_lint(NON_ASCII_IDENTS, sp, IdentifierNonAsciiChar);
             if check_uncommon_codepoints
                 && !symbol_str.chars().all(GeneralSecurityProfile::identifier_allowed)
             {
-                cx.struct_span_lint(
-                    UNCOMMON_CODEPOINTS,
-                    sp,
-                    fluent::lint_identifier_uncommon_codepoints,
-                    |lint| lint,
-                )
+                cx.emit_spanned_lint(UNCOMMON_CODEPOINTS, sp, IdentifierUncommonCodepoints);
             }
         }
 
@@ -222,14 +215,13 @@ impl EarlyLintPass for NonAsciiIdents {
                     .entry(skeleton_sym)
                     .and_modify(|(existing_symbol, existing_span, existing_is_ascii)| {
                         if !*existing_is_ascii || !is_ascii {
-                            cx.struct_span_lint(
+                            cx.emit_spanned_lint(
                                 CONFUSABLE_IDENTS,
                                 sp,
-                                fluent::lint_confusable_identifier_pair,
-                                |lint| {
-                                    lint.set_arg("existing_sym", *existing_symbol)
-                                        .set_arg("sym", symbol)
-                                        .span_label(*existing_span, fluent::label)
+                                ConfusableIdentifierPair {
+                                    existing_sym: *existing_symbol,
+                                    sym: symbol,
+                                    label: *existing_span,
                                 },
                             );
                         }
@@ -331,24 +323,18 @@ impl EarlyLintPass for NonAsciiIdents {
                 }
 
                 for ((sp, ch_list), script_set) in lint_reports {
-                    cx.struct_span_lint(
+                    let mut includes = String::new();
+                    for (idx, ch) in ch_list.into_iter().enumerate() {
+                        if idx != 0 {
+                            includes += ", ";
+                        }
+                        let char_info = format!("'{}' (U+{:04X})", ch, ch as u32);
+                        includes += &char_info;
+                    }
+                    cx.emit_spanned_lint(
                         MIXED_SCRIPT_CONFUSABLES,
                         sp,
-                        fluent::lint_mixed_script_confusables,
-                        |lint| {
-                            let mut includes = String::new();
-                            for (idx, ch) in ch_list.into_iter().enumerate() {
-                                if idx != 0 {
-                                    includes += ", ";
-                                }
-                                let char_info = format!("'{}' (U+{:04X})", ch, ch as u32);
-                                includes += &char_info;
-                            }
-                            lint.set_arg("set", script_set.to_string())
-                                .set_arg("includes", includes)
-                                .note(fluent::includes_note)
-                                .note(fluent::note)
-                        },
+                        MixedScriptConfusables { set: script_set.to_string(), includes },
                     );
                 }
             }
diff --git a/compiler/rustc_lint/src/non_fmt_panic.rs b/compiler/rustc_lint/src/non_fmt_panic.rs
index 8dccfe0046c..4a02c6cce47 100644
--- a/compiler/rustc_lint/src/non_fmt_panic.rs
+++ b/compiler/rustc_lint/src/non_fmt_panic.rs
@@ -1,3 +1,4 @@
+use crate::lints::{NonFmtPanicBraces, NonFmtPanicUnused};
 use crate::{LateContext, LateLintPass, LintContext};
 use rustc_ast as ast;
 use rustc_errors::{fluent, Applicability};
@@ -118,6 +119,7 @@ fn check_panic<'tcx>(cx: &LateContext<'tcx>, f: &'tcx hir::Expr<'tcx>, arg: &'tc
         arg_span = expn.call_site;
     }
 
+    #[allow(rustc::diagnostic_outside_of_impl)]
     cx.struct_span_lint(NON_FMT_PANICS, arg_span, fluent::lint_non_fmt_panic, |lint| {
         lint.set_arg("name", symbol);
         lint.note(fluent::note);
@@ -253,25 +255,14 @@ fn check_panic_str<'tcx>(
                 .map(|span| fmt_span.from_inner(InnerSpan::new(span.start, span.end)))
                 .collect(),
         };
-        cx.struct_span_lint(NON_FMT_PANICS, arg_spans, fluent::lint_non_fmt_panic_unused, |lint| {
-            lint.set_arg("count", n_arguments);
-            lint.note(fluent::note);
-            if is_arg_inside_call(arg.span, span) {
-                lint.span_suggestion(
-                    arg.span.shrink_to_hi(),
-                    fluent::add_args_suggestion,
-                    ", ...",
-                    Applicability::HasPlaceholders,
-                );
-                lint.span_suggestion(
-                    arg.span.shrink_to_lo(),
-                    fluent::add_fmt_suggestion,
-                    "\"{}\", ",
-                    Applicability::MachineApplicable,
-                );
-            }
-            lint
-        });
+        cx.emit_spanned_lint(
+            NON_FMT_PANICS,
+            arg_spans,
+            NonFmtPanicUnused {
+                count: n_arguments,
+                suggestion: is_arg_inside_call(arg.span, span).then_some(arg.span),
+            },
+        );
     } else {
         let brace_spans: Option<Vec<_>> =
             snippet.filter(|s| s.starts_with('"') || s.starts_with("r#")).map(|s| {
@@ -281,22 +272,12 @@ fn check_panic_str<'tcx>(
                     .collect()
             });
         let count = brace_spans.as_ref().map(|v| v.len()).unwrap_or(/* any number >1 */ 2);
-        cx.struct_span_lint(
+        cx.emit_spanned_lint(
             NON_FMT_PANICS,
             brace_spans.unwrap_or_else(|| vec![span]),
-            fluent::lint_non_fmt_panic_braces,
-            |lint| {
-                lint.set_arg("count", count);
-                lint.note(fluent::note);
-                if is_arg_inside_call(arg.span, span) {
-                    lint.span_suggestion(
-                        arg.span.shrink_to_lo(),
-                        fluent::suggestion,
-                        "\"{}\", ",
-                        Applicability::MachineApplicable,
-                    );
-                }
-                lint
+            NonFmtPanicBraces {
+                count,
+                suggestion: is_arg_inside_call(arg.span, span).then_some(arg.span.shrink_to_lo()),
             },
         );
     }
diff --git a/compiler/rustc_lint/src/nonstandard_style.rs b/compiler/rustc_lint/src/nonstandard_style.rs
index f37d6e9a63d..74d234fabea 100644
--- a/compiler/rustc_lint/src/nonstandard_style.rs
+++ b/compiler/rustc_lint/src/nonstandard_style.rs
@@ -1,7 +1,10 @@
+use crate::lints::{
+    NonCamelCaseType, NonCamelCaseTypeSub, NonSnakeCaseDiag, NonSnakeCaseDiagSub,
+    NonUpperCaseGlobal, NonUpperCaseGlobalSub,
+};
 use crate::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};
 use rustc_ast as ast;
 use rustc_attr as attr;
-use rustc_errors::{fluent, Applicability};
 use rustc_hir as hir;
 use rustc_hir::def::{DefKind, Res};
 use rustc_hir::intravisit::FnKind;
@@ -136,30 +139,17 @@ impl NonCamelCaseTypes {
         let name = ident.name.as_str();
 
         if !is_camel_case(name) {
-            cx.struct_span_lint(
+            let cc = to_camel_case(name);
+            let sub = if *name != cc {
+                NonCamelCaseTypeSub::Suggestion { span: ident.span, replace: cc }
+            } else {
+                NonCamelCaseTypeSub::Label { span: ident.span }
+            };
+            cx.emit_spanned_lint(
                 NON_CAMEL_CASE_TYPES,
                 ident.span,
-                fluent::lint_non_camel_case_type,
-                |lint| {
-                    let cc = to_camel_case(name);
-                    // We cannot provide meaningful suggestions
-                    // if the characters are in the category of "Lowercase Letter".
-                    if *name != cc {
-                        lint.span_suggestion(
-                            ident.span,
-                            fluent::suggestion,
-                            to_camel_case(name),
-                            Applicability::MaybeIncorrect,
-                        );
-                    } else {
-                        lint.span_label(ident.span, fluent::label);
-                    }
-
-                    lint.set_arg("sort", sort);
-                    lint.set_arg("name", name);
-                    lint
-                },
-            )
+                NonCamelCaseType { sort, name, sub },
+            );
         }
     }
 }
@@ -294,47 +284,37 @@ impl NonSnakeCase {
         let name = ident.name.as_str();
 
         if !is_snake_case(name) {
-            cx.struct_span_lint(NON_SNAKE_CASE, ident.span, fluent::lint_non_snake_case, |lint| {
-                let sc = NonSnakeCase::to_snake_case(name);
-                // We cannot provide meaningful suggestions
-                // if the characters are in the category of "Uppercase Letter".
-                if name != sc {
-                    // We have a valid span in almost all cases, but we don't have one when linting a crate
-                    // name provided via the command line.
-                    if !ident.span.is_dummy() {
-                        let sc_ident = Ident::from_str_and_span(&sc, ident.span);
-                        let (message, suggestion) = if sc_ident.is_reserved() {
-                            // We shouldn't suggest a reserved identifier to fix non-snake-case identifiers.
-                            // Instead, recommend renaming the identifier entirely or, if permitted,
-                            // escaping it to create a raw identifier.
-                            if sc_ident.name.can_be_raw() {
-                                (fluent::rename_or_convert_suggestion, sc_ident.to_string())
-                            } else {
-                                lint.note(fluent::cannot_convert_note);
-                                (fluent::rename_suggestion, String::new())
+            let span = ident.span;
+            let sc = NonSnakeCase::to_snake_case(name);
+            // We cannot provide meaningful suggestions
+            // if the characters are in the category of "Uppercase Letter".
+            let sub = if name != sc {
+                // We have a valid span in almost all cases, but we don't have one when linting a crate
+                // name provided via the command line.
+                if !span.is_dummy() {
+                    let sc_ident = Ident::from_str_and_span(&sc, span);
+                    if sc_ident.is_reserved() {
+                        // We shouldn't suggest a reserved identifier to fix non-snake-case identifiers.
+                        // Instead, recommend renaming the identifier entirely or, if permitted,
+                        // escaping it to create a raw identifier.
+                        if sc_ident.name.can_be_raw() {
+                            NonSnakeCaseDiagSub::RenameOrConvertSuggestion {
+                                span,
+                                suggestion: sc_ident,
                             }
                         } else {
-                            (fluent::convert_suggestion, sc.clone())
-                        };
-
-                        lint.span_suggestion(
-                            ident.span,
-                            message,
-                            suggestion,
-                            Applicability::MaybeIncorrect,
-                        );
+                            NonSnakeCaseDiagSub::SuggestionAndNote { span }
+                        }
                     } else {
-                        lint.help(fluent::help);
+                        NonSnakeCaseDiagSub::ConvertSuggestion { span, suggestion: sc.clone() }
                     }
                 } else {
-                    lint.span_label(ident.span, fluent::label);
+                    NonSnakeCaseDiagSub::Help
                 }
-
-                lint.set_arg("sort", sort);
-                lint.set_arg("name", name);
-                lint.set_arg("sc", sc);
-                lint
-            });
+            } else {
+                NonSnakeCaseDiagSub::Label { span }
+            };
+            cx.emit_spanned_lint(NON_SNAKE_CASE, span, NonSnakeCaseDiag { sort, name, sc, sub });
         }
     }
 }
@@ -490,30 +470,19 @@ impl NonUpperCaseGlobals {
     fn check_upper_case(cx: &LateContext<'_>, sort: &str, ident: &Ident) {
         let name = ident.name.as_str();
         if name.chars().any(|c| c.is_lowercase()) {
-            cx.struct_span_lint(
+            let uc = NonSnakeCase::to_snake_case(&name).to_uppercase();
+            // We cannot provide meaningful suggestions
+            // if the characters are in the category of "Lowercase Letter".
+            let sub = if *name != uc {
+                NonUpperCaseGlobalSub::Suggestion { span: ident.span, replace: uc }
+            } else {
+                NonUpperCaseGlobalSub::Label { span: ident.span }
+            };
+            cx.emit_spanned_lint(
                 NON_UPPER_CASE_GLOBALS,
                 ident.span,
-                fluent::lint_non_upper_case_global,
-                |lint| {
-                    let uc = NonSnakeCase::to_snake_case(&name).to_uppercase();
-                    // We cannot provide meaningful suggestions
-                    // if the characters are in the category of "Lowercase Letter".
-                    if *name != uc {
-                        lint.span_suggestion(
-                            ident.span,
-                            fluent::suggestion,
-                            uc,
-                            Applicability::MaybeIncorrect,
-                        );
-                    } else {
-                        lint.span_label(ident.span, fluent::label);
-                    }
-
-                    lint.set_arg("sort", sort);
-                    lint.set_arg("name", name);
-                    lint
-                },
-            )
+                NonUpperCaseGlobal { sort, name, sub },
+            );
         }
     }
 }
diff --git a/compiler/rustc_lint/src/noop_method_call.rs b/compiler/rustc_lint/src/noop_method_call.rs
index 2ef425a1093..d67a00619dd 100644
--- a/compiler/rustc_lint/src/noop_method_call.rs
+++ b/compiler/rustc_lint/src/noop_method_call.rs
@@ -1,7 +1,7 @@
 use crate::context::LintContext;
+use crate::lints::NoopMethodCallDiag;
 use crate::LateContext;
 use crate::LateLintPass;
-use rustc_errors::fluent;
 use rustc_hir::def::DefKind;
 use rustc_hir::{Expr, ExprKind};
 use rustc_middle::ty;
@@ -85,11 +85,10 @@ impl<'tcx> LateLintPass<'tcx> for NoopMethodCall {
         }
         let expr_span = expr.span;
         let span = expr_span.with_lo(receiver.span.hi());
-        cx.struct_span_lint(NOOP_METHOD_CALL, span, fluent::lint_noop_method_call, |lint| {
-            lint.set_arg("method", call.ident.name)
-                .set_arg("receiver_ty", receiver_ty)
-                .span_label(span, fluent::label)
-                .note(fluent::note)
-        });
+        cx.emit_spanned_lint(
+            NOOP_METHOD_CALL,
+            span,
+            NoopMethodCallDiag { method: call.ident.name, receiver_ty, label: span },
+        );
     }
 }
diff --git a/compiler/rustc_lint/src/pass_by_value.rs b/compiler/rustc_lint/src/pass_by_value.rs
index 22caadfab17..57482a9edba 100644
--- a/compiler/rustc_lint/src/pass_by_value.rs
+++ b/compiler/rustc_lint/src/pass_by_value.rs
@@ -1,5 +1,5 @@
+use crate::lints::PassByValueDiag;
 use crate::{LateContext, LateLintPass, LintContext};
-use rustc_errors::{fluent, Applicability};
 use rustc_hir as hir;
 use rustc_hir::def::Res;
 use rustc_hir::{GenericArg, PathSegment, QPath, TyKind};
@@ -29,20 +29,11 @@ impl<'tcx> LateLintPass<'tcx> for PassByValue {
                     }
                 }
                 if let Some(t) = path_for_pass_by_value(cx, &inner_ty) {
-                    cx.struct_span_lint(
+                    cx.emit_spanned_lint(
                         PASS_BY_VALUE,
                         ty.span,
-                        fluent::lint_pass_by_value,
-                        |lint| {
-                            lint.set_arg("ty", t.clone()).span_suggestion(
-                                ty.span,
-                                fluent::suggestion,
-                                t,
-                                // Changing type of function argument
-                                Applicability::MaybeIncorrect,
-                            )
-                        },
-                    )
+                        PassByValueDiag { ty: t.clone(), suggestion: ty.span },
+                    );
                 }
             }
             _ => {}
diff --git a/compiler/rustc_lint/src/redundant_semicolon.rs b/compiler/rustc_lint/src/redundant_semicolon.rs
index 3521de7fc08..9a8b14b4907 100644
--- a/compiler/rustc_lint/src/redundant_semicolon.rs
+++ b/compiler/rustc_lint/src/redundant_semicolon.rs
@@ -1,6 +1,5 @@
-use crate::{EarlyContext, EarlyLintPass, LintContext};
+use crate::{lints::RedundantSemicolonsDiag, EarlyContext, EarlyLintPass, LintContext};
 use rustc_ast::{Block, StmtKind};
-use rustc_errors::{fluent, Applicability};
 use rustc_span::Span;
 
 declare_lint! {
@@ -48,18 +47,10 @@ fn maybe_lint_redundant_semis(cx: &EarlyContext<'_>, seq: &mut Option<(Span, boo
             return;
         }
 
-        cx.struct_span_lint(
+        cx.emit_spanned_lint(
             REDUNDANT_SEMICOLONS,
             span,
-            fluent::lint_redundant_semicolons,
-            |lint| {
-                lint.set_arg("multiple", multiple).span_suggestion(
-                    span,
-                    fluent::suggestion,
-                    "",
-                    Applicability::MaybeIncorrect,
-                )
-            },
+            RedundantSemicolonsDiag { multiple, suggestion: span },
         );
     }
 }
diff --git a/compiler/rustc_lint/src/traits.rs b/compiler/rustc_lint/src/traits.rs
index 1b21c2dac37..7ea1a138b7e 100644
--- a/compiler/rustc_lint/src/traits.rs
+++ b/compiler/rustc_lint/src/traits.rs
@@ -1,7 +1,7 @@
+use crate::lints::{DropGlue, DropTraitConstraintsDiag};
 use crate::LateContext;
 use crate::LateLintPass;
 use crate::LintContext;
-use rustc_errors::fluent;
 use rustc_hir as hir;
 use rustc_span::symbol::sym;
 
@@ -101,17 +101,13 @@ impl<'tcx> LateLintPass<'tcx> for DropTraitConstraints {
                 if trait_predicate.trait_ref.self_ty().is_impl_trait() {
                     continue;
                 }
-                let Some(needs_drop) = cx.tcx.get_diagnostic_item(sym::needs_drop) else {
-                    continue;
+                let Some(def_id) = cx.tcx.get_diagnostic_item(sym::needs_drop) else {
+                    return
                 };
-                cx.struct_span_lint(
+                cx.emit_spanned_lint(
                     DROP_BOUNDS,
                     span,
-                    fluent::lint_drop_trait_constraints,
-                    |lint| {
-                        lint.set_arg("predicate", predicate)
-                            .set_arg("needs_drop", cx.tcx.def_path_str(needs_drop))
-                    },
+                    DropTraitConstraintsDiag { predicate, tcx: cx.tcx, def_id },
                 );
             }
         }
@@ -123,12 +119,11 @@ impl<'tcx> LateLintPass<'tcx> for DropTraitConstraints {
         };
         for bound in &bounds[..] {
             let def_id = bound.trait_ref.trait_def_id();
-            if cx.tcx.lang_items().drop_trait() == def_id
-                && let Some(needs_drop) = cx.tcx.get_diagnostic_item(sym::needs_drop)
-            {
-                cx.struct_span_lint(DYN_DROP, bound.span, fluent::lint_drop_glue, |lint| {
-                    lint.set_arg("needs_drop", cx.tcx.def_path_str(needs_drop))
-                });
+            if cx.tcx.lang_items().drop_trait() == def_id {
+                let Some(def_id) = cx.tcx.get_diagnostic_item(sym::needs_drop) else {
+                    return
+                };
+                cx.emit_spanned_lint(DYN_DROP, bound.span, DropGlue { tcx: cx.tcx, def_id });
             }
         }
     }
diff --git a/compiler/rustc_lint/src/types.rs b/compiler/rustc_lint/src/types.rs
index fa415243ba0..f2ab44ac97c 100644
--- a/compiler/rustc_lint/src/types.rs
+++ b/compiler/rustc_lint/src/types.rs
@@ -1,11 +1,16 @@
+use crate::lints::{
+    AtomicOrderingFence, AtomicOrderingLoad, AtomicOrderingStore, ImproperCTypes,
+    InvalidAtomicOrderingDiag, OnlyCastu8ToChar, OverflowingBinHex, OverflowingBinHexSign,
+    OverflowingBinHexSub, OverflowingInt, OverflowingIntHelp, OverflowingLiteral, OverflowingUInt,
+    RangeEndpointOutOfRange, UnusedComparisons, VariantSizeDifferencesDiag,
+};
 use crate::{LateContext, LateLintPass, LintContext};
 use rustc_ast as ast;
 use rustc_attr as attr;
 use rustc_data_structures::fx::FxHashSet;
-use rustc_errors::{fluent, Applicability, DiagnosticMessage};
+use rustc_errors::{fluent, DiagnosticMessage};
 use rustc_hir as hir;
 use rustc_hir::{is_range_literal, Expr, ExprKind, Node};
-use rustc_macros::LintDiagnostic;
 use rustc_middle::ty::layout::{IntegerExt, LayoutOf, SizeSkeleton};
 use rustc_middle::ty::subst::SubstsRef;
 use rustc_middle::ty::{self, AdtKind, DefIdTree, Ty, TyCtxt, TypeSuperVisitable, TypeVisitable};
@@ -146,32 +151,22 @@ fn lint_overflowing_range_endpoint<'tcx>(
     };
     let Ok(start) = cx.sess().source_map().span_to_snippet(eps[0].span) else { return false };
 
-    cx.struct_span_lint(
+    use rustc_ast::{LitIntType, LitKind};
+    let suffix = match lit.node {
+        LitKind::Int(_, LitIntType::Signed(s)) => s.name_str(),
+        LitKind::Int(_, LitIntType::Unsigned(s)) => s.name_str(),
+        LitKind::Int(_, LitIntType::Unsuffixed) => "",
+        _ => bug!(),
+    };
+    cx.emit_spanned_lint(
         OVERFLOWING_LITERALS,
         struct_expr.span,
-        fluent::lint_range_endpoint_out_of_range,
-        |lint| {
-            use ast::{LitIntType, LitKind};
-
-            lint.set_arg("ty", ty);
-
-            // We need to preserve the literal's suffix,
-            // as it may determine typing information.
-            let suffix = match lit.node {
-                LitKind::Int(_, LitIntType::Signed(s)) => s.name_str(),
-                LitKind::Int(_, LitIntType::Unsigned(s)) => s.name_str(),
-                LitKind::Int(_, LitIntType::Unsuffixed) => "",
-                _ => bug!(),
-            };
-            let suggestion = format!("{}..={}{}", start, lit_val - 1, suffix);
-            lint.span_suggestion(
-                struct_expr.span,
-                fluent::suggestion,
-                suggestion,
-                Applicability::MachineApplicable,
-            );
-
-            lint
+        RangeEndpointOutOfRange {
+            ty,
+            suggestion: struct_expr.span,
+            start,
+            literal: lit_val - 1,
+            suffix,
         },
     );
 
@@ -228,58 +223,37 @@ fn report_bin_hex_error(
     val: u128,
     negative: bool,
 ) {
-    cx.struct_span_lint(
-        OVERFLOWING_LITERALS,
-        expr.span,
-        fluent::lint_overflowing_bin_hex,
-        |lint| {
-            let (t, actually) = match ty {
-                attr::IntType::SignedInt(t) => {
-                    let actually = if negative {
-                        -(size.sign_extend(val) as i128)
-                    } else {
-                        size.sign_extend(val) as i128
-                    };
-                    (t.name_str(), actually.to_string())
-                }
-                attr::IntType::UnsignedInt(t) => {
-                    let actually = size.truncate(val);
-                    (t.name_str(), actually.to_string())
-                }
+    let (t, actually) = match ty {
+        attr::IntType::SignedInt(t) => {
+            let actually = if negative {
+                -(size.sign_extend(val) as i128)
+            } else {
+                size.sign_extend(val) as i128
             };
-
-            if negative {
-                // If the value is negative,
-                // emits a note about the value itself, apart from the literal.
-                lint.note(fluent::negative_note);
-                lint.note(fluent::negative_becomes_note);
+            (t.name_str(), actually.to_string())
+        }
+        attr::IntType::UnsignedInt(t) => {
+            let actually = size.truncate(val);
+            (t.name_str(), actually.to_string())
+        }
+    };
+    let sign =
+        if negative { OverflowingBinHexSign::Negative } else { OverflowingBinHexSign::Positive };
+    let sub = get_type_suggestion(cx.typeck_results().node_type(expr.hir_id), val, negative).map(
+        |suggestion_ty| {
+            if let Some(pos) = repr_str.chars().position(|c| c == 'i' || c == 'u') {
+                let (sans_suffix, _) = repr_str.split_at(pos);
+                OverflowingBinHexSub::Suggestion { span: expr.span, suggestion_ty, sans_suffix }
             } else {
-                lint.note(fluent::positive_note);
-            }
-            if let Some(sugg_ty) =
-                get_type_suggestion(cx.typeck_results().node_type(expr.hir_id), val, negative)
-            {
-                lint.set_arg("suggestion_ty", sugg_ty);
-                if let Some(pos) = repr_str.chars().position(|c| c == 'i' || c == 'u') {
-                    let (sans_suffix, _) = repr_str.split_at(pos);
-                    lint.span_suggestion(
-                        expr.span,
-                        fluent::suggestion,
-                        format!("{}{}", sans_suffix, sugg_ty),
-                        Applicability::MachineApplicable,
-                    );
-                } else {
-                    lint.help(fluent::help);
-                }
+                OverflowingBinHexSub::Help { suggestion_ty }
             }
-            lint.set_arg("ty", t)
-                .set_arg("lit", repr_str)
-                .set_arg("dec", val)
-                .set_arg("actually", actually);
-
-            lint
         },
     );
+    cx.emit_spanned_lint(
+        OVERFLOWING_LITERALS,
+        expr.span,
+        OverflowingBinHex { ty: t, lit: repr_str.clone(), dec: val, actually, sign, sub },
+    )
 }
 
 // This function finds the next fitting type and generates a suggestion string.
@@ -363,28 +337,19 @@ fn lint_int_literal<'tcx>(
             return;
         }
 
-        cx.struct_span_lint(OVERFLOWING_LITERALS, e.span, fluent::lint_overflowing_int, |lint| {
-            lint.set_arg("ty", t.name_str())
-                .set_arg(
-                    "lit",
-                    cx.sess()
-                        .source_map()
-                        .span_to_snippet(lit.span)
-                        .expect("must get snippet from literal"),
-                )
-                .set_arg("min", min)
-                .set_arg("max", max)
-                .note(fluent::note);
-
-            if let Some(sugg_ty) =
-                get_type_suggestion(cx.typeck_results().node_type(e.hir_id), v, negative)
-            {
-                lint.set_arg("suggestion_ty", sugg_ty);
-                lint.help(fluent::help);
-            }
-
-            lint
-        });
+        let lit = cx
+            .sess()
+            .source_map()
+            .span_to_snippet(lit.span)
+            .expect("must get snippet from literal");
+        let help = get_type_suggestion(cx.typeck_results().node_type(e.hir_id), v, negative)
+            .map(|suggestion_ty| OverflowingIntHelp { suggestion_ty });
+
+        cx.emit_spanned_lint(
+            OVERFLOWING_LITERALS,
+            e.span,
+            OverflowingInt { ty: t.name_str(), lit, min, max, help },
+        );
     }
 }
 
@@ -408,18 +373,10 @@ fn lint_uint_literal<'tcx>(
             match par_e.kind {
                 hir::ExprKind::Cast(..) => {
                     if let ty::Char = cx.typeck_results().expr_ty(par_e).kind() {
-                        cx.struct_span_lint(
+                        cx.emit_spanned_lint(
                             OVERFLOWING_LITERALS,
                             par_e.span,
-                            fluent::lint_only_cast_u8_to_char,
-                            |lint| {
-                                lint.span_suggestion(
-                                    par_e.span,
-                                    fluent::suggestion,
-                                    format!("'\\u{{{:X}}}'", lit_val),
-                                    Applicability::MachineApplicable,
-                                )
-                            },
+                            OnlyCastu8ToChar { span: par_e.span, literal: lit_val },
                         );
                         return;
                     }
@@ -443,19 +400,20 @@ fn lint_uint_literal<'tcx>(
             );
             return;
         }
-        cx.struct_span_lint(OVERFLOWING_LITERALS, e.span, fluent::lint_overflowing_uint, |lint| {
-            lint.set_arg("ty", t.name_str())
-                .set_arg(
-                    "lit",
-                    cx.sess()
-                        .source_map()
-                        .span_to_snippet(lit.span)
-                        .expect("must get snippet from literal"),
-                )
-                .set_arg("min", min)
-                .set_arg("max", max)
-                .note(fluent::note)
-        });
+        cx.emit_spanned_lint(
+            OVERFLOWING_LITERALS,
+            e.span,
+            OverflowingUInt {
+                ty: t.name_str(),
+                lit: cx
+                    .sess()
+                    .source_map()
+                    .span_to_snippet(lit.span)
+                    .expect("must get snippet from literal"),
+                min,
+                max,
+            },
+        );
     }
 }
 
@@ -484,20 +442,16 @@ fn lint_literal<'tcx>(
                 _ => bug!(),
             };
             if is_infinite == Ok(true) {
-                cx.struct_span_lint(
+                cx.emit_spanned_lint(
                     OVERFLOWING_LITERALS,
                     e.span,
-                    fluent::lint_overflowing_literal,
-                    |lint| {
-                        lint.set_arg("ty", t.name_str())
-                            .set_arg(
-                                "lit",
-                                cx.sess()
-                                    .source_map()
-                                    .span_to_snippet(lit.span)
-                                    .expect("must get snippet from literal"),
-                            )
-                            .note(fluent::note)
+                    OverflowingLiteral {
+                        ty: t.name_str(),
+                        lit: cx
+                            .sess()
+                            .source_map()
+                            .span_to_snippet(lit.span)
+                            .expect("must get snippet from literal"),
                     },
                 );
             }
@@ -517,12 +471,7 @@ impl<'tcx> LateLintPass<'tcx> for TypeLimits {
             }
             hir::ExprKind::Binary(binop, ref l, ref r) => {
                 if is_comparison(binop) && !check_limits(cx, binop, &l, &r) {
-                    cx.struct_span_lint(
-                        UNUSED_COMPARISONS,
-                        e.span,
-                        fluent::lint_unused_comparisons,
-                        |lint| lint,
-                    );
+                    cx.emit_spanned_lint(UNUSED_COMPARISONS, e.span, UnusedComparisons);
                 }
             }
             hir::ExprKind::Lit(ref lit) => lint_literal(cx, self, e, lit),
@@ -878,39 +827,39 @@ impl<'a, 'tcx> ImproperCTypesVisitor<'a, 'tcx> {
     ) -> FfiResult<'tcx> {
         use FfiResult::*;
 
-        if def.repr().transparent() {
+        let transparent_safety = def.repr().transparent().then(|| {
             // Can assume that at most one field is not a ZST, so only check
             // that field's type for FFI-safety.
             if let Some(field) = transparent_newtype_field(self.cx.tcx, variant) {
-                self.check_field_type_for_ffi(cache, field, substs)
+                return self.check_field_type_for_ffi(cache, field, substs);
             } else {
                 // All fields are ZSTs; this means that the type should behave
-                // like (), which is FFI-unsafe
+                // like (), which is FFI-unsafe... except if all fields are PhantomData,
+                // which is tested for below
                 FfiUnsafe { ty, reason: fluent::lint_improper_ctypes_struct_zst, help: None }
             }
-        } else {
-            // We can't completely trust repr(C) markings; make sure the fields are
-            // actually safe.
-            let mut all_phantom = !variant.fields.is_empty();
-            for field in &variant.fields {
-                match self.check_field_type_for_ffi(cache, &field, substs) {
-                    FfiSafe => {
-                        all_phantom = false;
-                    }
-                    FfiPhantom(..) if def.is_enum() => {
-                        return FfiUnsafe {
-                            ty,
-                            reason: fluent::lint_improper_ctypes_enum_phantomdata,
-                            help: None,
-                        };
-                    }
-                    FfiPhantom(..) => {}
-                    r => return r,
+        });
+        // We can't completely trust repr(C) markings; make sure the fields are
+        // actually safe.
+        let mut all_phantom = !variant.fields.is_empty();
+        for field in &variant.fields {
+            match self.check_field_type_for_ffi(cache, &field, substs) {
+                FfiSafe => {
+                    all_phantom = false;
+                }
+                FfiPhantom(..) if !def.repr().transparent() && def.is_enum() => {
+                    return FfiUnsafe {
+                        ty,
+                        reason: fluent::lint_improper_ctypes_enum_phantomdata,
+                        help: None,
+                    };
                 }
+                FfiPhantom(..) => {}
+                r => return transparent_safety.unwrap_or(r),
             }
-
-            if all_phantom { FfiPhantom(ty) } else { FfiSafe }
         }
+
+        if all_phantom { FfiPhantom(ty) } else { transparent_safety.unwrap_or(FfiSafe) }
     }
 
     /// Checks if the given type is "ffi-safe" (has a stable, well-defined
@@ -1174,26 +1123,21 @@ impl<'a, 'tcx> ImproperCTypesVisitor<'a, 'tcx> {
             CItemKind::Declaration => IMPROPER_CTYPES,
             CItemKind::Definition => IMPROPER_CTYPES_DEFINITIONS,
         };
-
-        self.cx.struct_span_lint(lint, sp, fluent::lint_improper_ctypes, |lint| {
-            let item_description = match self.mode {
-                CItemKind::Declaration => "block",
-                CItemKind::Definition => "fn",
+        let desc = match self.mode {
+            CItemKind::Declaration => "block",
+            CItemKind::Definition => "fn",
+        };
+        let span_note = if let ty::Adt(def, _) = ty.kind()
+            && let Some(sp) = self.cx.tcx.hir().span_if_local(def.did()) {
+                Some(sp)
+            } else {
+                None
             };
-            lint.set_arg("ty", ty);
-            lint.set_arg("desc", item_description);
-            lint.span_label(sp, fluent::label);
-            if let Some(help) = help {
-                lint.help(help);
-            }
-            lint.note(note);
-            if let ty::Adt(def, _) = ty.kind() {
-                if let Some(sp) = self.cx.tcx.hir().span_if_local(def.did()) {
-                    lint.span_note(sp, fluent::note);
-                }
-            }
-            lint
-        });
+        self.cx.emit_spanned_lint(
+            lint,
+            sp,
+            ImproperCTypes { ty, desc, label: sp, help, note, span_note },
+        );
     }
 
     fn check_for_opaque_ty(&mut self, sp: Span, ty: Ty<'tcx>) -> bool {
@@ -1397,11 +1341,10 @@ impl<'tcx> LateLintPass<'tcx> for VariantSizeDifferences {
             // We only warn if the largest variant is at least thrice as large as
             // the second-largest.
             if largest > slargest * 3 && slargest > 0 {
-                cx.struct_span_lint(
+                cx.emit_spanned_lint(
                     VARIANT_SIZE_DIFFERENCES,
                     enum_definition.variants[largest_index].span,
-                    fluent::lint_variant_size_differences,
-                    |lint| lint.set_arg("largest", largest),
+                    VariantSizeDifferencesDiag { largest },
                 );
             }
         }
@@ -1509,17 +1452,19 @@ impl InvalidAtomicOrdering {
 
     fn check_atomic_load_store(cx: &LateContext<'_>, expr: &Expr<'_>) {
         if let Some((method, args)) = Self::inherent_atomic_method_call(cx, expr, &[sym::load, sym::store])
-            && let Some((ordering_arg, invalid_ordering, msg)) = match method {
-                sym::load => Some((&args[0], sym::Release, fluent::lint_atomic_ordering_load)),
-                sym::store => Some((&args[1], sym::Acquire, fluent::lint_atomic_ordering_store)),
+            && let Some((ordering_arg, invalid_ordering)) = match method {
+                sym::load => Some((&args[0], sym::Release)),
+                sym::store => Some((&args[1], sym::Acquire)),
                 _ => None,
             }
             && let Some(ordering) = Self::match_ordering(cx, ordering_arg)
             && (ordering == invalid_ordering || ordering == sym::AcqRel)
         {
-            cx.struct_span_lint(INVALID_ATOMIC_ORDERING, ordering_arg.span, msg, |lint| {
-                lint.help(fluent::help)
-            });
+            if method == sym::load {
+                cx.emit_spanned_lint(INVALID_ATOMIC_ORDERING, ordering_arg.span, AtomicOrderingLoad);
+            } else {
+                cx.emit_spanned_lint(INVALID_ATOMIC_ORDERING, ordering_arg.span, AtomicOrderingStore);
+            };
         }
     }
 
@@ -1530,10 +1475,7 @@ impl InvalidAtomicOrdering {
             && matches!(cx.tcx.get_diagnostic_name(def_id), Some(sym::fence | sym::compiler_fence))
             && Self::match_ordering(cx, &args[0]) == Some(sym::Relaxed)
         {
-            cx.struct_span_lint(INVALID_ATOMIC_ORDERING, args[0].span, fluent::lint_atomic_ordering_fence, |lint| {
-                lint
-                    .help(fluent::help)
-            });
+            cx.emit_spanned_lint(INVALID_ATOMIC_ORDERING, args[0].span, AtomicOrderingFence);
         }
     }
 
@@ -1550,15 +1492,6 @@ impl InvalidAtomicOrdering {
         let Some(fail_ordering) = Self::match_ordering(cx, fail_order_arg) else { return };
 
         if matches!(fail_ordering, sym::Release | sym::AcqRel) {
-            #[derive(LintDiagnostic)]
-            #[diag(lint_atomic_ordering_invalid)]
-            #[help]
-            struct InvalidAtomicOrderingDiag {
-                method: Symbol,
-                #[label]
-                fail_order_arg_span: Span,
-            }
-
             cx.emit_spanned_lint(
                 INVALID_ATOMIC_ORDERING,
                 fail_order_arg.span,
diff --git a/compiler/rustc_lint/src/unused.rs b/compiler/rustc_lint/src/unused.rs
index 525079681ca..ac2b32b44e6 100644
--- a/compiler/rustc_lint/src/unused.rs
+++ b/compiler/rustc_lint/src/unused.rs
@@ -1,9 +1,14 @@
+use crate::lints::{
+    PathStatementDrop, PathStatementDropSub, PathStatementNoEffect, UnusedAllocationDiag,
+    UnusedAllocationMutDiag, UnusedClosure, UnusedDef, UnusedDelim, UnusedDelimSuggestion,
+    UnusedGenerator, UnusedImportBracesDiag, UnusedOp, UnusedResult,
+};
 use crate::Lint;
 use crate::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};
 use rustc_ast as ast;
 use rustc_ast::util::{classify, parser};
 use rustc_ast::{ExprKind, StmtKind};
-use rustc_errors::{fluent, pluralize, Applicability, MultiSpan};
+use rustc_errors::{pluralize, MultiSpan};
 use rustc_hir as hir;
 use rustc_hir::def::{DefKind, Res};
 use rustc_hir::def_id::DefId;
@@ -163,23 +168,20 @@ impl<'tcx> LateLintPass<'tcx> for UnusedResults {
         let mut op_warned = false;
 
         if let Some(must_use_op) = must_use_op {
-            cx.struct_span_lint(UNUSED_MUST_USE, expr.span, fluent::lint_unused_op, |lint| {
-                lint.set_arg("op", must_use_op)
-                    .span_label(expr.span, fluent::label)
-                    .span_suggestion_verbose(
-                        expr.span.shrink_to_lo(),
-                        fluent::suggestion,
-                        "let _ = ",
-                        Applicability::MachineApplicable,
-                    )
-            });
+            cx.emit_spanned_lint(
+                UNUSED_MUST_USE,
+                expr.span,
+                UnusedOp {
+                    op: must_use_op,
+                    label: expr.span,
+                    suggestion: expr.span.shrink_to_lo(),
+                },
+            );
             op_warned = true;
         }
 
         if !(type_lint_emitted_or_suppressed || fn_warned || op_warned) {
-            cx.struct_span_lint(UNUSED_RESULTS, s.span, fluent::lint_unused_result, |lint| {
-                lint.set_arg("ty", ty)
-            });
+            cx.emit_spanned_lint(UNUSED_RESULTS, s.span, UnusedResult { ty });
         }
 
         fn check_fn_must_use(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool {
@@ -402,47 +404,31 @@ impl<'tcx> LateLintPass<'tcx> for UnusedResults {
                     );
                 }
                 MustUsePath::Closure(span) => {
-                    cx.struct_span_lint(
+                    cx.emit_spanned_lint(
                         UNUSED_MUST_USE,
                         *span,
-                        fluent::lint_unused_closure,
-                        |lint| {
-                            // FIXME(davidtwco): this isn't properly translatable because of the
-                            // pre/post strings
-                            lint.set_arg("count", plural_len)
-                                .set_arg("pre", descr_pre)
-                                .set_arg("post", descr_post)
-                                .note(fluent::note)
-                        },
+                        UnusedClosure { count: plural_len, pre: descr_pre, post: descr_post },
                     );
                 }
                 MustUsePath::Generator(span) => {
-                    cx.struct_span_lint(
+                    cx.emit_spanned_lint(
                         UNUSED_MUST_USE,
                         *span,
-                        fluent::lint_unused_generator,
-                        |lint| {
-                            // FIXME(davidtwco): this isn't properly translatable because of the
-                            // pre/post strings
-                            lint.set_arg("count", plural_len)
-                                .set_arg("pre", descr_pre)
-                                .set_arg("post", descr_post)
-                                .note(fluent::note)
-                        },
+                        UnusedGenerator { count: plural_len, pre: descr_pre, post: descr_post },
                     );
                 }
                 MustUsePath::Def(span, def_id, reason) => {
-                    cx.struct_span_lint(UNUSED_MUST_USE, *span, fluent::lint_unused_def, |lint| {
-                        // FIXME(davidtwco): this isn't properly translatable because of the pre/post
-                        // strings
-                        lint.set_arg("pre", descr_pre);
-                        lint.set_arg("post", descr_post);
-                        lint.set_arg("def", cx.tcx.def_path_str(*def_id));
-                        if let Some(note) = reason {
-                            lint.note(note.as_str());
-                        }
-                        lint
-                    });
+                    cx.emit_spanned_lint(
+                        UNUSED_MUST_USE,
+                        *span,
+                        UnusedDef {
+                            pre: descr_pre,
+                            post: descr_post,
+                            cx,
+                            def_id: *def_id,
+                            note: *reason,
+                        },
+                    );
                 }
             }
         }
@@ -478,31 +464,15 @@ impl<'tcx> LateLintPass<'tcx> for PathStatements {
             if let hir::ExprKind::Path(_) = expr.kind {
                 let ty = cx.typeck_results().expr_ty(expr);
                 if ty.needs_drop(cx.tcx, cx.param_env) {
-                    cx.struct_span_lint(
-                        PATH_STATEMENTS,
-                        s.span,
-                        fluent::lint_path_statement_drop,
-                        |lint| {
-                            if let Ok(snippet) = cx.sess().source_map().span_to_snippet(expr.span) {
-                                lint.span_suggestion(
-                                    s.span,
-                                    fluent::suggestion,
-                                    format!("drop({});", snippet),
-                                    Applicability::MachineApplicable,
-                                );
-                            } else {
-                                lint.span_help(s.span, fluent::suggestion);
-                            }
-                            lint
-                        },
-                    );
+                    let sub = if let Ok(snippet) = cx.sess().source_map().span_to_snippet(expr.span)
+                    {
+                        PathStatementDropSub::Suggestion { span: s.span, snippet }
+                    } else {
+                        PathStatementDropSub::Help { span: s.span }
+                    };
+                    cx.emit_spanned_lint(PATH_STATEMENTS, s.span, PathStatementDrop { sub })
                 } else {
-                    cx.struct_span_lint(
-                        PATH_STATEMENTS,
-                        s.span,
-                        fluent::lint_path_statement_no_effect,
-                        |lint| lint,
-                    );
+                    cx.emit_spanned_lint(PATH_STATEMENTS, s.span, PathStatementNoEffect);
                 }
             }
         }
@@ -695,36 +665,35 @@ trait UnusedDelimLint {
         } else {
             MultiSpan::from(value_span)
         };
-        cx.struct_span_lint(self.lint(), primary_span, fluent::lint_unused_delim, |lint| {
-            lint.set_arg("delim", Self::DELIM_STR);
-            lint.set_arg("item", msg);
-            if let Some((lo, hi)) = spans {
-                let sm = cx.sess().source_map();
-                let lo_replace =
+        let suggestion = spans.map(|(lo, hi)| {
+            let sm = cx.sess().source_map();
+            let lo_replace =
                     if keep_space.0 &&
                         let Ok(snip) = sm.span_to_prev_source(lo) && !snip.ends_with(' ') {
-                        " ".to_string()
+                        " "
                         } else {
-                            "".to_string()
+                            ""
                         };
 
-                let hi_replace =
+            let hi_replace =
                     if keep_space.1 &&
                         let Ok(snip) = sm.span_to_next_source(hi) && !snip.starts_with(' ') {
-                        " ".to_string()
+                        " "
                         } else {
-                            "".to_string()
+                            ""
                         };
-
-                let replacement = vec![(lo, lo_replace), (hi, hi_replace)];
-                lint.multipart_suggestion(
-                    fluent::suggestion,
-                    replacement,
-                    Applicability::MachineApplicable,
-                );
+            UnusedDelimSuggestion {
+                start_span: lo,
+                start_replace: lo_replace,
+                end_span: hi,
+                end_replace: hi_replace,
             }
-            lint
         });
+        cx.emit_spanned_lint(
+            self.lint(),
+            primary_span,
+            UnusedDelim { delim: Self::DELIM_STR, item: msg, suggestion },
+        );
     }
 
     fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &ast::Expr) {
@@ -1297,11 +1266,10 @@ impl UnusedImportBraces {
                 ast::UseTreeKind::Nested(_) => return,
             };
 
-            cx.struct_span_lint(
+            cx.emit_spanned_lint(
                 UNUSED_IMPORT_BRACES,
                 item.span,
-                fluent::lint_unused_import_braces,
-                |lint| lint.set_arg("node", node_name),
+                UnusedImportBracesDiag { node: node_name },
             );
         }
     }
@@ -1351,17 +1319,14 @@ impl<'tcx> LateLintPass<'tcx> for UnusedAllocation {
 
         for adj in cx.typeck_results().expr_adjustments(e) {
             if let adjustment::Adjust::Borrow(adjustment::AutoBorrow::Ref(_, m)) = adj.kind {
-                cx.struct_span_lint(
-                    UNUSED_ALLOCATION,
-                    e.span,
-                    match m {
-                        adjustment::AutoBorrowMutability::Not => fluent::lint_unused_allocation,
-                        adjustment::AutoBorrowMutability::Mut { .. } => {
-                            fluent::lint_unused_allocation_mut
-                        }
-                    },
-                    |lint| lint,
-                );
+                match m {
+                    adjustment::AutoBorrowMutability::Not => {
+                        cx.emit_spanned_lint(UNUSED_ALLOCATION, e.span, UnusedAllocationDiag);
+                    }
+                    adjustment::AutoBorrowMutability::Mut { .. } => {
+                        cx.emit_spanned_lint(UNUSED_ALLOCATION, e.span, UnusedAllocationMutDiag);
+                    }
+                };
             }
         }
     }