about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--compiler/rustc_ast_passes/src/ast_validation.rs2
-rw-r--r--compiler/rustc_expand/src/base.rs18
-rw-r--r--compiler/rustc_lint/messages.ftl178
-rw-r--r--compiler/rustc_lint/src/context.rs21
-rw-r--r--compiler/rustc_lint/src/context/diagnostics.rs767
-rw-r--r--compiler/rustc_lint/src/context/diagnostics/check_cfg.rs16
-rw-r--r--compiler/rustc_lint/src/early.rs4
-rw-r--r--compiler/rustc_lint/src/lints.rs601
-rw-r--r--compiler/rustc_lint_defs/src/lib.rs19
-rw-r--r--compiler/rustc_metadata/src/creader.rs2
-rw-r--r--compiler/rustc_middle/src/middle/stability.rs21
-rw-r--r--compiler/rustc_resolve/src/check_unused.rs34
-rw-r--r--compiler/rustc_resolve/src/macros.rs5
-rw-r--r--tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.rs2
-rw-r--r--tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.stderr2
-rw-r--r--tests/ui/lint/anonymous-reexport.stderr2
-rw-r--r--tests/ui/lint/unused/import_remove_line.stderr2
-rw-r--r--tests/ui/lint/unused/lint-unused-imports.rs2
-rw-r--r--tests/ui/lint/unused/lint-unused-imports.stderr2
-rw-r--r--tests/ui/malformed/malformed-regressions.rs6
-rw-r--r--tests/ui/malformed/malformed-regressions.stderr6
-rw-r--r--tests/ui/privacy/issue-46209-private-enum-variant-reexport.rs2
-rw-r--r--tests/ui/privacy/issue-46209-private-enum-variant-reexport.stderr2
-rw-r--r--tests/ui/span/multispan-import-lint.stderr2
-rw-r--r--tests/ui/suggestions/unused-imports.stderr4
-rw-r--r--tests/ui/use/use-nested-groups-unused-imports.rs2
-rw-r--r--tests/ui/use/use-nested-groups-unused-imports.stderr2
27 files changed, 1176 insertions, 550 deletions
diff --git a/compiler/rustc_ast_passes/src/ast_validation.rs b/compiler/rustc_ast_passes/src/ast_validation.rs
index c287f6e4b3d..b1158dc6fca 100644
--- a/compiler/rustc_ast_passes/src/ast_validation.rs
+++ b/compiler/rustc_ast_passes/src/ast_validation.rs
@@ -1510,7 +1510,7 @@ impl<'a> Visitor<'a> for AstValidator<'a> {
                 DEPRECATED_WHERE_CLAUSE_LOCATION,
                 item.id,
                 err.span,
-                BuiltinLintDiag::DeprecatedWhereclauseLocation(sugg),
+                BuiltinLintDiag::DeprecatedWhereclauseLocation(err.span, sugg),
             );
         }
 
diff --git a/compiler/rustc_expand/src/base.rs b/compiler/rustc_expand/src/base.rs
index c7ca515afc4..77db801c76f 100644
--- a/compiler/rustc_expand/src/base.rs
+++ b/compiler/rustc_expand/src/base.rs
@@ -1364,17 +1364,15 @@ fn pretty_printing_compatibility_hack(item: &Item, sess: &Session) -> bool {
                             };
 
                             if crate_matches {
-                                // FIXME: make this translatable
-                                #[allow(rustc::untranslatable_diagnostic)]
                                 sess.psess.buffer_lint_with_diagnostic(
-                                        PROC_MACRO_BACK_COMPAT,
-                                        item.ident.span,
-                                        ast::CRATE_NODE_ID,
-                                        BuiltinLintDiag::ProcMacroBackCompat(
-                                        "older versions of the `rental` crate will stop compiling in future versions of Rust; \
-                                        please update to `rental` v0.5.6, or switch to one of the `rental` alternatives".to_string()
-                                        )
-                                    );
+                                    PROC_MACRO_BACK_COMPAT,
+                                    item.ident.span,
+                                    ast::CRATE_NODE_ID,
+                                    BuiltinLintDiag::ProcMacroBackCompat {
+                                        crate_name: "rental".to_string(),
+                                        fixed_version: "0.5.6".to_string(),
+                                    },
+                                );
                                 return true;
                             }
                         }
diff --git a/compiler/rustc_lint/messages.ftl b/compiler/rustc_lint/messages.ftl
index 7abba3bb5c4..0cd6f1caebf 100644
--- a/compiler/rustc_lint/messages.ftl
+++ b/compiler/rustc_lint/messages.ftl
@@ -1,7 +1,20 @@
+lint_abs_path_with_module = absolute paths must start with `self`, `super`, `crate`, or an external crate name in the 2018 edition
+    .suggestion = use `crate`
+
+lint_ambiguous_glob_reexport = ambiguous glob re-exports
+    .label_first_reexport = the name `{$name}` in the {$namespace} namespace is first re-exported here
+    .label_duplicate_reexport = but the name `{$name}` in the {$namespace} namespace is also re-exported here
+
 lint_ambiguous_wide_pointer_comparisons = ambiguous wide pointer comparison, the comparison includes metadata which may not be expected
     .addr_metadata_suggestion = use explicit `std::ptr::eq` method to compare metadata and addresses
     .addr_suggestion = use `std::ptr::addr_eq` or untyped pointers to only compare their addresses
 
+lint_associated_const_elided_lifetime = {$elided ->
+        [true] `&` without an explicit lifetime name cannot be used here
+        *[false] `'_` cannot be used here
+    }
+    .suggestion = use the `'static` lifetime
+
 lint_async_fn_in_trait = use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified
     .note = you can suppress this lint if you plan to use the trait only in your own code, or do not care about auto traits like `Send` on the `Future`
     .suggestion = you can alternatively desugar to a normal `fn` that returns `impl Future` and add any desired bounds such as `Send`, but these cannot be relaxed without a breaking API change
@@ -19,10 +32,19 @@ lint_atomic_ordering_load = atomic loads cannot have `Release` or `AcqRel` order
 lint_atomic_ordering_store = atomic stores cannot have `Acquire` or `AcqRel` ordering
     .help = consider using ordering modes `Release`, `SeqCst` or `Relaxed`
 
+lint_avoid_att_syntax =
+    avoid using `.att_syntax`, prefer using `options(att_syntax)` instead
+
+lint_avoid_intel_syntax =
+    avoid using `.intel_syntax`, Intel syntax is the default
+
 lint_bad_attribute_argument = bad attribute argument
 
 lint_bad_opt_access = {$msg}
 
+lint_break_with_label_and_loop = this labeled break expression is easy to confuse with an unlabeled break with a labeled value expression
+    .suggestion = wrap this expression in parentheses
+
 lint_builtin_allow_internal_unsafe =
     `allow_internal_unsafe` allows defining macros using unsafe without triggering the `unsafe_code` lint at their call site
 
@@ -156,6 +178,9 @@ lint_builtin_unused_doc_comment = unused doc comment
 lint_builtin_while_true = denote infinite loops with `loop {"{"} ... {"}"}`
     .suggestion = use `loop`
 
+lint_byte_slice_in_packed_struct_with_derive = {$ty} slice in a packed struct that derives a built-in trait
+    .help = consider implementing the trait by hand, or remove the `packed` attribute
+
 lint_cfg_attr_no_attributes =
     `#[cfg_attr]` does not expand to any attributes
 
@@ -179,6 +204,8 @@ lint_cstring_ptr = getting the inner pointer of a temporary `CString`
     .note = pointers do not have a lifetime; when calling `as_ptr` the `CString` will be deallocated at the end of the statement because nothing is referencing it as far as the type system is concerned
     .help = for more information, see https://doc.rust-lang.org/reference/destructors.html
 
+lint_custom_inner_attribute_unstable = custom inner attributes are unstable
+
 lint_default_hash_types = prefer `{$preferred}` over `{$used}`, it has better performance
     .note = a `use rustc_data_structures::fx::{$preferred}` may be necessary
 
@@ -189,8 +216,10 @@ lint_deprecated_lint_name =
     .suggestion = change it to
     .help = change it to {$replace}
 
-lint_deprecated_where_clause_location =
-    where clause not allowed here
+lint_deprecated_where_clause_location = where clause not allowed here
+    .note = see issue #89122 <https://github.com/rust-lang/rust/issues/89122> for more information
+    .suggestion_move_to_end = move it to the end of the type declaration
+    .suggestion_remove_where = remove this `where`
 
 lint_diag_out_of_impl =
     diagnostics should only be created in `Diagnostic`/`Subdiagnostic`/`LintDiagnostic` impls
@@ -209,6 +238,11 @@ lint_dropping_references = calls to `std::mem::drop` with a reference instead of
     .label = argument has type `{$arg_ty}`
     .note = use `let _ = ...` to ignore the expression or result
 
+lint_duplicate_macro_attribute =
+    duplicated attribute
+
+lint_duplicate_matcher_binding = duplicate matcher binding
+
 lint_enum_intrinsics_mem_discriminant =
     the return value of `mem::discriminant` is unspecified when called with a non-enum type
     .note = the argument to `discriminant` should be a reference to an enum, but it was passed a reference to a `{$ty_param}`, which is not an enum.
@@ -221,7 +255,12 @@ lint_expectation = this lint expectation is unfulfilled
     .note = the `unfulfilled_lint_expectations` lint can't be expected and will always produce this message
     .rationale = {$rationale}
 
+lint_extern_crate_not_idiomatic = `extern crate` is not idiomatic in the new edition
+    .suggestion = convert it to a `use`
+
 lint_extern_without_abi = extern declarations without an explicit ABI are deprecated
+    .label = ABI should be specified here
+    .help = the default ABI is {$default_abi}
 
 lint_for_loops_over_fallibles =
     for loop over {$article} `{$ty}`. This is more readably written as an `if let` statement
@@ -237,6 +276,12 @@ lint_forgetting_references = calls to `std::mem::forget` with a reference instea
     .label = argument has type `{$arg_ty}`
     .note = use `let _ = ...` to ignore the expression or result
 
+lint_hidden_glob_reexport = private item shadows public glob re-export
+    .note_glob_reexport = the name `{$name}` in the {$namespace} namespace is supposed to be publicly re-exported here
+    .note_private_item = but the private item here shadows it
+
+lint_hidden_lifetime_parameters = hidden lifetime parameters in types are deprecated
+
 lint_hidden_unicode_codepoints = unicode codepoint changing visible direction of text present in {$label}
     .label = this {$label} contains {$count ->
         [one] an invisible
@@ -278,6 +323,11 @@ lint_identifier_uncommon_codepoints = identifier contains {$codepoints_len ->
 
 lint_ignored_unless_crate_specified = {$level}({$name}) is ignored unless specified at crate level
 
+lint_ill_formed_attribute_input = {$num_suggestions ->
+        [1] attribute must be of the form {$suggestions}
+        *[other] valid forms for the attribute are {$suggestions}
+    }
+
 lint_impl_trait_overcaptures = `{$self_ty}` will capture more lifetimes than possibly intended in edition 2024
     .note = specifically, {$num_captured ->
         [one] this lifetime is
@@ -348,6 +398,14 @@ lint_improper_ctypes_union_layout_help = consider adding a `#[repr(C)]` or `#[re
 lint_improper_ctypes_union_layout_reason = this union has unspecified layout
 lint_improper_ctypes_union_non_exhaustive = this union is non-exhaustive
 
+lint_incomplete_include =
+    include macro expected single expression in source
+
+lint_inner_macro_attribute_unstable = inner macro attributes are unstable
+
+lint_invalid_crate_type_value = invalid `crate_type` value
+    .suggestion = did you mean
+
 # FIXME: we should ordinalize $valid_up_to when we add support for doing so
 lint_invalid_from_utf8_checked = calls to `{$method}` with a invalid literal always return an error
     .label = the literal was valid UTF-8 up to the {$valid_up_to} bytes
@@ -376,9 +434,22 @@ lint_invalid_reference_casting_note_book = for more information, visit <https://
 
 lint_invalid_reference_casting_note_ty_has_interior_mutability = even for types with interior mutability, the only legal way to obtain a mutable pointer from a shared reference is through `UnsafeCell::get`
 
+lint_legacy_derive_helpers = derive helper attribute is used before it is introduced
+    .label = the attribute is introduced here
+
 lint_lintpass_by_hand = implementing `LintPass` by hand
     .help = try using `declare_lint_pass!` or `impl_lint_pass!` instead
 
+lint_macro_expanded_macro_exports_accessed_by_absolute_paths = macro-expanded `macro_export` macros from the current crate cannot be referred to by absolute paths
+    .note = the macro is defined here
+
+lint_macro_is_private = macro `{$ident}` is private
+
+lint_macro_rule_never_used = rule #{$n} of macro `{$name}` is never used
+
+lint_macro_use_deprecated =
+    deprecated `#[macro_use]` attribute used to import macros should be replaced at use sites with a `use` item to import the macro instead
+
 lint_malformed_attribute = malformed lint attribute input
 
 lint_map_unit_fn = `Iterator::map` call that discard the iterator's values
@@ -388,6 +459,12 @@ lint_map_unit_fn = `Iterator::map` call that discard the iterator's values
     .map_label = after this call to map, the resulting iterator is `impl Iterator<Item = ()>`, which means the only information carried by the iterator is the number of items
     .suggestion = you might have meant to use `Iterator::for_each`
 
+lint_metavariable_still_repeating = variable '{$name}' is still repeating at this depth
+
+lint_metavariable_wrong_operator = meta-variable repeats with different Kleene operator
+
+lint_missing_fragment_specifier = missing fragment specifier
+
 lint_mixed_script_confusables =
     the usage of Script Group `{$set}` in this crate consists solely of mixed script confusables
     .includes_note = the usage includes {$includes}
@@ -395,6 +472,11 @@ lint_mixed_script_confusables =
 
 lint_multiple_supertrait_upcastable = `{$ident}` is object-safe and has multiple supertraits
 
+lint_named_argument_used_positionally = named argument `{$named_arg_name}` is not used by name
+    .label_named_arg = this named argument is referred to by position in formatting string
+    .label_position_arg = this formatting argument uses named argument `{$named_arg_name}` by position
+    .suggestion = use the named argument by name to avoid ambiguity
+
 lint_node_source = `forbid` level set here
     .note = {$reason}
 
@@ -506,6 +588,9 @@ lint_opaque_hidden_inferred_bound = opaque type `{$ty}` does not satisfy its ass
 
 lint_opaque_hidden_inferred_bound_sugg = add this bound
 
+lint_or_patterns_back_compat = the meaning of the `pat` fragment specifier is changing in Rust 2021, which may affect this macro
+    .suggestion = use pat_param to preserve semantics
+
 lint_overflowing_bin_hex = literal out of range for `{$ty}`
     .negative_note = the literal `{$lit}` (decimal `{$dec}`) does not fit into the type `{$ty}`
     .negative_becomes_note = and the value `-{$lit}` will become `{$actually}{$ty}`
@@ -541,6 +626,15 @@ lint_pattern_in_bodiless = patterns aren't allowed in functions without bodies
 lint_pattern_in_foreign = patterns aren't allowed in foreign function declarations
     .label = pattern not allowed in foreign function
 
+lint_private_extern_crate_reexport =
+    extern crate `{$ident}` is private, and cannot be re-exported (error E0365), consider declaring with `pub`
+
+lint_proc_macro_back_compat = using an old version of `{$crate_name}`
+    .note = older versions of the `{$crate_name}` crate will stop compiling in future versions of Rust; please update to `{$crate_name}` v{$fixed_version}, or switch to one of the `{$crate_name}` alternatives
+
+lint_proc_macro_derive_resolution_fallback = cannot find {$ns} `{$ident}` in this scope
+    .label = names from parent modules are not accessible without an explicit import
+
 lint_ptr_null_checks_fn_ptr = function pointers are not nullable, so checking them for null will always return false
     .help = wrap the function pointer inside an `Option` and use `Option::is_none` to check for null pointer value
     .label = expression has type `{$orig_ty}`
@@ -562,6 +656,16 @@ lint_reason_must_be_string_literal = reason must be a string literal
 
 lint_reason_must_come_last = reason in lint attribute must come last
 
+lint_redundant_import = the item `{$ident}` is imported redundantly
+    .label_imported_here = the item `{ident}` is already imported here
+    .label_defined_here = the item `{ident}` is already defined here
+    .label_imported_prelude = the item `{ident}` is already imported by the extern prelude
+    .label_defined_prelude = the item `{ident}` is already defined by the extern prelude
+
+lint_redundant_import_visibility = glob import doesn't reexport anything with visibility `{$import_vis}` because no imported item is public enough
+    .note = the most public imported item is `{$max_vis}`
+    .help = reduce the glob import's visibility or increase visibility of imported items
+
 lint_redundant_semicolons =
     unnecessary trailing {$multiple ->
         [true] semicolons
@@ -572,6 +676,8 @@ lint_redundant_semicolons =
         *[false] this semicolon
     }
 
+lint_remove_mut_from_pattern = remove `mut` from the parameter
+
 lint_removed_lint = lint `{$name}` has been removed: {$reason}
 
 lint_renamed_lint = lint `{$name}` has been renamed to `{$replace}`
@@ -580,6 +686,10 @@ lint_renamed_lint = lint `{$name}` has been renamed to `{$replace}`
 
 lint_requested_level = requested on the command line with `{$level} {$lint_name}`
 
+lint_reserved_prefix = prefix `{$prefix}` is unknown
+    .label = unknown prefix
+    .suggestion = insert whitespace here to avoid this being parsed as a prefix in Rust 2021
+
 lint_shadowed_into_iter =
     this method call resolves to `<&{$target} as IntoIterator>::into_iter` (due to backwards compatibility), but will resolve to `<{$target} as IntoIterator>::into_iter` in Rust {$edition}
     .use_iter_suggestion = use `.iter()` instead of `.into_iter()` to avoid ambiguity
@@ -587,6 +697,11 @@ lint_shadowed_into_iter =
     .use_explicit_into_iter_suggestion =
         or use `IntoIterator::into_iter(..)` instead of `.into_iter()` to explicitly iterate by value
 
+lint_single_use_lifetime = lifetime parameter `{$ident}` only used once
+    .label_param = this lifetime...
+    .label_use = ...is used only here
+    .suggestion = elide the single-use lifetime
+
 lint_span_use_eq_ctxt = use `.eq_ctxt()` instead of `.ctxt() == .ctxt()`
 
 lint_supertrait_as_deref_target = this `Deref` implementation is covered by an implicit supertrait coercion
@@ -600,6 +715,10 @@ lint_suspicious_double_ref_clone =
 lint_suspicious_double_ref_deref =
     using `.deref()` on a double reference, which returns `{$ty}` instead of dereferencing the inner type
 
+lint_trailing_semi_macro = trailing semicolon in macro used in expression position
+    .note1 = macro invocations at the end of a block are treated as expressions
+    .note2 = to ignore the value produced by the macro, add a semicolon after the invocation of `{$name}`
+
 lint_ty_qualified = usage of qualified `ty::{$ty}`
     .suggestion = try importing it and using it unqualified
 
@@ -621,6 +740,7 @@ lint_unexpected_cfg_define_features = consider defining some features in `Cargo.
 lint_unexpected_cfg_doc_cargo = see <https://doc.rust-lang.org/nightly/rustc/check-cfg/cargo-specifics.html> for more information about checking conditional configuration
 lint_unexpected_cfg_doc_rustc = see <https://doc.rust-lang.org/nightly/rustc/check-cfg.html> for more information about checking conditional configuration
 
+lint_unexpected_cfg_name = unexpected `cfg` condition name: `{$name}`
 lint_unexpected_cfg_name_expected_names = expected names are: {$possibilities}{$and_more ->
         [0] {""}
         *[other] {" "}and {$and_more} more
@@ -632,6 +752,10 @@ lint_unexpected_cfg_name_similar_name_no_value = there is a config with a simila
 lint_unexpected_cfg_name_similar_name_value = there is a config with a similar name and value
 lint_unexpected_cfg_name_with_similar_value = found config with similar value
 
+lint_unexpected_cfg_value = unexpected `cfg` condition value: {$has_value ->
+        [true] `{$value}`
+        *[false] (none)
+    }
 lint_unexpected_cfg_value_add_feature = consider adding `{$value}` as a feature in `Cargo.toml`
 lint_unexpected_cfg_value_expected_values = expected values for `{$name}` are: {$have_none_possibility ->
         [true] {"(none), "}
@@ -650,9 +774,22 @@ lint_unexpected_cfg_value_specify_value = specify a config value
 lint_ungated_async_fn_track_caller = `#[track_caller]` on async functions is a no-op
      .label = this function will not propagate the caller location
 
+lint_unicode_text_flow = unicode codepoint changing visible direction of text present in comment
+    .label = {$num_codepoints ->
+            [1] this comment contains an invisible unicode text flow control codepoint
+            *[other] this comment contains invisible unicode text flow control codepoints
+        }
+    .note = these kind of unicode codepoints change the way text flows on applications that support them, but can cause confusion because they change the order of characters on the screen
+    .suggestion = if their presence wasn't intentional, you can remove them
+    .label_comment_char = {$c_debug}
+
+
 lint_unit_bindings = binding has unit type `()`
     .label = this pattern is inferred to be the unit type `()`
 
+lint_unknown_diagnostic_attribute = unknown diagnostic attribute
+lint_unknown_diagnostic_attribute_typo_sugg = an attribute with a similar name exists
+
 lint_unknown_gated_lint =
     unknown lint: `{$name}`
     .note = the `{$name}` lint is unstable
@@ -668,11 +805,16 @@ lint_unknown_lint =
         *[false] did you mean: `{$replace}`
     }
 
+lint_unknown_macro_variable = unknown macro variable `{$name}`
+
 lint_unknown_tool_in_scoped_lint = unknown tool name `{$tool_name}` found in scoped lint: `{$tool_name}::{$lint_name}`
     .help = add `#![register_tool({$tool_name})]` to the crate root
 
 lint_unnameable_test_items = cannot test inner items
 
+lint_unnecessary_qualification = unnecessary qualification
+    .suggestion = remove the unnecessary path segments
+
 lint_unsupported_group = `{$lint_group}` lint group is not supported with ´--force-warn´
 
 lint_untranslatable_diag = diagnostics should be created using translatable messages
@@ -680,6 +822,9 @@ lint_untranslatable_diag = diagnostics should be created using translatable mess
 lint_unused_allocation = unnecessary allocation, use `&` instead
 lint_unused_allocation_mut = unnecessary allocation, use `&mut` instead
 
+lint_unused_builtin_attribute = unused attribute `{$attr_name}`
+    .note = the built-in attribute `{$attr_name}` will be ignored, since it's applied to the macro invocation `{$macro_name}`
+
 lint_unused_closure =
     unused {$pre}{$count ->
         [one] closure
@@ -696,14 +841,43 @@ lint_unused_coroutine =
     }{$post} that must be used
     .note = coroutines are lazy and do nothing unless resumed
 
+lint_unused_crate_dependency = external crate `{$extern_crate}` unused in `{$local_crate}`: remove the dependency or add `use {$extern_crate} as _;`
+
 lint_unused_def = unused {$pre}`{$def}`{$post} that must be used
     .suggestion = use `let _ = ...` to ignore the resulting value
 
 lint_unused_delim = unnecessary {$delim} around {$item}
     .suggestion = remove these {$delim}
 
+lint_unused_doc_comment = unused doc comment
+    .label = rustdoc does not generate documentation for macro invocations
+    .help = to document an item produced by a macro, the macro must produce the documentation as part of its expansion
+
+lint_unused_extern_crate = unused extern crate
+    .suggestion = remove it
+
 lint_unused_import_braces = braces around {$node} is unnecessary
 
+lint_unused_imports = {$num_snippets ->
+        [one] unused import: {$span_snippets}
+        *[other] unused imports: {$span_snippets}
+    }
+    .suggestion_remove_whole_use = remove the whole `use` item
+    .suggestion_remove_imports = {$num_to_remove ->
+            [one] remove the unused import
+            *[other] remove the unused imports
+        }
+    .help = if this is a test module, consider adding a `#[cfg(test)]` to the containing module
+
+lint_unused_label = unused label
+
+lint_unused_lifetime = lifetime parameter `{$ident}` never used
+    .suggestion = elide the unused lifetime
+
+lint_unused_macro_definition = unused macro definition: `{$name}`
+
+lint_unused_macro_use = unused `#[macro_use]` import
+
 lint_unused_op = unused {$op} that must be used
     .label = the {$op} produces a value
     .suggestion = use `let _ = ...` to ignore the resulting value
diff --git a/compiler/rustc_lint/src/context.rs b/compiler/rustc_lint/src/context.rs
index 563d96d9519..deeb3ae090c 100644
--- a/compiler/rustc_lint/src/context.rs
+++ b/compiler/rustc_lint/src/context.rs
@@ -527,29 +527,24 @@ pub struct EarlyContext<'a> {
     pub buffered: LintBuffer,
 }
 
-pub trait LintContext {
-    fn sess(&self) -> &Session;
-
+impl EarlyContext<'_> {
     /// Emit a lint at the appropriate level, with an optional associated span and an existing
     /// diagnostic.
     ///
     /// [`lint_level`]: rustc_middle::lint::lint_level#decorate-signature
     #[rustc_lint_diagnostics]
-    fn span_lint_with_diagnostics(
+    pub fn span_lint_with_diagnostics(
         &self,
         lint: &'static Lint,
-        span: Option<impl Into<MultiSpan>>,
-        decorate: impl for<'a, 'b> FnOnce(&'b mut Diag<'a, ()>),
+        span: MultiSpan,
         diagnostic: BuiltinLintDiag,
     ) {
-        // We first generate a blank diagnostic.
-        self.opt_span_lint(lint, span, diagnostics::builtin_message(&diagnostic), |db| {
-            // Now, set up surrounding context.
-            diagnostics::builtin(self.sess(), diagnostic, db);
-            // Rewrap `db`, and pass control to the user.
-            decorate(db)
-        });
+        diagnostics::emit_buffered_lint(self, lint, span, diagnostic)
     }
+}
+
+pub trait LintContext {
+    fn sess(&self) -> &Session;
 
     // FIXME: These methods should not take an Into<MultiSpan> -- instead, callers should need to
     // set the span in their `decorate` function (preferably using set_span).
diff --git a/compiler/rustc_lint/src/context/diagnostics.rs b/compiler/rustc_lint/src/context/diagnostics.rs
index f298ed98d98..236eeee6152 100644
--- a/compiler/rustc_lint/src/context/diagnostics.rs
+++ b/compiler/rustc_lint/src/context/diagnostics.rs
@@ -1,64 +1,57 @@
 #![allow(rustc::diagnostic_outside_of_impl)]
 #![allow(rustc::untranslatable_diagnostic)]
 
+use std::borrow::Cow;
+
 use rustc_ast::util::unicode::TEXT_FLOW_CONTROL_CHARS;
-use rustc_errors::{
-    elided_lifetime_in_path_suggestion, pluralize, Diag, DiagMessage, LintDiagnostic,
-};
-use rustc_errors::{Applicability, SuggestionStyle};
+use rustc_errors::Applicability;
+use rustc_errors::{elided_lifetime_in_path_suggestion, DiagArgValue, MultiSpan};
 use rustc_middle::middle::stability;
-use rustc_session::lint::BuiltinLintDiag;
-use rustc_session::Session;
+use rustc_session::lint::{BuiltinLintDiag, Lint};
 use rustc_span::BytePos;
 
-use std::fmt::Write;
+use crate::{lints, EarlyContext, LintContext as _};
 
 mod check_cfg;
 
-#[cfg(test)]
-mod tests;
-
-pub(super) fn builtin(sess: &Session, diagnostic: BuiltinLintDiag, diag: &mut Diag<'_, ()>) {
+pub(super) fn emit_buffered_lint(
+    ctx: &EarlyContext<'_>,
+    lint: &'static Lint,
+    span: MultiSpan,
+    diagnostic: BuiltinLintDiag,
+) {
+    let sess = ctx.sess();
     match diagnostic {
-        BuiltinLintDiag::UnicodeTextFlow(span, content) => {
+        BuiltinLintDiag::UnicodeTextFlow(comment_span, content) => {
             let spans: Vec<_> = content
                 .char_indices()
                 .filter_map(|(i, c)| {
                     TEXT_FLOW_CONTROL_CHARS.contains(&c).then(|| {
-                        let lo = span.lo() + BytePos(2 + i as u32);
-                        (c, span.with_lo(lo).with_hi(lo + BytePos(c.len_utf8() as u32)))
+                        let lo = comment_span.lo() + BytePos(2 + i as u32);
+                        (c, comment_span.with_lo(lo).with_hi(lo + BytePos(c.len_utf8() as u32)))
                     })
                 })
                 .collect();
-            let (an, s) = match spans.len() {
-                1 => ("an ", ""),
-                _ => ("", "s"),
-            };
-            diag.span_label(
+            let characters = spans
+                .iter()
+                .map(|&(c, span)| lints::UnicodeCharNoteSub { span, c_debug: format!("{c:?}") })
+                .collect();
+            let suggestions = (!spans.is_empty()).then_some(lints::UnicodeTextFlowSuggestion {
+                spans: spans.iter().map(|(_c, span)| *span).collect(),
+            });
+            ctx.emit_span_lint(
+                lint,
                 span,
-                format!(
-                    "this comment contains {an}invisible unicode text flow control codepoint{s}",
-                ),
-            );
-            for (c, span) in &spans {
-                diag.span_label(*span, format!("{c:?}"));
-            }
-            diag.note(
-                "these kind of unicode codepoints change the way text flows on \
-                         applications that support them, but can cause confusion because they \
-                         change the order of characters on the screen",
-            );
-            if !spans.is_empty() {
-                diag.multipart_suggestion_with_style(
-                    "if their presence wasn't intentional, you can remove them",
-                    spans.into_iter().map(|(_, span)| (span, "".to_string())).collect(),
-                    Applicability::MachineApplicable,
-                    SuggestionStyle::HideCodeAlways,
-                );
-            }
-        }
-        BuiltinLintDiag::AbsPathWithModule(span) => {
-            let (sugg, app) = match sess.source_map().span_to_snippet(span) {
+                lints::UnicodeTextFlow {
+                    comment_span,
+                    characters,
+                    suggestions,
+                    num_codepoints: spans.len(),
+                },
+            )
+        }
+        BuiltinLintDiag::AbsPathWithModule(mod_span) => {
+            let (replacement, applicability) = match sess.source_map().span_to_snippet(mod_span) {
                 Ok(ref s) => {
                     // FIXME(Manishearth) ideally the emitting code
                     // can tell us whether or not this is global
@@ -68,64 +61,90 @@ pub(super) fn builtin(sess: &Session, diagnostic: BuiltinLintDiag, diag: &mut Di
                 }
                 Err(_) => ("crate::<path>".to_string(), Applicability::HasPlaceholders),
             };
-            diag.span_suggestion(span, "use `crate`", sugg, app);
-        }
-        BuiltinLintDiag::ProcMacroDeriveResolutionFallback { span, .. } => {
-            diag.span_label(
+            ctx.emit_span_lint(
+                lint,
                 span,
-                "names from parent modules are not accessible without an explicit import",
+                lints::AbsPathWithModule {
+                    sugg: lints::AbsPathWithModuleSugg {
+                        span: mod_span,
+                        applicability,
+                        replacement,
+                    },
+                },
             );
         }
-        BuiltinLintDiag::MacroExpandedMacroExportsAccessedByAbsolutePaths(span_def) => {
-            diag.span_note(span_def, "the macro is defined here");
-        }
+        BuiltinLintDiag::ProcMacroDeriveResolutionFallback { span: macro_span, ns, ident } => ctx
+            .emit_span_lint(
+                lint,
+                span,
+                lints::ProcMacroDeriveResolutionFallback { span: macro_span, ns, ident },
+            ),
+        BuiltinLintDiag::MacroExpandedMacroExportsAccessedByAbsolutePaths(span_def) => ctx
+            .emit_span_lint(
+                lint,
+                span,
+                lints::MacroExpandedMacroExportsAccessedByAbsolutePaths { definition: span_def },
+            ),
         BuiltinLintDiag::ElidedLifetimesInPaths(n, path_span, incl_angl_brckt, insertion_span) => {
-            diag.subdiagnostic(
-                sess.dcx(),
-                elided_lifetime_in_path_suggestion(
-                    sess.source_map(),
-                    n,
-                    path_span,
-                    incl_angl_brckt,
-                    insertion_span,
-                ),
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::ElidedLifetimesInPaths {
+                    subdiag: elided_lifetime_in_path_suggestion(
+                        sess.source_map(),
+                        n,
+                        path_span,
+                        incl_angl_brckt,
+                        insertion_span,
+                    ),
+                },
             );
         }
         BuiltinLintDiag::UnknownCrateTypes { span, candidate } => {
-            if let Some(candidate) = candidate {
-                diag.span_suggestion(
-                    span,
-                    "did you mean",
-                    format!(r#""{candidate}""#),
-                    Applicability::MaybeIncorrect,
-                );
-            }
-        }
-        BuiltinLintDiag::UnusedImports { fix_msg, fixes, test_module_span, .. } => {
-            if !fixes.is_empty() {
-                diag.tool_only_multipart_suggestion(
-                    fix_msg,
-                    fixes,
-                    Applicability::MachineApplicable,
-                );
-            }
+            let sugg = candidate.map(|candidate| lints::UnknownCrateTypesSub { span, candidate });
+            ctx.emit_span_lint(lint, span, lints::UnknownCrateTypes { sugg });
+        }
+        BuiltinLintDiag::UnusedImports {
+            remove_whole_use,
+            num_to_remove,
+            remove_spans,
+            test_module_span,
+            span_snippets,
+        } => {
+            let sugg = if remove_whole_use {
+                lints::UnusedImportsSugg::RemoveWholeUse { span: remove_spans[0] }
+            } else {
+                lints::UnusedImportsSugg::RemoveImports { remove_spans, num_to_remove }
+            };
+            let test_module_span =
+                test_module_span.map(|span| sess.source_map().guess_head_span(span));
 
-            if let Some(span) = test_module_span {
-                diag.span_help(
-                    sess.source_map().guess_head_span(span),
-                    "if this is a test module, consider adding a `#[cfg(test)]` to the containing module",
-                );
-            }
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::UnusedImports {
+                    sugg,
+                    test_module_span,
+                    num_snippets: span_snippets.len(),
+                    span_snippets: DiagArgValue::StrListSepByAnd(
+                        span_snippets.into_iter().map(Cow::Owned).collect(),
+                    ),
+                },
+            );
         }
         BuiltinLintDiag::RedundantImport(spans, ident) => {
-            for (span, is_imported) in spans {
-                let introduced = if is_imported { "imported" } else { "defined" };
-                let span_msg = if span.is_dummy() { "by the extern prelude" } else { "here" };
-                diag.span_label(
-                    span,
-                    format!("the item `{ident}` is already {introduced} {span_msg}"),
-                );
-            }
+            let subs = spans
+                .into_iter()
+                .map(|(span, is_imported)| {
+                    (match (span.is_dummy(), is_imported) {
+                        (false, true) => lints::RedundantImportSub::ImportedHere,
+                        (false, false) => lints::RedundantImportSub::DefinedHere,
+                        (true, true) => lints::RedundantImportSub::ImportedPrelude,
+                        (true, false) => lints::RedundantImportSub::DefinedPrelude,
+                    })(span)
+                })
+                .collect();
+            ctx.emit_span_lint(lint, span, lints::RedundantImport { subs, ident });
         }
         BuiltinLintDiag::DeprecatedMacro {
             suggestion,
@@ -139,106 +158,110 @@ pub(super) fn builtin(sess: &Session, diagnostic: BuiltinLintDiag, diag: &mut Di
                 kind: "macro".to_owned(),
                 suggestion,
             });
-            let deprecated =
-                stability::Deprecated { sub, kind: "macro".to_owned(), path, note, since_kind };
-            deprecated.decorate_lint(diag);
+            ctx.emit_span_lint(
+                lint,
+                span,
+                stability::Deprecated { sub, kind: "macro".to_owned(), path, note, since_kind },
+            );
         }
-        BuiltinLintDiag::UnusedDocComment(span) => {
-            diag.span_label(span, "rustdoc does not generate documentation for macro invocations");
-            diag.help("to document an item produced by a macro, \
-                                  the macro must produce the documentation as part of its expansion");
+        BuiltinLintDiag::UnusedDocComment(attr_span) => {
+            ctx.emit_span_lint(lint, span, lints::UnusedDocComment { span: attr_span });
         }
-        BuiltinLintDiag::PatternsInFnsWithoutBody { span, ident, .. } => {
-            diag.span_suggestion(
+        BuiltinLintDiag::PatternsInFnsWithoutBody { span: remove_span, ident, is_foreign } => {
+            let sub = lints::PatternsInFnsWithoutBodySub { ident, span: remove_span };
+
+            ctx.emit_span_lint(
+                lint,
                 span,
-                "remove `mut` from the parameter",
-                ident,
-                Applicability::MachineApplicable,
+                if is_foreign {
+                    lints::PatternsInFnsWithoutBody::Foreign { sub }
+                } else {
+                    lints::PatternsInFnsWithoutBody::Bodiless { sub }
+                },
             );
         }
-        BuiltinLintDiag::MissingAbi(span, default_abi) => {
-            diag.span_label(span, "ABI should be specified here");
-            diag.help(format!("the default ABI is {}", default_abi.name()));
+        BuiltinLintDiag::MissingAbi(label_span, default_abi) => {
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::MissingAbi { span: label_span, default_abi: default_abi.name() },
+            );
         }
-        BuiltinLintDiag::LegacyDeriveHelpers(span) => {
-            diag.span_label(span, "the attribute is introduced here");
+        BuiltinLintDiag::LegacyDeriveHelpers(label_span) => {
+            ctx.emit_span_lint(lint, span, lints::LegacyDeriveHelpers { span: label_span });
         }
-        BuiltinLintDiag::ProcMacroBackCompat(note) => {
-            diag.note(note);
+        BuiltinLintDiag::ProcMacroBackCompat { crate_name, fixed_version } => {
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::ProcMacroBackCompat { crate_name, fixed_version },
+            );
         }
-        BuiltinLintDiag::OrPatternsBackCompat(span, suggestion) => {
-            diag.span_suggestion(
+        BuiltinLintDiag::OrPatternsBackCompat(suggestion_span, suggestion) => {
+            ctx.emit_span_lint(
+                lint,
                 span,
-                "use pat_param to preserve semantics",
-                suggestion,
-                Applicability::MachineApplicable,
+                lints::OrPatternsBackCompat { span: suggestion_span, suggestion },
             );
         }
-        BuiltinLintDiag::ReservedPrefix(span, _) => {
-            diag.span_label(span, "unknown prefix");
-            diag.span_suggestion_verbose(
-                span.shrink_to_hi(),
-                "insert whitespace here to avoid this being parsed as a prefix in Rust 2021",
-                " ",
-                Applicability::MachineApplicable,
+        BuiltinLintDiag::ReservedPrefix(label_span, prefix) => {
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::ReservedPrefix {
+                    label: label_span,
+                    suggestion: label_span.shrink_to_hi(),
+                    prefix,
+                },
             );
         }
         BuiltinLintDiag::UnusedBuiltinAttribute { attr_name, macro_name, invoc_span } => {
-            diag.span_note(
-                        invoc_span,
-                        format!("the built-in attribute `{attr_name}` will be ignored, since it's applied to the macro invocation `{macro_name}`")
-                    );
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::UnusedBuiltinAttribute { invoc_span, attr_name, macro_name },
+            );
         }
         BuiltinLintDiag::TrailingMacro(is_trailing, name) => {
-            if is_trailing {
-                diag.note("macro invocations at the end of a block are treated as expressions");
-                diag.note(format!("to ignore the value produced by the macro, add a semicolon after the invocation of `{name}`"));
-            }
-        }
-        BuiltinLintDiag::BreakWithLabelAndLoop(span) => {
-            diag.multipart_suggestion(
-                "wrap this expression in parentheses",
-                vec![
-                    (span.shrink_to_lo(), "(".to_string()),
-                    (span.shrink_to_hi(), ")".to_string()),
-                ],
-                Applicability::MachineApplicable,
+            ctx.emit_span_lint(lint, span, lints::TrailingMacro { is_trailing, name });
+        }
+        BuiltinLintDiag::BreakWithLabelAndLoop(sugg_span) => {
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::BreakWithLabelAndLoop {
+                    sub: lints::BreakWithLabelAndLoopSub {
+                        left: sugg_span.shrink_to_lo(),
+                        right: sugg_span.shrink_to_hi(),
+                    },
+                },
             );
         }
         BuiltinLintDiag::UnexpectedCfgName(name, value) => {
-            check_cfg::unexpected_cfg_name(sess, diag, name, value)
+            ctx.emit_span_lint(lint, span, check_cfg::unexpected_cfg_name(sess, name, value));
         }
         BuiltinLintDiag::UnexpectedCfgValue(name, value) => {
-            check_cfg::unexpected_cfg_value(sess, diag, name, value)
-        }
-        BuiltinLintDiag::DeprecatedWhereclauseLocation(sugg) => {
-            let left_sp = diag.span.primary_span().unwrap();
-            match sugg {
-                Some((right_sp, sugg)) => diag.multipart_suggestion(
-                    "move it to the end of the type declaration",
-                    vec![(left_sp, String::new()), (right_sp, sugg)],
-                    Applicability::MachineApplicable,
-                ),
-                None => diag.span_suggestion(
-                    left_sp,
-                    "remove this `where`",
-                    "",
-                    Applicability::MachineApplicable,
-                ),
+            ctx.emit_span_lint(lint, span, check_cfg::unexpected_cfg_value(sess, name, value));
+        }
+        BuiltinLintDiag::DeprecatedWhereclauseLocation(left_sp, sugg) => {
+            let suggestion = match sugg {
+                Some((right_sp, sugg)) => lints::DeprecatedWhereClauseLocationSugg::MoveToEnd {
+                    left: left_sp,
+                    right: right_sp,
+                    sugg,
+                },
+                None => lints::DeprecatedWhereClauseLocationSugg::RemoveWhere { span: left_sp },
             };
-            diag.note("see issue #89122 <https://github.com/rust-lang/rust/issues/89122> for more information");
+            ctx.emit_span_lint(lint, span, lints::DeprecatedWhereClauseLocation { suggestion });
         }
         BuiltinLintDiag::SingleUseLifetime {
             param_span,
             use_span: Some((use_span, elide)),
             deletion_span,
-            ..
+            ident,
         } => {
             debug!(?param_span, ?use_span, ?deletion_span);
-            diag.span_label(param_span, "this lifetime...");
-            diag.span_label(use_span, "...is used only here");
-            if let Some(deletion_span) = deletion_span {
-                let msg = "elide the single-use lifetime";
+            let suggestion = if let Some(deletion_span) = deletion_span {
                 let (use_span, replace_lt) = if elide {
                     let use_span = sess.source_map().span_extend_while_whitespace(use_span);
                     (use_span, String::new())
@@ -249,26 +272,22 @@ pub(super) fn builtin(sess: &Session, diagnostic: BuiltinLintDiag, diag: &mut Di
 
                 // issue 107998 for the case such as a wrong function pointer type
                 // `deletion_span` is empty and there is no need to report lifetime uses here
-                let suggestions = if deletion_span.is_empty() {
-                    vec![(use_span, replace_lt)]
-                } else {
-                    vec![(deletion_span, String::new()), (use_span, replace_lt)]
-                };
-                diag.multipart_suggestion(msg, suggestions, Applicability::MachineApplicable);
-            }
+                let deletion_span =
+                    if deletion_span.is_empty() { None } else { Some(deletion_span) };
+                Some(lints::SingleUseLifetimeSugg { deletion_span, use_span, replace_lt })
+            } else {
+                None
+            };
+
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::SingleUseLifetime { suggestion, param_span, use_span, ident },
+            );
         }
-        BuiltinLintDiag::SingleUseLifetime {
-            param_span: _, use_span: None, deletion_span, ..
-        } => {
+        BuiltinLintDiag::SingleUseLifetime { use_span: None, deletion_span, ident, .. } => {
             debug!(?deletion_span);
-            if let Some(deletion_span) = deletion_span {
-                diag.span_suggestion(
-                    deletion_span,
-                    "elide the unused lifetime",
-                    "",
-                    Applicability::MachineApplicable,
-                );
-            }
+            ctx.emit_span_lint(lint, span, lints::UnusedLifetime { deletion_span, ident });
         }
         BuiltinLintDiag::NamedArgumentUsedPositionally {
             position_sp_to_replace,
@@ -277,19 +296,12 @@ pub(super) fn builtin(sess: &Session, diagnostic: BuiltinLintDiag, diag: &mut Di
             named_arg_name,
             is_formatting_arg,
         } => {
-            diag.span_label(
-                named_arg_sp,
-                "this named argument is referred to by position in formatting string",
-            );
-            if let Some(positional_arg_for_msg) = position_sp_for_msg {
-                let msg = format!(
-                    "this formatting argument uses named argument `{named_arg_name}` by position"
-                );
-                diag.span_label(positional_arg_for_msg, msg);
-            }
-
-            if let Some(positional_arg_to_replace) = position_sp_to_replace {
-                let name = if is_formatting_arg { named_arg_name + "$" } else { named_arg_name };
+            let (suggestion, name) = if let Some(positional_arg_to_replace) = position_sp_to_replace
+            {
+                let mut name = named_arg_name.clone();
+                if is_formatting_arg {
+                    name.push('$')
+                };
                 let span_to_replace = if let Ok(positional_arg_content) =
                     sess.source_map().span_to_snippet(positional_arg_to_replace)
                     && positional_arg_content.starts_with(':')
@@ -298,31 +310,40 @@ pub(super) fn builtin(sess: &Session, diagnostic: BuiltinLintDiag, diag: &mut Di
                 } else {
                     positional_arg_to_replace
                 };
-                diag.span_suggestion_verbose(
-                    span_to_replace,
-                    "use the named argument by name to avoid ambiguity",
+                (Some(span_to_replace), name)
+            } else {
+                (None, String::new())
+            };
+
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::NamedArgumentUsedPositionally {
+                    named_arg_sp,
+                    position_label_sp: position_sp_for_msg,
+                    suggestion,
                     name,
-                    Applicability::MaybeIncorrect,
-                );
-            }
+                    named_arg_name,
+                },
+            )
         }
-        BuiltinLintDiag::ByteSliceInPackedStructWithDerive { .. } => {
-            diag.help("consider implementing the trait by hand, or remove the `packed` attribute");
+        BuiltinLintDiag::ByteSliceInPackedStructWithDerive { ty } => {
+            ctx.emit_span_lint(lint, span, lints::ByteSliceInPackedStructWithDerive { ty })
         }
         BuiltinLintDiag::UnusedExternCrate { removal_span } => {
-            diag.span_suggestion(removal_span, "remove it", "", Applicability::MachineApplicable);
+            ctx.emit_span_lint(lint, span, lints::UnusedExternCrate { removal_span })
         }
         BuiltinLintDiag::ExternCrateNotIdiomatic { vis_span, ident_span } => {
             let suggestion_span = vis_span.between(ident_span);
-            diag.span_suggestion_verbose(
-                suggestion_span,
-                "convert it to a `use`",
-                if vis_span.is_empty() { "use " } else { " use " },
-                Applicability::MachineApplicable,
+            let code = if vis_span.is_empty() { "use " } else { " use " };
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::ExternCrateNotIdiomatic { span: suggestion_span, code },
             );
         }
         BuiltinLintDiag::AmbiguousGlobImports { diag: ambiguity } => {
-            rustc_errors::report_ambiguity_error(diag, ambiguity);
+            ctx.emit_span_lint(lint, span, lints::AmbiguousGlobImports { ambiguity });
         }
         BuiltinLintDiag::AmbiguousGlobReexports {
             name,
@@ -330,15 +351,15 @@ pub(super) fn builtin(sess: &Session, diagnostic: BuiltinLintDiag, diag: &mut Di
             first_reexport_span,
             duplicate_reexport_span,
         } => {
-            diag.span_label(
-                first_reexport_span,
-                format!("the name `{name}` in the {namespace} namespace is first re-exported here"),
-            );
-            diag.span_label(
-                duplicate_reexport_span,
-                format!(
-                    "but the name `{name}` in the {namespace} namespace is also re-exported here"
-                ),
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::AmbiguousGlobReexports {
+                    first_reexport: first_reexport_span,
+                    duplicate_reexport: duplicate_reexport_span,
+                    name,
+                    namespace,
+                },
             );
         }
         BuiltinLintDiag::HiddenGlobReexports {
@@ -347,261 +368,127 @@ pub(super) fn builtin(sess: &Session, diagnostic: BuiltinLintDiag, diag: &mut Di
             glob_reexport_span,
             private_item_span,
         } => {
-            diag.span_note(glob_reexport_span, format!("the name `{name}` in the {namespace} namespace is supposed to be publicly re-exported here"));
-            diag.span_note(private_item_span, "but the private item here shadows it".to_owned());
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::HiddenGlobReexports {
+                    glob_reexport: glob_reexport_span,
+                    private_item: private_item_span,
+
+                    name,
+                    namespace,
+                },
+            );
         }
         BuiltinLintDiag::UnusedQualifications { removal_span } => {
-            diag.span_suggestion_verbose(
-                removal_span,
-                "remove the unnecessary path segments",
-                "",
-                Applicability::MachineApplicable,
-            );
+            ctx.emit_span_lint(lint, span, lints::UnusedQualifications { removal_span });
         }
-        BuiltinLintDiag::AssociatedConstElidedLifetime { elided, span } => {
-            diag.span_suggestion_verbose(
-                if elided { span.shrink_to_hi() } else { span },
-                "use the `'static` lifetime",
-                if elided { "'static " } else { "'static" },
-                Applicability::MachineApplicable,
+        BuiltinLintDiag::AssociatedConstElidedLifetime { elided, span: lt_span } => {
+            let lt_span = if elided { lt_span.shrink_to_hi() } else { lt_span };
+            let code = if elided { "'static " } else { "'static" };
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::AssociatedConstElidedLifetime { span: lt_span, code, elided },
             );
         }
-        BuiltinLintDiag::RedundantImportVisibility { max_vis, span, .. } => {
-            diag.span_note(span, format!("the most public imported item is `{max_vis}`"));
-            diag.help(
-                "reduce the glob import's visibility or increase visibility of imported items",
+        BuiltinLintDiag::RedundantImportVisibility { max_vis, span: vis_span, import_vis } => {
+            ctx.emit_span_lint(
+                lint,
+                span,
+                lints::RedundantImportVisibility { span: vis_span, help: (), max_vis, import_vis },
             );
         }
-        BuiltinLintDiag::UnknownDiagnosticAttribute { span, typo_name } => {
-            if let Some(typo_name) = typo_name {
-                diag.span_suggestion_verbose(
-                    span,
-                    "an attribute with a similar name exists",
-                    typo_name,
-                    Applicability::MachineApplicable,
-                );
-            }
-        }
-        BuiltinLintDiag::MacroUseDeprecated
-        | BuiltinLintDiag::UnusedMacroUse
-        | BuiltinLintDiag::PrivateExternCrateReexport(_)
-        | BuiltinLintDiag::UnusedLabel
-        | BuiltinLintDiag::MacroIsPrivate(_)
-        | BuiltinLintDiag::UnusedMacroDefinition(_)
-        | BuiltinLintDiag::MacroRuleNeverUsed(_, _)
-        | BuiltinLintDiag::UnstableFeature(_)
-        | BuiltinLintDiag::AvoidUsingIntelSyntax
-        | BuiltinLintDiag::AvoidUsingAttSyntax
-        | BuiltinLintDiag::IncompleteInclude
-        | BuiltinLintDiag::UnnameableTestItems
-        | BuiltinLintDiag::DuplicateMacroAttribute
-        | BuiltinLintDiag::CfgAttrNoAttributes
-        | BuiltinLintDiag::CrateTypeInCfgAttr
-        | BuiltinLintDiag::CrateNameInCfgAttr
-        | BuiltinLintDiag::MissingFragmentSpecifier
-        | BuiltinLintDiag::MetaVariableStillRepeating(_)
-        | BuiltinLintDiag::MetaVariableWrongOperator
-        | BuiltinLintDiag::DuplicateMatcherBinding
-        | BuiltinLintDiag::UnknownMacroVariable(_)
-        | BuiltinLintDiag::UnusedExternCrate2 { .. }
-        | BuiltinLintDiag::WasmCAbi
-        | BuiltinLintDiag::IllFormedAttributeInput { .. }
-        | BuiltinLintDiag::InnerAttributeUnstable { .. } => {}
-    }
-}
-
-pub(super) fn builtin_message(diagnostic: &BuiltinLintDiag) -> DiagMessage {
-    match diagnostic {
-        BuiltinLintDiag::AbsPathWithModule(_) => {
-            "absolute paths must start with `self`, `super`, `crate`, or an \
-                external crate name in the 2018 edition"
-                .into()
-        }
-        BuiltinLintDiag::ProcMacroDeriveResolutionFallback { ns, ident, .. } => {
-            format!("cannot find {} `{}` in this scope", ns.descr(), ident).into()
-        }
-        BuiltinLintDiag::MacroExpandedMacroExportsAccessedByAbsolutePaths(_) => {
-            "macro-expanded `macro_export` macros from the current crate cannot \
-                be referred to by absolute paths"
-                .into()
-        }
-        BuiltinLintDiag::ElidedLifetimesInPaths(_, _, _, _) => {
-            "hidden lifetime parameters in types are deprecated".into()
-        }
-        BuiltinLintDiag::UnknownCrateTypes { .. } => "invalid `crate_type` value".into(),
-        BuiltinLintDiag::UnusedImports { span_snippets, .. } => format!(
-            "unused import{}{}",
-            pluralize!(span_snippets.len()),
-            if !span_snippets.is_empty() {
-                format!(": {}", span_snippets.join(", "))
-            } else {
-                String::new()
-            }
-        )
-        .into(),
-        BuiltinLintDiag::RedundantImport(_, source) => {
-            format!("the item `{source}` is imported redundantly").into()
-        }
-        BuiltinLintDiag::DeprecatedMacro { since_kind, .. } => {
-            stability::Deprecated::msg_for_since_kind(since_kind)
-        }
-        BuiltinLintDiag::MissingAbi(_, _) => crate::fluent_generated::lint_extern_without_abi,
-        BuiltinLintDiag::UnusedDocComment(_) => "unused doc comment".into(),
-        BuiltinLintDiag::UnusedBuiltinAttribute { attr_name, .. } => {
-            format!("unused attribute `{attr_name}`").into()
-        }
-        BuiltinLintDiag::PatternsInFnsWithoutBody { is_foreign, .. } => {
-            if *is_foreign {
-                crate::fluent_generated::lint_pattern_in_foreign
-            } else {
-                crate::fluent_generated::lint_pattern_in_bodiless
-            }
-        }
-        BuiltinLintDiag::LegacyDeriveHelpers(_) => {
-            "derive helper attribute is used before it is introduced".into()
-        }
-        BuiltinLintDiag::ProcMacroBackCompat(_) => "using an old version of `rental`".into(),
-        BuiltinLintDiag::OrPatternsBackCompat(_, _) => {
-            "the meaning of the `pat` fragment specifier is changing in Rust 2021, \
-            which may affect this macro"
-                .into()
-        }
-        BuiltinLintDiag::ReservedPrefix(_, prefix) => {
-            format!("prefix `{prefix}` is unknown").into()
-        }
-        BuiltinLintDiag::TrailingMacro(_, _) => {
-            "trailing semicolon in macro used in expression position".into()
-        }
-        BuiltinLintDiag::BreakWithLabelAndLoop(_) => {
-            "this labeled break expression is easy to confuse with an unlabeled break with a \
-            labeled value expression"
-                .into()
-        }
-        BuiltinLintDiag::UnicodeTextFlow(_, _) => {
-            "unicode codepoint changing visible direction of text present in comment".into()
-        }
-        BuiltinLintDiag::UnexpectedCfgName((name, _), _) => {
-            format!("unexpected `cfg` condition name: `{}`", name).into()
-        }
-        BuiltinLintDiag::UnexpectedCfgValue(_, v) => if let Some((value, _)) = v {
-            format!("unexpected `cfg` condition value: `{value}`")
-        } else {
-            format!("unexpected `cfg` condition value: (none)")
+        BuiltinLintDiag::UnknownDiagnosticAttribute { span: typo_span, typo_name } => {
+            let typo = typo_name.map(|typo_name| lints::UnknownDiagnosticAttributeTypoSugg {
+                span: typo_span,
+                typo_name,
+            });
+            ctx.emit_span_lint(lint, span, lints::UnknownDiagnosticAttribute { typo });
         }
-        .into(),
-        BuiltinLintDiag::DeprecatedWhereclauseLocation(_) => {
-            crate::fluent_generated::lint_deprecated_where_clause_location
+        BuiltinLintDiag::MacroUseDeprecated => {
+            ctx.emit_span_lint(lint, span, lints::MacroUseDeprecated)
         }
-        BuiltinLintDiag::SingleUseLifetime { use_span, ident, .. } => {
-            if use_span.is_some() {
-                format!("lifetime parameter `{}` only used once", ident).into()
-            } else {
-                format!("lifetime parameter `{}` never used", ident).into()
-            }
+        BuiltinLintDiag::UnusedMacroUse => ctx.emit_span_lint(lint, span, lints::UnusedMacroUse),
+        BuiltinLintDiag::PrivateExternCrateReexport(ident) => {
+            ctx.emit_span_lint(lint, span, lints::PrivateExternCrateReexport { ident })
         }
-        BuiltinLintDiag::NamedArgumentUsedPositionally { named_arg_name, .. } => {
-            format!("named argument `{}` is not used by name", named_arg_name).into()
+        BuiltinLintDiag::UnusedLabel => ctx.emit_span_lint(lint, span, lints::UnusedLabel),
+        BuiltinLintDiag::MacroIsPrivate(ident) => {
+            ctx.emit_span_lint(lint, span, lints::MacroIsPrivate { ident })
         }
-        BuiltinLintDiag::ByteSliceInPackedStructWithDerive { ty } => {
-            format!("{ty} slice in a packed struct that derives a built-in trait").into()
-        }
-        BuiltinLintDiag::UnusedExternCrate { .. } => "unused extern crate".into(),
-        BuiltinLintDiag::ExternCrateNotIdiomatic { .. } => {
-            "`extern crate` is not idiomatic in the new edition".into()
-        }
-        BuiltinLintDiag::AmbiguousGlobImports { diag } => diag.msg.clone().into(),
-        BuiltinLintDiag::AmbiguousGlobReexports { .. } => "ambiguous glob re-exports".into(),
-        BuiltinLintDiag::HiddenGlobReexports { .. } => {
-            "private item shadows public glob re-export".into()
-        }
-        BuiltinLintDiag::UnusedQualifications { .. } => "unnecessary qualification".into(),
-        BuiltinLintDiag::AssociatedConstElidedLifetime { elided, .. } => if *elided {
-            "`&` without an explicit lifetime name cannot be used here"
-        } else {
-            "`'_` cannot be used here"
-        }
-        .into(),
-        BuiltinLintDiag::RedundantImportVisibility { import_vis, .. } => format!(
-            "glob import doesn't reexport anything with visibility `{}` \
-            because no imported item is public enough",
-            import_vis
-        )
-        .into(),
-        BuiltinLintDiag::MacroUseDeprecated => "deprecated `#[macro_use]` attribute used to \
-                                import macros should be replaced at use sites \
-                                with a `use` item to import the macro \
-                                instead"
-            .into(),
-        BuiltinLintDiag::UnusedMacroUse => "unused `#[macro_use]` import".into(),
-        BuiltinLintDiag::PrivateExternCrateReexport(ident) => format!(
-            "extern crate `{ident}` is private, and cannot be \
-                                   re-exported (error E0365), consider declaring with \
-                                   `pub`"
-        )
-        .into(),
-        BuiltinLintDiag::UnusedLabel => "unused label".into(),
-        BuiltinLintDiag::MacroIsPrivate(ident) => format!("macro `{ident}` is private").into(),
         BuiltinLintDiag::UnusedMacroDefinition(name) => {
-            format!("unused macro definition: `{}`", name).into()
+            ctx.emit_span_lint(lint, span, lints::UnusedMacroDefinition { name })
         }
         BuiltinLintDiag::MacroRuleNeverUsed(n, name) => {
-            format!("rule #{} of macro `{}` is never used", n + 1, name).into()
+            ctx.emit_span_lint(lint, span, lints::MacroRuleNeverUsed { n: n + 1, name })
+        }
+        BuiltinLintDiag::UnstableFeature(msg) => {
+            ctx.emit_span_lint(lint, span, lints::UnstableFeature { msg })
         }
-        BuiltinLintDiag::UnstableFeature(msg) => msg.clone().into(),
         BuiltinLintDiag::AvoidUsingIntelSyntax => {
-            "avoid using `.intel_syntax`, Intel syntax is the default".into()
+            ctx.emit_span_lint(lint, span, lints::AvoidIntelSyntax)
         }
         BuiltinLintDiag::AvoidUsingAttSyntax => {
-            "avoid using `.att_syntax`, prefer using `options(att_syntax)` instead".into()
+            ctx.emit_span_lint(lint, span, lints::AvoidAttSyntax)
         }
         BuiltinLintDiag::IncompleteInclude => {
-            "include macro expected single expression in source".into()
+            ctx.emit_span_lint(lint, span, lints::IncompleteInclude)
+        }
+        BuiltinLintDiag::UnnameableTestItems => {
+            ctx.emit_span_lint(lint, span, lints::UnnameableTestItems)
+        }
+        BuiltinLintDiag::DuplicateMacroAttribute => {
+            ctx.emit_span_lint(lint, span, lints::DuplicateMacroAttribute)
         }
-        BuiltinLintDiag::UnnameableTestItems => crate::fluent_generated::lint_unnameable_test_items,
-        BuiltinLintDiag::DuplicateMacroAttribute => "duplicated attribute".into(),
         BuiltinLintDiag::CfgAttrNoAttributes => {
-            crate::fluent_generated::lint_cfg_attr_no_attributes
+            ctx.emit_span_lint(lint, span, lints::CfgAttrNoAttributes)
         }
         BuiltinLintDiag::CrateTypeInCfgAttr => {
-            crate::fluent_generated::lint_crate_type_in_cfg_attr_deprecated
+            ctx.emit_span_lint(lint, span, lints::CrateTypeInCfgAttr)
         }
         BuiltinLintDiag::CrateNameInCfgAttr => {
-            crate::fluent_generated::lint_crate_name_in_cfg_attr_deprecated
+            ctx.emit_span_lint(lint, span, lints::CrateNameInCfgAttr)
+        }
+        BuiltinLintDiag::MissingFragmentSpecifier => {
+            ctx.emit_span_lint(lint, span, lints::MissingFragmentSpecifier)
         }
-        BuiltinLintDiag::MissingFragmentSpecifier => "missing fragment specifier".into(),
         BuiltinLintDiag::MetaVariableStillRepeating(name) => {
-            format!("variable '{name}' is still repeating at this depth").into()
+            ctx.emit_span_lint(lint, span, lints::MetaVariableStillRepeating { name })
         }
         BuiltinLintDiag::MetaVariableWrongOperator => {
-            "meta-variable repeats with different Kleene operator".into()
+            ctx.emit_span_lint(lint, span, lints::MetaVariableWrongOperator)
+        }
+        BuiltinLintDiag::DuplicateMatcherBinding => {
+            ctx.emit_span_lint(lint, span, lints::DuplicateMatcherBinding)
         }
-        BuiltinLintDiag::DuplicateMatcherBinding => "duplicate matcher binding".into(),
         BuiltinLintDiag::UnknownMacroVariable(name) => {
-            format!("unknown macro variable `{name}`").into()
-        }
-        BuiltinLintDiag::UnusedExternCrate2 { extern_crate, local_crate } => format!(
-            "external crate `{}` unused in `{}`: remove the dependency or add `use {} as _;`",
-            extern_crate, local_crate, extern_crate
-        )
-        .into(),
-        BuiltinLintDiag::WasmCAbi => crate::fluent_generated::lint_wasm_c_abi,
-        BuiltinLintDiag::IllFormedAttributeInput { suggestions } => suggestions
-            .iter()
-            .enumerate()
-            .fold("attribute must be of the form ".to_string(), |mut acc, (i, sugg)| {
-                if i != 0 {
-                    write!(acc, " or ").unwrap();
-                }
-                write!(acc, "`{sugg}`").unwrap();
-                acc
-            })
-            .into(),
-        BuiltinLintDiag::InnerAttributeUnstable { is_macro } => if *is_macro {
-            "inner macro attributes are unstable"
-        } else {
-            "custom inner attributes are unstable"
-        }
-        .into(),
-        BuiltinLintDiag::UnknownDiagnosticAttribute { .. } => "unknown diagnostic attribute".into(),
+            ctx.emit_span_lint(lint, span, lints::UnknownMacroVariable { name })
+        }
+        BuiltinLintDiag::UnusedCrateDependency { extern_crate, local_crate } => ctx.emit_span_lint(
+            lint,
+            span,
+            lints::UnusedCrateDependency { extern_crate, local_crate },
+        ),
+        BuiltinLintDiag::WasmCAbi => ctx.emit_span_lint(lint, span, lints::WasmCAbi),
+        BuiltinLintDiag::IllFormedAttributeInput { suggestions } => ctx.emit_span_lint(
+            lint,
+            span,
+            lints::IllFormedAttributeInput {
+                num_suggestions: suggestions.len(),
+                suggestions: DiagArgValue::StrListSepByAnd(
+                    suggestions.into_iter().map(|s| format!("`{s}`").into()).collect(),
+                ),
+            },
+        ),
+        BuiltinLintDiag::InnerAttributeUnstable { is_macro } => ctx.emit_span_lint(
+            lint,
+            span,
+            if is_macro {
+                lints::InnerAttributeUnstable::InnerMacroAttribute
+            } else {
+                lints::InnerAttributeUnstable::CustomInnerAttribute
+            },
+        ),
     }
 }
diff --git a/compiler/rustc_lint/src/context/diagnostics/check_cfg.rs b/compiler/rustc_lint/src/context/diagnostics/check_cfg.rs
index 38a87d9a11c..020ca1753cf 100644
--- a/compiler/rustc_lint/src/context/diagnostics/check_cfg.rs
+++ b/compiler/rustc_lint/src/context/diagnostics/check_cfg.rs
@@ -1,4 +1,3 @@
-use rustc_errors::Diag;
 use rustc_middle::bug;
 use rustc_session::{config::ExpectedValues, Session};
 use rustc_span::edit_distance::find_best_match_for_name;
@@ -45,10 +44,9 @@ fn to_check_cfg_arg(name: Symbol, value: Option<Symbol>, quotes: EscapeQuotes) -
 
 pub(super) fn unexpected_cfg_name(
     sess: &Session,
-    diag: &mut Diag<'_, ()>,
     (name, name_span): (Symbol, Span),
     value: Option<(Symbol, Span)>,
-) {
+) -> lints::UnexpectedCfgName {
     #[allow(rustc::potential_query_instability)]
     let possibilities: Vec<Symbol> = sess.psess.check_config.expecteds.keys().copied().collect();
 
@@ -179,15 +177,14 @@ pub(super) fn unexpected_cfg_name(
         ))
     };
 
-    diag.subdiagnostic(diag.dcx, lints::UnexpectedCfgNameSub { code_sugg, invocation_help });
+    lints::UnexpectedCfgName { code_sugg, invocation_help, name }
 }
 
 pub(super) fn unexpected_cfg_value(
     sess: &Session,
-    diag: &mut Diag<'_, ()>,
     (name, name_span): (Symbol, Span),
     value: Option<(Symbol, Span)>,
-) {
+) -> lints::UnexpectedCfgValue {
     let Some(ExpectedValues::Some(values)) = &sess.psess.check_config.expecteds.get(&name) else {
         bug!(
             "it shouldn't be possible to have a diagnostic on a value whose name is not in values"
@@ -287,5 +284,10 @@ pub(super) fn unexpected_cfg_value(
         lints::unexpected_cfg_value::InvocationHelp::Rustc(help)
     };
 
-    diag.subdiagnostic(diag.dcx, lints::UnexpectedCfgValueSub { code_sugg, invocation_help });
+    lints::UnexpectedCfgValue {
+        code_sugg,
+        invocation_help,
+        has_value: value.is_some(),
+        value: value.map_or_else(String::new, |(v, _span)| v.to_string()),
+    }
 }
diff --git a/compiler/rustc_lint/src/early.rs b/compiler/rustc_lint/src/early.rs
index 80cb70ba277..736c7a11069 100644
--- a/compiler/rustc_lint/src/early.rs
+++ b/compiler/rustc_lint/src/early.rs
@@ -14,7 +14,7 @@
 //! upon. As the ast is traversed, this keeps track of the current lint level
 //! for all lint attributes.
 
-use crate::context::{EarlyContext, LintContext, LintStore};
+use crate::context::{EarlyContext, LintStore};
 use crate::passes::{EarlyLintPass, EarlyLintPassObject};
 use rustc_ast::ptr::P;
 use rustc_ast::visit::{self as ast_visit, walk_list, Visitor};
@@ -45,7 +45,7 @@ impl<'a, T: EarlyLintPass> EarlyContextAndPass<'a, T> {
     fn inlined_check_id(&mut self, id: ast::NodeId) {
         for early_lint in self.context.buffered.take(id) {
             let BufferedEarlyLint { span, node_id: _, lint_id, diagnostic } = early_lint;
-            self.context.span_lint_with_diagnostics(lint_id.lint, Some(span), |_| {}, diagnostic);
+            self.context.span_lint_with_diagnostics(lint_id.lint, span, diagnostic);
         }
     }
 
diff --git a/compiler/rustc_lint/src/lints.rs b/compiler/rustc_lint/src/lints.rs
index 2a07db27616..3bd6faca379 100644
--- a/compiler/rustc_lint/src/lints.rs
+++ b/compiler/rustc_lint/src/lints.rs
@@ -5,16 +5,22 @@ use std::num::NonZero;
 use crate::errors::RequestedLevel;
 use crate::fluent_generated as fluent;
 use rustc_errors::{
-    codes::*, Applicability, Diag, DiagMessage, DiagStyledString, EmissionGuarantee,
-    LintDiagnostic, SubdiagMessageOp, Subdiagnostic, SuggestionStyle,
+    codes::*, Applicability, Diag, DiagArgValue, DiagMessage, DiagStyledString,
+    ElidedLifetimeInPathSubdiag, EmissionGuarantee, LintDiagnostic, SubdiagMessageOp,
+    Subdiagnostic, SuggestionStyle,
 };
-use rustc_hir::def_id::DefId;
+use rustc_hir::{def::Namespace, def_id::DefId};
 use rustc_macros::{LintDiagnostic, Subdiagnostic};
 use rustc_middle::ty::{
     inhabitedness::InhabitedPredicate, Clause, PolyExistentialTraitRef, Ty, TyCtxt,
 };
-use rustc_session::Session;
-use rustc_span::{edition::Edition, sym, symbol::Ident, Span, Symbol};
+use rustc_session::{lint::AmbiguityErrorDiag, Session};
+use rustc_span::{
+    edition::Edition,
+    sym,
+    symbol::{Ident, MacroRulesNormalizedIdent},
+    Span, Symbol,
+};
 
 use crate::{
     builtin::InitError, builtin::TypeAliasBounds, errors::OverruledAttributeSub, LateContext,
@@ -1986,12 +1992,15 @@ impl UnexpectedCfgRustcHelp {
     }
 }
 
-#[derive(Subdiagnostic)]
-pub struct UnexpectedCfgNameSub {
+#[derive(LintDiagnostic)]
+#[diag(lint_unexpected_cfg_name)]
+pub struct UnexpectedCfgName {
     #[subdiagnostic]
     pub code_sugg: unexpected_cfg_name::CodeSuggestion,
     #[subdiagnostic]
     pub invocation_help: unexpected_cfg_name::InvocationHelp,
+
+    pub name: Symbol,
 }
 
 pub mod unexpected_cfg_name {
@@ -2093,12 +2102,16 @@ pub mod unexpected_cfg_name {
     }
 }
 
-#[derive(Subdiagnostic)]
-pub struct UnexpectedCfgValueSub {
+#[derive(LintDiagnostic)]
+#[diag(lint_unexpected_cfg_value)]
+pub struct UnexpectedCfgValue {
     #[subdiagnostic]
     pub code_sugg: unexpected_cfg_value::CodeSuggestion,
     #[subdiagnostic]
     pub invocation_help: unexpected_cfg_value::InvocationHelp,
+
+    pub has_value: bool,
+    pub value: String,
 }
 
 pub mod unexpected_cfg_value {
@@ -2204,3 +2217,573 @@ pub mod unexpected_cfg_value {
         Other(#[subdiagnostic] super::UnexpectedCfgCargoHelp),
     }
 }
+
+#[derive(LintDiagnostic)]
+#[diag(lint_macro_use_deprecated)]
+pub struct MacroUseDeprecated;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_macro_use)]
+pub struct UnusedMacroUse;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_private_extern_crate_reexport)]
+pub struct PrivateExternCrateReexport {
+    pub ident: Ident,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_label)]
+pub struct UnusedLabel;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_macro_is_private)]
+pub struct MacroIsPrivate {
+    pub ident: Ident,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_macro_definition)]
+pub struct UnusedMacroDefinition {
+    pub name: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_macro_rule_never_used)]
+pub struct MacroRuleNeverUsed {
+    pub n: usize,
+    pub name: Symbol,
+}
+
+pub struct UnstableFeature {
+    pub msg: DiagMessage,
+}
+
+impl<'a> LintDiagnostic<'a, ()> for UnstableFeature {
+    fn decorate_lint<'b>(self, _diag: &'b mut Diag<'a, ()>) {}
+
+    fn msg(&self) -> DiagMessage {
+        self.msg.clone()
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_avoid_intel_syntax)]
+pub struct AvoidIntelSyntax;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_avoid_att_syntax)]
+pub struct AvoidAttSyntax;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_incomplete_include)]
+pub struct IncompleteInclude;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unnameable_test_items)]
+pub struct UnnameableTestItems;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_duplicate_macro_attribute)]
+pub struct DuplicateMacroAttribute;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_cfg_attr_no_attributes)]
+pub struct CfgAttrNoAttributes;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_crate_type_in_cfg_attr_deprecated)]
+pub struct CrateTypeInCfgAttr;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_crate_name_in_cfg_attr_deprecated)]
+pub struct CrateNameInCfgAttr;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_missing_fragment_specifier)]
+pub struct MissingFragmentSpecifier;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_metavariable_still_repeating)]
+pub struct MetaVariableStillRepeating {
+    pub name: MacroRulesNormalizedIdent,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_metavariable_wrong_operator)]
+pub struct MetaVariableWrongOperator;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_duplicate_matcher_binding)]
+pub struct DuplicateMatcherBinding;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unknown_macro_variable)]
+pub struct UnknownMacroVariable {
+    pub name: MacroRulesNormalizedIdent,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_crate_dependency)]
+pub struct UnusedCrateDependency {
+    pub extern_crate: Symbol,
+    pub local_crate: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_wasm_c_abi)]
+pub struct WasmCAbi;
+
+#[derive(LintDiagnostic)]
+#[diag(lint_ill_formed_attribute_input)]
+pub struct IllFormedAttributeInput {
+    pub num_suggestions: usize,
+    pub suggestions: DiagArgValue,
+}
+
+#[derive(LintDiagnostic)]
+pub enum InnerAttributeUnstable {
+    #[diag(lint_inner_macro_attribute_unstable)]
+    InnerMacroAttribute,
+    #[diag(lint_custom_inner_attribute_unstable)]
+    CustomInnerAttribute,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unknown_diagnostic_attribute)]
+pub struct UnknownDiagnosticAttribute {
+    #[subdiagnostic]
+    pub typo: Option<UnknownDiagnosticAttributeTypoSugg>,
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(
+    lint_unknown_diagnostic_attribute_typo_sugg,
+    style = "verbose",
+    code = "{typo_name}",
+    applicability = "machine-applicable"
+)]
+pub struct UnknownDiagnosticAttributeTypoSugg {
+    #[primary_span]
+    pub span: Span,
+    pub typo_name: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unicode_text_flow)]
+#[note]
+pub struct UnicodeTextFlow {
+    #[label]
+    pub comment_span: Span,
+    #[subdiagnostic]
+    pub characters: Vec<UnicodeCharNoteSub>,
+    #[subdiagnostic]
+    pub suggestions: Option<UnicodeTextFlowSuggestion>,
+
+    pub num_codepoints: usize,
+}
+
+#[derive(Subdiagnostic)]
+#[label(lint_label_comment_char)]
+pub struct UnicodeCharNoteSub {
+    #[primary_span]
+    pub span: Span,
+    pub c_debug: String,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable", style = "hidden")]
+pub struct UnicodeTextFlowSuggestion {
+    #[suggestion_part(code = "")]
+    pub spans: Vec<Span>,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_abs_path_with_module)]
+pub struct AbsPathWithModule {
+    #[subdiagnostic]
+    pub sugg: AbsPathWithModuleSugg,
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(lint_suggestion, code = "{replacement}")]
+pub struct AbsPathWithModuleSugg {
+    #[primary_span]
+    pub span: Span,
+    #[applicability]
+    pub applicability: Applicability,
+    pub replacement: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_proc_macro_derive_resolution_fallback)]
+pub struct ProcMacroDeriveResolutionFallback {
+    #[label]
+    pub span: Span,
+    pub ns: Namespace,
+    pub ident: Ident,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_macro_expanded_macro_exports_accessed_by_absolute_paths)]
+pub struct MacroExpandedMacroExportsAccessedByAbsolutePaths {
+    #[note]
+    pub definition: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_hidden_lifetime_parameters)]
+pub struct ElidedLifetimesInPaths {
+    #[subdiagnostic]
+    pub subdiag: ElidedLifetimeInPathSubdiag,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_invalid_crate_type_value)]
+pub struct UnknownCrateTypes {
+    #[subdiagnostic]
+    pub sugg: Option<UnknownCrateTypesSub>,
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(lint_suggestion, code = r#""{candidate}""#, applicability = "maybe-incorrect")]
+pub struct UnknownCrateTypesSub {
+    #[primary_span]
+    pub span: Span,
+    pub candidate: Symbol,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_imports)]
+pub struct UnusedImports {
+    #[subdiagnostic]
+    pub sugg: UnusedImportsSugg,
+    #[help]
+    pub test_module_span: Option<Span>,
+
+    pub span_snippets: DiagArgValue,
+    pub num_snippets: usize,
+}
+
+#[derive(Subdiagnostic)]
+pub enum UnusedImportsSugg {
+    #[suggestion(
+        lint_suggestion_remove_whole_use,
+        applicability = "machine-applicable",
+        code = "",
+        style = "tool-only"
+    )]
+    RemoveWholeUse {
+        #[primary_span]
+        span: Span,
+    },
+    #[multipart_suggestion(
+        lint_suggestion_remove_imports,
+        applicability = "machine-applicable",
+        style = "tool-only"
+    )]
+    RemoveImports {
+        #[suggestion_part(code = "")]
+        remove_spans: Vec<Span>,
+        num_to_remove: usize,
+    },
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_redundant_import)]
+pub struct RedundantImport {
+    #[subdiagnostic]
+    pub subs: Vec<RedundantImportSub>,
+
+    pub ident: Ident,
+}
+
+#[derive(Subdiagnostic)]
+pub enum RedundantImportSub {
+    #[label(lint_label_imported_here)]
+    ImportedHere(#[primary_span] Span),
+    #[label(lint_label_defined_here)]
+    DefinedHere(#[primary_span] Span),
+    #[label(lint_label_imported_prelude)]
+    ImportedPrelude(#[primary_span] Span),
+    #[label(lint_label_defined_prelude)]
+    DefinedPrelude(#[primary_span] Span),
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_doc_comment)]
+#[help]
+pub struct UnusedDocComment {
+    #[label]
+    pub span: Span,
+}
+
+#[derive(LintDiagnostic)]
+pub enum PatternsInFnsWithoutBody {
+    #[diag(lint_pattern_in_foreign)]
+    Foreign {
+        #[subdiagnostic]
+        sub: PatternsInFnsWithoutBodySub,
+    },
+    #[diag(lint_pattern_in_bodiless)]
+    Bodiless {
+        #[subdiagnostic]
+        sub: PatternsInFnsWithoutBodySub,
+    },
+}
+
+#[derive(Subdiagnostic)]
+#[suggestion(lint_remove_mut_from_pattern, code = "{ident}", applicability = "machine-applicable")]
+pub struct PatternsInFnsWithoutBodySub {
+    #[primary_span]
+    pub span: Span,
+
+    pub ident: Ident,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_extern_without_abi)]
+#[help]
+pub struct MissingAbi {
+    #[label]
+    pub span: Span,
+
+    pub default_abi: &'static str,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_legacy_derive_helpers)]
+pub struct LegacyDeriveHelpers {
+    #[label]
+    pub span: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_proc_macro_back_compat)]
+#[note]
+pub struct ProcMacroBackCompat {
+    pub crate_name: String,
+    pub fixed_version: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_or_patterns_back_compat)]
+pub struct OrPatternsBackCompat {
+    #[suggestion(code = "{suggestion}", applicability = "machine-applicable")]
+    pub span: Span,
+    pub suggestion: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_reserved_prefix)]
+pub struct ReservedPrefix {
+    #[label]
+    pub label: Span,
+    #[suggestion(code = " ", applicability = "machine-applicable")]
+    pub suggestion: Span,
+
+    pub prefix: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_builtin_attribute)]
+pub struct UnusedBuiltinAttribute {
+    #[note]
+    pub invoc_span: Span,
+
+    pub attr_name: Symbol,
+    pub macro_name: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_trailing_semi_macro)]
+pub struct TrailingMacro {
+    #[note(lint_note1)]
+    #[note(lint_note2)]
+    pub is_trailing: bool,
+
+    pub name: Ident,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_break_with_label_and_loop)]
+pub struct BreakWithLabelAndLoop {
+    #[subdiagnostic]
+    pub sub: BreakWithLabelAndLoopSub,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")]
+pub struct BreakWithLabelAndLoopSub {
+    #[suggestion_part(code = "(")]
+    pub left: Span,
+    #[suggestion_part(code = ")")]
+    pub right: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_deprecated_where_clause_location)]
+#[note]
+pub struct DeprecatedWhereClauseLocation {
+    #[subdiagnostic]
+    pub suggestion: DeprecatedWhereClauseLocationSugg,
+}
+
+#[derive(Subdiagnostic)]
+pub enum DeprecatedWhereClauseLocationSugg {
+    #[multipart_suggestion(lint_suggestion_move_to_end, applicability = "machine-applicable")]
+    MoveToEnd {
+        #[suggestion_part(code = "")]
+        left: Span,
+        #[suggestion_part(code = "{sugg}")]
+        right: Span,
+
+        sugg: String,
+    },
+    #[suggestion(lint_suggestion_remove_where, code = "", applicability = "machine-applicable")]
+    RemoveWhere {
+        #[primary_span]
+        span: Span,
+    },
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_single_use_lifetime)]
+pub struct SingleUseLifetime {
+    #[label(lint_label_param)]
+    pub param_span: Span,
+    #[label(lint_label_use)]
+    pub use_span: Span,
+    #[subdiagnostic]
+    pub suggestion: Option<SingleUseLifetimeSugg>,
+
+    pub ident: Ident,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")]
+pub struct SingleUseLifetimeSugg {
+    #[suggestion_part(code = "")]
+    pub deletion_span: Option<Span>,
+    #[suggestion_part(code = "{replace_lt}")]
+    pub use_span: Span,
+
+    pub replace_lt: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_lifetime)]
+pub struct UnusedLifetime {
+    #[suggestion(code = "", applicability = "machine-applicable")]
+    pub deletion_span: Option<Span>,
+
+    pub ident: Ident,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_named_argument_used_positionally)]
+pub struct NamedArgumentUsedPositionally {
+    #[label(lint_label_named_arg)]
+    pub named_arg_sp: Span,
+    #[label(lint_label_position_arg)]
+    pub position_label_sp: Option<Span>,
+    #[suggestion(style = "verbose", code = "{name}", applicability = "maybe-incorrect")]
+    pub suggestion: Option<Span>,
+
+    pub name: String,
+    pub named_arg_name: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_byte_slice_in_packed_struct_with_derive)]
+#[help]
+pub struct ByteSliceInPackedStructWithDerive {
+    // FIXME: make this translatable
+    pub ty: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unused_extern_crate)]
+pub struct UnusedExternCrate {
+    #[suggestion(code = "", applicability = "machine-applicable")]
+    pub removal_span: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_extern_crate_not_idiomatic)]
+pub struct ExternCrateNotIdiomatic {
+    #[suggestion(style = "verbose", code = "{code}", applicability = "machine-applicable")]
+    pub span: Span,
+
+    pub code: &'static str,
+}
+
+// FIXME: make this translatable
+pub struct AmbiguousGlobImports {
+    pub ambiguity: AmbiguityErrorDiag,
+}
+
+impl<'a, G: EmissionGuarantee> LintDiagnostic<'a, G> for AmbiguousGlobImports {
+    fn decorate_lint<'b>(self, diag: &'b mut Diag<'a, G>) {
+        rustc_errors::report_ambiguity_error(diag, self.ambiguity);
+    }
+
+    fn msg(&self) -> DiagMessage {
+        DiagMessage::Str(self.ambiguity.msg.clone().into())
+    }
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_ambiguous_glob_reexport)]
+pub struct AmbiguousGlobReexports {
+    #[label(lint_label_first_reexport)]
+    pub first_reexport: Span,
+    #[label(lint_label_duplicate_reexport)]
+    pub duplicate_reexport: Span,
+
+    pub name: String,
+    // FIXME: make this translatable
+    pub namespace: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_hidden_glob_reexport)]
+pub struct HiddenGlobReexports {
+    #[note(lint_note_glob_reexport)]
+    pub glob_reexport: Span,
+    #[note(lint_note_private_item)]
+    pub private_item: Span,
+
+    pub name: String,
+    // FIXME: make this translatable
+    pub namespace: String,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_unnecessary_qualification)]
+pub struct UnusedQualifications {
+    #[suggestion(style = "verbose", code = "", applicability = "machine-applicable")]
+    pub removal_span: Span,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_associated_const_elided_lifetime)]
+pub struct AssociatedConstElidedLifetime {
+    #[suggestion(style = "verbose", code = "{code}", applicability = "machine-applicable")]
+    pub span: Span,
+
+    pub code: &'static str,
+    pub elided: bool,
+}
+
+#[derive(LintDiagnostic)]
+#[diag(lint_redundant_import_visibility)]
+pub struct RedundantImportVisibility {
+    #[note]
+    pub span: Span,
+    #[help]
+    pub help: (),
+
+    pub import_vis: String,
+    pub max_vis: String,
+}
diff --git a/compiler/rustc_lint_defs/src/lib.rs b/compiler/rustc_lint_defs/src/lib.rs
index 4dacafaaea6..8f3f6620cfc 100644
--- a/compiler/rustc_lint_defs/src/lib.rs
+++ b/compiler/rustc_lint_defs/src/lib.rs
@@ -5,7 +5,7 @@ use rustc_data_structures::fx::{FxIndexMap, FxIndexSet};
 use rustc_data_structures::stable_hasher::{
     HashStable, StableCompare, StableHasher, ToStableHashKey,
 };
-use rustc_error_messages::MultiSpan;
+use rustc_error_messages::{DiagMessage, MultiSpan};
 use rustc_hir::def::Namespace;
 use rustc_hir::HashStableContext;
 use rustc_hir::HirId;
@@ -591,8 +591,9 @@ pub enum BuiltinLintDiag {
         candidate: Option<Symbol>,
     },
     UnusedImports {
-        fix_msg: String,
-        fixes: Vec<(Span, String)>,
+        remove_whole_use: bool,
+        num_to_remove: usize,
+        remove_spans: Vec<Span>,
         test_module_span: Option<Span>,
         span_snippets: Vec<String>,
     },
@@ -617,7 +618,10 @@ pub enum BuiltinLintDiag {
         is_foreign: bool,
     },
     LegacyDeriveHelpers(Span),
-    ProcMacroBackCompat(String),
+    ProcMacroBackCompat {
+        crate_name: String,
+        fixed_version: String,
+    },
     OrPatternsBackCompat(Span, String),
     ReservedPrefix(Span, String),
     TrailingMacro(bool, Ident),
@@ -625,7 +629,7 @@ pub enum BuiltinLintDiag {
     UnicodeTextFlow(Span, String),
     UnexpectedCfgName((Symbol, Span), Option<(Symbol, Span)>),
     UnexpectedCfgValue((Symbol, Span), Option<(Symbol, Span)>),
-    DeprecatedWhereclauseLocation(Option<(Span, String)>),
+    DeprecatedWhereclauseLocation(Span, Option<(Span, String)>),
     SingleUseLifetime {
         /// Span of the parameter which declares this lifetime.
         param_span: Span,
@@ -708,7 +712,7 @@ pub enum BuiltinLintDiag {
     MacroIsPrivate(Ident),
     UnusedMacroDefinition(Symbol),
     MacroRuleNeverUsed(usize, Symbol),
-    UnstableFeature(String),
+    UnstableFeature(DiagMessage),
     AvoidUsingIntelSyntax,
     AvoidUsingAttSyntax,
     IncompleteInclude,
@@ -722,8 +726,7 @@ pub enum BuiltinLintDiag {
     MetaVariableWrongOperator,
     DuplicateMatcherBinding,
     UnknownMacroVariable(MacroRulesNormalizedIdent),
-    // FIXME: combine with UnusedExternCrate?
-    UnusedExternCrate2 {
+    UnusedCrateDependency {
         extern_crate: Symbol,
         local_crate: Symbol,
     },
diff --git a/compiler/rustc_metadata/src/creader.rs b/compiler/rustc_metadata/src/creader.rs
index 394222cdb35..770088e05f9 100644
--- a/compiler/rustc_metadata/src/creader.rs
+++ b/compiler/rustc_metadata/src/creader.rs
@@ -978,7 +978,7 @@ impl<'a, 'tcx> CrateLoader<'a, 'tcx> {
                 lint::builtin::UNUSED_CRATE_DEPENDENCIES,
                 span,
                 ast::CRATE_NODE_ID,
-                BuiltinLintDiag::UnusedExternCrate2 {
+                BuiltinLintDiag::UnusedCrateDependency {
                     extern_crate: name_interned,
                     local_crate: self.tcx.crate_name(LOCAL_CRATE),
                 },
diff --git a/compiler/rustc_middle/src/middle/stability.rs b/compiler/rustc_middle/src/middle/stability.rs
index 37d2c6a7078..0451ab4a06b 100644
--- a/compiler/rustc_middle/src/middle/stability.rs
+++ b/compiler/rustc_middle/src/middle/stability.rs
@@ -154,19 +154,6 @@ pub struct Deprecated {
     pub since_kind: DeprecatedSinceKind,
 }
 
-impl Deprecated {
-    // FIXME: remove
-    pub fn msg_for_since_kind(since_kind: &DeprecatedSinceKind) -> rustc_errors::DiagMessage {
-        match since_kind {
-            DeprecatedSinceKind::InEffect => crate::fluent_generated::middle_deprecated,
-            DeprecatedSinceKind::InFuture => crate::fluent_generated::middle_deprecated_in_future,
-            DeprecatedSinceKind::InVersion(_) => {
-                crate::fluent_generated::middle_deprecated_in_version
-            }
-        }
-    }
-}
-
 impl<'a, G: EmissionGuarantee> rustc_errors::LintDiagnostic<'a, G> for Deprecated {
     fn decorate_lint<'b>(self, diag: &'b mut Diag<'a, G>) {
         diag.arg("kind", self.kind);
@@ -186,7 +173,13 @@ impl<'a, G: EmissionGuarantee> rustc_errors::LintDiagnostic<'a, G> for Deprecate
     }
 
     fn msg(&self) -> rustc_errors::DiagMessage {
-        Self::msg_for_since_kind(&self.since_kind)
+        match &self.since_kind {
+            DeprecatedSinceKind::InEffect => crate::fluent_generated::middle_deprecated,
+            DeprecatedSinceKind::InFuture => crate::fluent_generated::middle_deprecated_in_future,
+            DeprecatedSinceKind::InVersion(_) => {
+                crate::fluent_generated::middle_deprecated_in_version
+            }
+        }
     }
 }
 
diff --git a/compiler/rustc_resolve/src/check_unused.rs b/compiler/rustc_resolve/src/check_unused.rs
index 55b2d91260e..93b53f510fc 100644
--- a/compiler/rustc_resolve/src/check_unused.rs
+++ b/compiler/rustc_resolve/src/check_unused.rs
@@ -433,20 +433,12 @@ impl Resolver<'_, '_> {
         visitor.report_unused_extern_crate_items(maybe_unused_extern_crates);
 
         for unused in visitor.unused_imports.values() {
-            let mut fixes = Vec::new();
-            let spans = match calc_unused_spans(unused, &unused.use_tree, unused.use_tree_id) {
-                UnusedSpanResult::Used => continue,
-                UnusedSpanResult::Unused { spans, remove } => {
-                    fixes.push((remove, String::new()));
-                    spans
-                }
-                UnusedSpanResult::PartialUnused { spans, remove } => {
-                    for fix in &remove {
-                        fixes.push((*fix, String::new()));
-                    }
-                    spans
-                }
-            };
+            let (spans, remove_spans) =
+                match calc_unused_spans(unused, &unused.use_tree, unused.use_tree_id) {
+                    UnusedSpanResult::Used => continue,
+                    UnusedSpanResult::Unused { spans, remove } => (spans, vec![remove]),
+                    UnusedSpanResult::PartialUnused { spans, remove } => (spans, remove),
+                };
 
             let ms = MultiSpan::from_spans(spans);
 
@@ -458,13 +450,8 @@ impl Resolver<'_, '_> {
                 .collect::<Vec<String>>();
             span_snippets.sort();
 
-            let fix_msg = if fixes.len() == 1 && fixes[0].0 == unused.item_span {
-                "remove the whole `use` item"
-            } else if ms.primary_spans().len() > 1 {
-                "remove the unused imports"
-            } else {
-                "remove the unused import"
-            };
+            let remove_whole_use = remove_spans.len() == 1 && remove_spans[0] == unused.item_span;
+            let num_to_remove = ms.primary_spans().len();
 
             // If we are in the `--test` mode, suppress a help that adds the `#[cfg(test)]`
             // attribute; however, if not, suggest adding the attribute. There is no way to
@@ -495,8 +482,9 @@ impl Resolver<'_, '_> {
                 unused.use_tree_id,
                 ms,
                 BuiltinLintDiag::UnusedImports {
-                    fix_msg: fix_msg.into(),
-                    fixes,
+                    remove_whole_use,
+                    num_to_remove,
+                    remove_spans,
                     test_module_span,
                     span_snippets,
                 },
diff --git a/compiler/rustc_resolve/src/macros.rs b/compiler/rustc_resolve/src/macros.rs
index 74f55d0a19c..f7073ee9a01 100644
--- a/compiler/rustc_resolve/src/macros.rs
+++ b/compiler/rustc_resolve/src/macros.rs
@@ -847,7 +847,10 @@ impl<'a, 'tcx> Resolver<'a, 'tcx> {
                             lint,
                             node_id,
                             span,
-                            BuiltinLintDiag::UnstableFeature(msg),
+                            BuiltinLintDiag::UnstableFeature(
+                                // FIXME make this translatable
+                                msg.into(),
+                            ),
                         )
                     };
                     stability::report_unstable(
diff --git a/tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.rs b/tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.rs
index bd3b69c2b4a..ca60bc62b51 100644
--- a/tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.rs
+++ b/tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.rs
@@ -45,7 +45,7 @@ mod inline {
     //~| NOTE not a function or closure
 
     #[inline = "2100"] fn f() { }
-    //~^ ERROR attribute must be of the form
+    //~^ ERROR valid forms for the attribute are
     //~| WARN this was previously accepted
     //~| NOTE #[deny(ill_formed_attribute_input)]` on by default
     //~| NOTE for more information, see issue #57571 <https://github.com/rust-lang/rust/issues/57571>
diff --git a/tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.stderr b/tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.stderr
index 89fa2abffc2..ac2bf78157d 100644
--- a/tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.stderr
+++ b/tests/ui/feature-gates/issue-43106-gating-of-builtin-attrs-error.stderr
@@ -7,7 +7,7 @@ LL | #![rustc_main]
    = help: add `#![feature(rustc_attrs)]` to the crate attributes to enable
    = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
 
-error: attribute must be of the form `#[inline]` or `#[inline(always|never)]`
+error: valid forms for the attribute are `#[inline]` and `#[inline(always|never)]`
   --> $DIR/issue-43106-gating-of-builtin-attrs-error.rs:47:5
    |
 LL |     #[inline = "2100"] fn f() { }
diff --git a/tests/ui/lint/anonymous-reexport.stderr b/tests/ui/lint/anonymous-reexport.stderr
index e3854a5459e..c4ee18fe8da 100644
--- a/tests/ui/lint/anonymous-reexport.stderr
+++ b/tests/ui/lint/anonymous-reexport.stderr
@@ -34,7 +34,7 @@ error: unused import: `Bar as _`
 LL | pub use self::my_mod::{Bar as _, Foo as _};
    |                        ^^^^^^^^
 
-error: unused imports: `Bar as _`, `TyBar as _`
+error: unused imports: `Bar as _` and `TyBar as _`
   --> $DIR/anonymous-reexport.rs:17:24
    |
 LL | pub use self::my_mod::{Bar as _, TyBar as _};
diff --git a/tests/ui/lint/unused/import_remove_line.stderr b/tests/ui/lint/unused/import_remove_line.stderr
index 0e8c5de3558..b2a16669a93 100644
--- a/tests/ui/lint/unused/import_remove_line.stderr
+++ b/tests/ui/lint/unused/import_remove_line.stderr
@@ -1,4 +1,4 @@
-warning: unused imports: `Duration`, `Instant`
+warning: unused imports: `Duration` and `Instant`
   --> $DIR/import_remove_line.rs:7:17
    |
 LL | use std::time::{Duration, Instant};
diff --git a/tests/ui/lint/unused/lint-unused-imports.rs b/tests/ui/lint/unused/lint-unused-imports.rs
index 88f2baa5da9..710fb7a5ed1 100644
--- a/tests/ui/lint/unused/lint-unused-imports.rs
+++ b/tests/ui/lint/unused/lint-unused-imports.rs
@@ -10,7 +10,7 @@ use std::fmt::{};
 
 // Should get errors for both 'Some' and 'None'
 use std::option::Option::{Some, None};
-//~^ ERROR unused imports: `None`, `Some`
+//~^ ERROR unused imports: `None` and `Some`
 
 use test::A;       //~ ERROR unused import: `test::A`
 // Be sure that if we just bring some methods into scope that they're also
diff --git a/tests/ui/lint/unused/lint-unused-imports.stderr b/tests/ui/lint/unused/lint-unused-imports.stderr
index 07684a84a64..a848fb31eba 100644
--- a/tests/ui/lint/unused/lint-unused-imports.stderr
+++ b/tests/ui/lint/unused/lint-unused-imports.stderr
@@ -10,7 +10,7 @@ note: the lint level is defined here
 LL | #![deny(unused_imports)]
    |         ^^^^^^^^^^^^^^
 
-error: unused imports: `None`, `Some`
+error: unused imports: `None` and `Some`
   --> $DIR/lint-unused-imports.rs:12:27
    |
 LL | use std::option::Option::{Some, None};
diff --git a/tests/ui/malformed/malformed-regressions.rs b/tests/ui/malformed/malformed-regressions.rs
index ac1444bbaef..f0a7aac59c1 100644
--- a/tests/ui/malformed/malformed-regressions.rs
+++ b/tests/ui/malformed/malformed-regressions.rs
@@ -1,8 +1,8 @@
-#[doc] //~ ERROR attribute must be of the form
+#[doc] //~ ERROR valid forms for the attribute are
 //~^ WARN this was previously accepted
-#[ignore()] //~ ERROR attribute must be of the form
+#[ignore()] //~ ERROR valid forms for the attribute are
 //~^ WARN this was previously accepted
-#[inline = ""] //~ ERROR attribute must be of the form
+#[inline = ""] //~ ERROR valid forms for the attribute are
 //~^ WARN this was previously accepted
 #[link] //~ ERROR attribute must be of the form
 //~^ WARN this was previously accepted
diff --git a/tests/ui/malformed/malformed-regressions.stderr b/tests/ui/malformed/malformed-regressions.stderr
index 9bfbe7ebafd..e1dbdb9ab3c 100644
--- a/tests/ui/malformed/malformed-regressions.stderr
+++ b/tests/ui/malformed/malformed-regressions.stderr
@@ -1,4 +1,4 @@
-error: attribute must be of the form `#[doc(hidden|inline|...)]` or `#[doc = "string"]`
+error: valid forms for the attribute are `#[doc(hidden|inline|...)]` and `#[doc = "string"]`
   --> $DIR/malformed-regressions.rs:1:1
    |
 LL | #[doc]
@@ -8,7 +8,7 @@ LL | #[doc]
    = note: for more information, see issue #57571 <https://github.com/rust-lang/rust/issues/57571>
    = note: `#[deny(ill_formed_attribute_input)]` on by default
 
-error: attribute must be of the form `#[ignore]` or `#[ignore = "reason"]`
+error: valid forms for the attribute are `#[ignore]` and `#[ignore = "reason"]`
   --> $DIR/malformed-regressions.rs:3:1
    |
 LL | #[ignore()]
@@ -17,7 +17,7 @@ LL | #[ignore()]
    = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
    = note: for more information, see issue #57571 <https://github.com/rust-lang/rust/issues/57571>
 
-error: attribute must be of the form `#[inline]` or `#[inline(always|never)]`
+error: valid forms for the attribute are `#[inline]` and `#[inline(always|never)]`
   --> $DIR/malformed-regressions.rs:5:1
    |
 LL | #[inline = ""]
diff --git a/tests/ui/privacy/issue-46209-private-enum-variant-reexport.rs b/tests/ui/privacy/issue-46209-private-enum-variant-reexport.rs
index 653dcab5771..796ba4d2332 100644
--- a/tests/ui/privacy/issue-46209-private-enum-variant-reexport.rs
+++ b/tests/ui/privacy/issue-46209-private-enum-variant-reexport.rs
@@ -6,7 +6,7 @@ mod rank {
     pub use self::Lieutenant::{JuniorGrade, Full};
     //~^ ERROR `JuniorGrade` is private, and cannot be re-exported
     //~| ERROR `Full` is private, and cannot be re-exported
-    //~| ERROR unused imports: `Full`, `JuniorGrade`
+    //~| ERROR unused imports: `Full` and `JuniorGrade`
     pub use self::PettyOfficer::*;
     //~^ ERROR glob import doesn't reexport anything
     //~| ERROR unused import: `self::PettyOfficer::*`
diff --git a/tests/ui/privacy/issue-46209-private-enum-variant-reexport.stderr b/tests/ui/privacy/issue-46209-private-enum-variant-reexport.stderr
index 93a39edbb82..f1701d547a6 100644
--- a/tests/ui/privacy/issue-46209-private-enum-variant-reexport.stderr
+++ b/tests/ui/privacy/issue-46209-private-enum-variant-reexport.stderr
@@ -46,7 +46,7 @@ error: unused import: `self::Professor::*`
 LL |     pub use self::Professor::*;
    |             ^^^^^^^^^^^^^^^^^^
 
-error: unused imports: `Full`, `JuniorGrade`
+error: unused imports: `Full` and `JuniorGrade`
   --> $DIR/issue-46209-private-enum-variant-reexport.rs:6:32
    |
 LL |     pub use self::Lieutenant::{JuniorGrade, Full};
diff --git a/tests/ui/span/multispan-import-lint.stderr b/tests/ui/span/multispan-import-lint.stderr
index 4a955d1b31f..a4ea1af237b 100644
--- a/tests/ui/span/multispan-import-lint.stderr
+++ b/tests/ui/span/multispan-import-lint.stderr
@@ -1,4 +1,4 @@
-warning: unused imports: `Eq`, `Ord`, `PartialEq`, `PartialOrd`
+warning: unused imports: `Eq`, `Ord`, `PartialEq`, and `PartialOrd`
   --> $DIR/multispan-import-lint.rs:5:16
    |
 LL | use std::cmp::{Eq, Ord, min, PartialEq, PartialOrd};
diff --git a/tests/ui/suggestions/unused-imports.stderr b/tests/ui/suggestions/unused-imports.stderr
index bf112608da7..31edb4e5ec2 100644
--- a/tests/ui/suggestions/unused-imports.stderr
+++ b/tests/ui/suggestions/unused-imports.stderr
@@ -1,4 +1,4 @@
-warning: unused imports: `A`, `C`
+warning: unused imports: `A` and `C`
   --> $DIR/unused-imports.rs:22:14
    |
 LL | use nested::{A, B, C};
@@ -10,7 +10,7 @@ note: the lint level is defined here
 LL | #![warn(unused_imports)]
    |         ^^^^^^^^^^^^^^
 
-warning: unused imports: `D`, `E`, `G`
+warning: unused imports: `D`, `E`, and `G`
   --> $DIR/unused-imports.rs:26:5
    |
 LL |     D,
diff --git a/tests/ui/use/use-nested-groups-unused-imports.rs b/tests/ui/use/use-nested-groups-unused-imports.rs
index ca6b8ba94d1..0c8ae558a59 100644
--- a/tests/ui/use/use-nested-groups-unused-imports.rs
+++ b/tests/ui/use/use-nested-groups-unused-imports.rs
@@ -13,7 +13,7 @@ mod foo {
 }
 
 use foo::{Foo, bar::{baz::{}, foobar::*}, *};
-    //~^ ERROR unused imports: `*`, `Foo`, `baz::{}`, `foobar::*`
+    //~^ ERROR unused imports: `*`, `Foo`, `baz::{}`, and `foobar::*`
 use foo::bar::baz::{*, *};
     //~^ ERROR unused import: `*`
 use foo::{};
diff --git a/tests/ui/use/use-nested-groups-unused-imports.stderr b/tests/ui/use/use-nested-groups-unused-imports.stderr
index 6610f8ecd4a..dd39a852772 100644
--- a/tests/ui/use/use-nested-groups-unused-imports.stderr
+++ b/tests/ui/use/use-nested-groups-unused-imports.stderr
@@ -1,4 +1,4 @@
-error: unused imports: `*`, `Foo`, `baz::{}`, `foobar::*`
+error: unused imports: `*`, `Foo`, `baz::{}`, and `foobar::*`
   --> $DIR/use-nested-groups-unused-imports.rs:15:11
    |
 LL | use foo::{Foo, bar::{baz::{}, foobar::*}, *};