about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--compiler/rustc_builtin_macros/messages.ftl14
-rw-r--r--compiler/rustc_builtin_macros/src/errors.rs21
-rw-r--r--compiler/rustc_builtin_macros/src/format.rs114
-rw-r--r--src/ci/docker/host-x86_64/dist-x86_64-linux/Dockerfile4
-rw-r--r--src/librustdoc/html/format.rs77
m---------src/tools/cargo0
-rw-r--r--src/tools/opt-dist/src/main.rs4
-rw-r--r--src/tools/opt-dist/src/tests.rs9
-rw-r--r--tests/ui/did_you_mean/issue-105225-named-args.rs10
-rw-r--r--tests/ui/did_you_mean/issue-105225-named-args.stderr22
-rw-r--r--tests/ui/did_you_mean/issue-105225.fixed21
-rw-r--r--tests/ui/did_you_mean/issue-105225.rs21
-rw-r--r--tests/ui/did_you_mean/issue-105225.stderr72
13 files changed, 349 insertions, 40 deletions
diff --git a/compiler/rustc_builtin_macros/messages.ftl b/compiler/rustc_builtin_macros/messages.ftl
index 207ae8ad844..dda466b026d 100644
--- a/compiler/rustc_builtin_macros/messages.ftl
+++ b/compiler/rustc_builtin_macros/messages.ftl
@@ -137,6 +137,20 @@ builtin_macros_format_positional_after_named = positional arguments cannot follo
     .label = positional arguments must be before named arguments
     .named_args = named argument
 
+builtin_macros_format_redundant_args = redundant {$n ->
+    [one] argument
+    *[more] arguments
+    }
+    .help = {$n ->
+        [one] the formatting string already captures the binding directly, it doesn't need to be included in the argument list
+        *[more] the formatting strings already captures the bindings directly, they don't need to be included in the argument list
+    }
+    .note = {$n ->
+        [one] the formatting specifier is referencing the binding already
+        *[more] the formatting specifiers are referencing the bindings already
+    }
+    .suggestion = this can be removed
+
 builtin_macros_format_remove_raw_ident = remove the `r#`
 
 builtin_macros_format_requires_string = requires at least a format string argument
diff --git a/compiler/rustc_builtin_macros/src/errors.rs b/compiler/rustc_builtin_macros/src/errors.rs
index 1238773d58b..fde4270334b 100644
--- a/compiler/rustc_builtin_macros/src/errors.rs
+++ b/compiler/rustc_builtin_macros/src/errors.rs
@@ -647,6 +647,27 @@ pub(crate) struct FormatPositionalMismatch {
 }
 
 #[derive(Diagnostic)]
+#[diag(builtin_macros_format_redundant_args)]
+pub(crate) struct FormatRedundantArgs {
+    #[primary_span]
+    pub(crate) span: MultiSpan,
+    pub(crate) n: usize,
+
+    #[note]
+    pub(crate) note: MultiSpan,
+
+    #[subdiagnostic]
+    pub(crate) sugg: Option<FormatRedundantArgsSugg>,
+}
+
+#[derive(Subdiagnostic)]
+#[multipart_suggestion(builtin_macros_suggestion, applicability = "machine-applicable")]
+pub(crate) struct FormatRedundantArgsSugg {
+    #[suggestion_part(code = "")]
+    pub(crate) spans: Vec<Span>,
+}
+
+#[derive(Diagnostic)]
 #[diag(builtin_macros_test_case_non_item)]
 pub(crate) struct TestCaseNonItem {
     #[primary_span]
diff --git a/compiler/rustc_builtin_macros/src/format.rs b/compiler/rustc_builtin_macros/src/format.rs
index 4b5c777f4ad..214fed8e2d8 100644
--- a/compiler/rustc_builtin_macros/src/format.rs
+++ b/compiler/rustc_builtin_macros/src/format.rs
@@ -1,3 +1,4 @@
+use parse::Position::ArgumentNamed;
 use rustc_ast::ptr::P;
 use rustc_ast::tokenstream::TokenStream;
 use rustc_ast::{token, StmtKind};
@@ -7,7 +8,9 @@ use rustc_ast::{
     FormatDebugHex, FormatOptions, FormatPlaceholder, FormatSign, FormatTrait,
 };
 use rustc_data_structures::fx::FxHashSet;
-use rustc_errors::{Applicability, MultiSpan, PResult, SingleLabelManySpans};
+use rustc_errors::{
+    Applicability, DiagnosticBuilder, ErrorGuaranteed, MultiSpan, PResult, SingleLabelManySpans,
+};
 use rustc_expand::base::{self, *};
 use rustc_parse_format as parse;
 use rustc_span::symbol::{Ident, Symbol};
@@ -364,8 +367,8 @@ fn make_format_args(
     let mut unfinished_literal = String::new();
     let mut placeholder_index = 0;
 
-    for piece in pieces {
-        match piece {
+    for piece in &pieces {
+        match *piece {
             parse::Piece::String(s) => {
                 unfinished_literal.push_str(s);
             }
@@ -513,7 +516,17 @@ fn make_format_args(
         // If there's a lot of unused arguments,
         // let's check if this format arguments looks like another syntax (printf / shell).
         let detect_foreign_fmt = unused.len() > args.explicit_args().len() / 2;
-        report_missing_placeholders(ecx, unused, detect_foreign_fmt, str_style, fmt_str, fmt_span);
+        report_missing_placeholders(
+            ecx,
+            unused,
+            &used,
+            &args,
+            &pieces,
+            detect_foreign_fmt,
+            str_style,
+            fmt_str,
+            fmt_span,
+        );
     }
 
     // Only check for unused named argument names if there are no other errors to avoid causing
@@ -580,6 +593,9 @@ fn invalid_placeholder_type_error(
 fn report_missing_placeholders(
     ecx: &mut ExtCtxt<'_>,
     unused: Vec<(Span, bool)>,
+    used: &[bool],
+    args: &FormatArguments,
+    pieces: &[parse::Piece<'_>],
     detect_foreign_fmt: bool,
     str_style: Option<usize>,
     fmt_str: &str,
@@ -598,6 +614,26 @@ fn report_missing_placeholders(
         })
     };
 
+    let placeholders = pieces
+        .iter()
+        .filter_map(|piece| {
+            if let parse::Piece::NextArgument(argument) = piece && let ArgumentNamed(binding) = argument.position {
+                let span = fmt_span.from_inner(InnerSpan::new(argument.position_span.start, argument.position_span.end));
+                Some((span, binding))
+            } else { None }
+        })
+        .collect::<Vec<_>>();
+
+    if !placeholders.is_empty() {
+        if let Some(mut new_diag) =
+            report_redundant_format_arguments(ecx, &args, used, placeholders)
+        {
+            diag.cancel();
+            new_diag.emit();
+            return;
+        }
+    }
+
     // Used to ensure we only report translations for *one* kind of foreign format.
     let mut found_foreign = false;
 
@@ -685,6 +721,76 @@ fn report_missing_placeholders(
     diag.emit();
 }
 
+/// This function detects and reports unused format!() arguments that are
+/// redundant due to implicit captures (e.g. `format!("{x}", x)`).
+fn report_redundant_format_arguments<'a>(
+    ecx: &mut ExtCtxt<'a>,
+    args: &FormatArguments,
+    used: &[bool],
+    placeholders: Vec<(Span, &str)>,
+) -> Option<DiagnosticBuilder<'a, ErrorGuaranteed>> {
+    let mut fmt_arg_indices = vec![];
+    let mut args_spans = vec![];
+    let mut fmt_spans = vec![];
+
+    for (i, unnamed_arg) in args.unnamed_args().iter().enumerate().rev() {
+        let Some(ty) = unnamed_arg.expr.to_ty() else { continue };
+        let Some(argument_binding) = ty.kind.is_simple_path() else { continue };
+        let argument_binding = argument_binding.as_str();
+
+        if used[i] {
+            continue;
+        }
+
+        let matching_placeholders = placeholders
+            .iter()
+            .filter(|(_, inline_binding)| argument_binding == *inline_binding)
+            .map(|(span, _)| span)
+            .collect::<Vec<_>>();
+
+        if !matching_placeholders.is_empty() {
+            fmt_arg_indices.push(i);
+            args_spans.push(unnamed_arg.expr.span);
+            for span in &matching_placeholders {
+                if fmt_spans.contains(*span) {
+                    continue;
+                }
+                fmt_spans.push(**span);
+            }
+        }
+    }
+
+    if !args_spans.is_empty() {
+        let multispan = MultiSpan::from(fmt_spans);
+        let mut suggestion_spans = vec![];
+
+        for (arg_span, fmt_arg_idx) in args_spans.iter().zip(fmt_arg_indices.iter()) {
+            let span = if fmt_arg_idx + 1 == args.explicit_args().len() {
+                *arg_span
+            } else {
+                arg_span.until(args.explicit_args()[*fmt_arg_idx + 1].expr.span)
+            };
+
+            suggestion_spans.push(span);
+        }
+
+        let sugg = if args.named_args().len() == 0 {
+            Some(errors::FormatRedundantArgsSugg { spans: suggestion_spans })
+        } else {
+            None
+        };
+
+        return Some(ecx.create_err(errors::FormatRedundantArgs {
+            n: args_spans.len(),
+            span: MultiSpan::from(args_spans),
+            note: multispan,
+            sugg,
+        }));
+    }
+
+    None
+}
+
 /// Handle invalid references to positional arguments. Output different
 /// errors for the case where all arguments are positional and for when
 /// there are named arguments or numbered positional arguments in the
diff --git a/src/ci/docker/host-x86_64/dist-x86_64-linux/Dockerfile b/src/ci/docker/host-x86_64/dist-x86_64-linux/Dockerfile
index c04121a8bee..9a2fcb0ce0a 100644
--- a/src/ci/docker/host-x86_64/dist-x86_64-linux/Dockerfile
+++ b/src/ci/docker/host-x86_64/dist-x86_64-linux/Dockerfile
@@ -57,9 +57,9 @@ COPY host-x86_64/dist-x86_64-linux/build-clang.sh /tmp/
 RUN ./build-clang.sh
 ENV CC=clang CXX=clang++
 
-# rustc-perf version from 2023-05-30
+# rustc-perf version from 2023-10-22
 # Should also be changed in the opt-dist tool for other environments.
-ENV PERF_COMMIT 8b2ac3042e1ff2c0074455a0a3618adef97156b1
+ENV PERF_COMMIT 4f313add609f43e928e98132358e8426ed3969ae
 RUN curl -LS -o perf.zip https://ci-mirrors.rust-lang.org/rustc/rustc-perf-$PERF_COMMIT.zip && \
     unzip perf.zip && \
     mv rustc-perf-$PERF_COMMIT rustc-perf && \
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index aa3f7184b4e..29fd880af50 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -847,7 +847,7 @@ fn resolved_path<'cx>(
 fn primitive_link(
     f: &mut fmt::Formatter<'_>,
     prim: clean::PrimitiveType,
-    name: &str,
+    name: fmt::Arguments<'_>,
     cx: &Context<'_>,
 ) -> fmt::Result {
     primitive_link_fragment(f, prim, name, "", cx)
@@ -856,7 +856,7 @@ fn primitive_link(
 fn primitive_link_fragment(
     f: &mut fmt::Formatter<'_>,
     prim: clean::PrimitiveType,
-    name: &str,
+    name: fmt::Arguments<'_>,
     fragment: &str,
     cx: &Context<'_>,
 ) -> fmt::Result {
@@ -907,7 +907,7 @@ fn primitive_link_fragment(
             None => {}
         }
     }
-    f.write_str(name)?;
+    std::fmt::Display::fmt(&name, f)?;
     if needs_termination {
         write!(f, "</a>")?;
     }
@@ -977,9 +977,11 @@ fn fmt_type<'cx>(
         }
         clean::Infer => write!(f, "_"),
         clean::Primitive(clean::PrimitiveType::Never) => {
-            primitive_link(f, PrimitiveType::Never, "!", cx)
+            primitive_link(f, PrimitiveType::Never, format_args!("!"), cx)
+        }
+        clean::Primitive(prim) => {
+            primitive_link(f, prim, format_args!("{}", prim.as_sym().as_str()), cx)
         }
-        clean::Primitive(prim) => primitive_link(f, prim, prim.as_sym().as_str(), cx),
         clean::BareFunction(ref decl) => {
             if f.alternate() {
                 write!(
@@ -998,16 +1000,16 @@ fn fmt_type<'cx>(
                     decl.unsafety.print_with_space(),
                     print_abi_with_space(decl.abi)
                 )?;
-                primitive_link(f, PrimitiveType::Fn, "fn", cx)?;
+                primitive_link(f, PrimitiveType::Fn, format_args!("fn"), cx)?;
                 write!(f, "{}", decl.decl.print(cx))
             }
         }
         clean::Tuple(ref typs) => {
             match &typs[..] {
-                &[] => primitive_link(f, PrimitiveType::Unit, "()", cx),
+                &[] => primitive_link(f, PrimitiveType::Unit, format_args!("()"), cx),
                 [one] => {
                     if let clean::Generic(name) = one {
-                        primitive_link(f, PrimitiveType::Tuple, &format!("({name},)"), cx)
+                        primitive_link(f, PrimitiveType::Tuple, format_args!("({name},)"), cx)
                     } else {
                         write!(f, "(")?;
                         // Carry `f.alternate()` into this display w/o branching manually.
@@ -1028,7 +1030,10 @@ fn fmt_type<'cx>(
                         primitive_link(
                             f,
                             PrimitiveType::Tuple,
-                            &format!("({})", generic_names.iter().map(|s| s.as_str()).join(", ")),
+                            format_args!(
+                                "({})",
+                                generic_names.iter().map(|s| s.as_str()).join(", ")
+                            ),
                             cx,
                         )
                     } else {
@@ -1047,7 +1052,7 @@ fn fmt_type<'cx>(
         }
         clean::Slice(ref t) => match **t {
             clean::Generic(name) => {
-                primitive_link(f, PrimitiveType::Slice, &format!("[{name}]"), cx)
+                primitive_link(f, PrimitiveType::Slice, format_args!("[{name}]"), cx)
             }
             _ => {
                 write!(f, "[")?;
@@ -1059,7 +1064,7 @@ fn fmt_type<'cx>(
             clean::Generic(name) if !f.alternate() => primitive_link(
                 f,
                 PrimitiveType::Array,
-                &format!("[{name}; {n}]", n = Escape(n)),
+                format_args!("[{name}; {n}]", n = Escape(n)),
                 cx,
             ),
             _ => {
@@ -1069,7 +1074,12 @@ fn fmt_type<'cx>(
                     write!(f, "; {n}")?;
                 } else {
                     write!(f, "; ")?;
-                    primitive_link(f, PrimitiveType::Array, &format!("{n}", n = Escape(n)), cx)?;
+                    primitive_link(
+                        f,
+                        PrimitiveType::Array,
+                        format_args!("{n}", n = Escape(n)),
+                        cx,
+                    )?;
                 }
                 write!(f, "]")
             }
@@ -1081,22 +1091,32 @@ fn fmt_type<'cx>(
             };
 
             if matches!(**t, clean::Generic(_)) || t.is_assoc_ty() {
-                let text = if f.alternate() {
-                    format!("*{m} {ty:#}", ty = t.print(cx))
+                let ty = t.print(cx);
+                if f.alternate() {
+                    primitive_link(
+                        f,
+                        clean::PrimitiveType::RawPointer,
+                        format_args!("*{m} {ty:#}"),
+                        cx,
+                    )
                 } else {
-                    format!("*{m} {ty}", ty = t.print(cx))
-                };
-                primitive_link(f, clean::PrimitiveType::RawPointer, &text, cx)
+                    primitive_link(
+                        f,
+                        clean::PrimitiveType::RawPointer,
+                        format_args!("*{m} {ty}"),
+                        cx,
+                    )
+                }
             } else {
-                primitive_link(f, clean::PrimitiveType::RawPointer, &format!("*{m} "), cx)?;
+                primitive_link(f, clean::PrimitiveType::RawPointer, format_args!("*{m} "), cx)?;
                 fmt::Display::fmt(&t.print(cx), f)
             }
         }
         clean::BorrowedRef { lifetime: ref l, mutability, type_: ref ty } => {
-            let lt = match l {
-                Some(l) => format!("{} ", l.print()),
-                _ => String::new(),
-            };
+            let lt = display_fn(|f| match l {
+                Some(l) => write!(f, "{} ", l.print()),
+                _ => Ok(()),
+            });
             let m = mutability.print_with_space();
             let amp = if f.alternate() { "&" } else { "&amp;" };
 
@@ -1104,7 +1124,7 @@ fn fmt_type<'cx>(
                 return primitive_link(
                     f,
                     PrimitiveType::Reference,
-                    &format!("{amp}{lt}{m}{name}"),
+                    format_args!("{amp}{lt}{m}{name}"),
                     cx,
                 );
             }
@@ -1254,7 +1274,7 @@ impl clean::Impl {
             {
                 // Hardcoded anchor library/core/src/primitive_docs.rs
                 // Link should match `# Trait implementations`
-                primitive_link_fragment(f, PrimitiveType::Tuple, &format!("({name}₁, {name}₂, …, {name}ₙ)"), "#trait-implementations-1", cx)?;
+                primitive_link_fragment(f, PrimitiveType::Tuple, format_args!("({name}₁, {name}₂, …, {name}ₙ)"), "#trait-implementations-1", cx)?;
             } else if let clean::BareFunction(bare_fn) = &self.for_ &&
                 let [clean::Argument { type_: clean::Type::Generic(name), .. }] = &bare_fn.decl.inputs.values[..] &&
                 (self.kind.is_fake_variadic() || self.kind.is_auto())
@@ -1281,7 +1301,7 @@ impl clean::Impl {
                 } else {
                     ""
                 };
-                primitive_link_fragment(f, PrimitiveType::Tuple, &format!("fn ({name}₁, {name}₂, …, {name}ₙ{ellipsis})"), "#trait-implementations-1", cx)?;
+                primitive_link_fragment(f, PrimitiveType::Tuple, format_args!("fn ({name}₁, {name}₂, …, {name}ₙ{ellipsis})"), "#trait-implementations-1", cx)?;
                 // Write output.
                 if !bare_fn.decl.output.is_unit() {
                     write!(f, " -> ")?;
@@ -1665,7 +1685,12 @@ impl clean::ImportSource {
                 }
                 let name = self.path.last();
                 if let hir::def::Res::PrimTy(p) = self.path.res {
-                    primitive_link(f, PrimitiveType::from(p), name.as_str(), cx)?;
+                    primitive_link(
+                        f,
+                        PrimitiveType::from(p),
+                        format_args!("{}", name.as_str()),
+                        cx,
+                    )?;
                 } else {
                     f.write_str(name.as_str())?;
                 }
diff --git a/src/tools/cargo b/src/tools/cargo
-Subproject 8eb8acbb116e7923ea2ce33a50109933ed5ab37
+Subproject d2f6a048529eb8e9ebc55d793abd63456c98fac
diff --git a/src/tools/opt-dist/src/main.rs b/src/tools/opt-dist/src/main.rs
index 9cdff84676e..f9ff1a0a486 100644
--- a/src/tools/opt-dist/src/main.rs
+++ b/src/tools/opt-dist/src/main.rs
@@ -408,8 +408,8 @@ fn download_rustc_perf(env: &Environment) -> anyhow::Result<()> {
 
     // FIXME: add some mechanism for synchronization of this commit SHA with
     // Linux (which builds rustc-perf in a Dockerfile)
-    // rustc-perf version from 2023-05-30
-    const PERF_COMMIT: &str = "8b2ac3042e1ff2c0074455a0a3618adef97156b1";
+    // rustc-perf version from 2023-10-22
+    const PERF_COMMIT: &str = "4f313add609f43e928e98132358e8426ed3969ae";
 
     let url = format!("https://ci-mirrors.rust-lang.org/rustc/rustc-perf-{PERF_COMMIT}.zip");
     let client = reqwest::blocking::Client::builder()
diff --git a/src/tools/opt-dist/src/tests.rs b/src/tools/opt-dist/src/tests.rs
index 8000c9e7fdd..3c33cfe985d 100644
--- a/src/tools/opt-dist/src/tests.rs
+++ b/src/tools/opt-dist/src/tests.rs
@@ -24,11 +24,10 @@ pub fn run_tests(env: &Environment) -> anyhow::Result<()> {
     let host_triple = env.host_triple();
     let version = find_dist_version(&dist_dir)?;
 
-    // Extract rustc, libstd, cargo and src archives to create the optimized sysroot
+    // Extract rustc, libstd and src archives to create the optimized sysroot
     let rustc_dir = extract_dist_dir(&format!("rustc-{version}-{host_triple}"))?.join("rustc");
     let libstd_dir = extract_dist_dir(&format!("rust-std-{version}-{host_triple}"))?
         .join(format!("rust-std-{host_triple}"));
-    let cargo_dir = extract_dist_dir(&format!("cargo-{version}-{host_triple}"))?.join("cargo");
     let extracted_src_dir = extract_dist_dir(&format!("rust-src-{version}"))?.join("rust-src");
 
     // We need to manually copy libstd to the extracted rustc sysroot
@@ -47,8 +46,6 @@ pub fn run_tests(env: &Environment) -> anyhow::Result<()> {
 
     let rustc_path = rustc_dir.join("bin").join(format!("rustc{}", executable_extension()));
     assert!(rustc_path.is_file());
-    let cargo_path = cargo_dir.join("bin").join(format!("cargo{}", executable_extension()));
-    assert!(cargo_path.is_file());
 
     // Specify path to a LLVM config so that LLVM is not rebuilt.
     // It doesn't really matter which LLVM config we choose, because no sysroot will be compiled.
@@ -65,13 +62,11 @@ change-id = 115898
 
 [build]
 rustc = "{rustc}"
-cargo = "{cargo}"
 
 [target.{host_triple}]
 llvm-config = "{llvm_config}"
 "#,
         rustc = rustc_path.to_string().replace('\\', "/"),
-        cargo = cargo_path.to_string().replace('\\', "/"),
         llvm_config = llvm_config.to_string().replace('\\', "/")
     );
     log::info!("Using following `config.toml` for running tests:\n{config_content}");
@@ -84,6 +79,8 @@ llvm-config = "{llvm_config}"
         env.python_binary(),
         x_py.as_str(),
         "test",
+        "--build",
+        env.host_triple(),
         "--stage",
         "0",
         "tests/assembly",
diff --git a/tests/ui/did_you_mean/issue-105225-named-args.rs b/tests/ui/did_you_mean/issue-105225-named-args.rs
new file mode 100644
index 00000000000..38e81776576
--- /dev/null
+++ b/tests/ui/did_you_mean/issue-105225-named-args.rs
@@ -0,0 +1,10 @@
+fn main() {
+    let x = "x";
+    let y = "y";
+
+    println!("{x}", x, x = y);
+    //~^ ERROR: redundant argument
+
+    println!("{x}", x = y, x = y);
+    //~^ ERROR: duplicate argument named `x`
+}
diff --git a/tests/ui/did_you_mean/issue-105225-named-args.stderr b/tests/ui/did_you_mean/issue-105225-named-args.stderr
new file mode 100644
index 00000000000..72204102ef6
--- /dev/null
+++ b/tests/ui/did_you_mean/issue-105225-named-args.stderr
@@ -0,0 +1,22 @@
+error: redundant argument
+  --> $DIR/issue-105225-named-args.rs:5:21
+   |
+LL |     println!("{x}", x, x = y);
+   |                     ^
+   |
+note: the formatting specifier is referencing the binding already
+  --> $DIR/issue-105225-named-args.rs:5:16
+   |
+LL |     println!("{x}", x, x = y);
+   |                ^
+
+error: duplicate argument named `x`
+  --> $DIR/issue-105225-named-args.rs:8:28
+   |
+LL |     println!("{x}", x = y, x = y);
+   |                     -      ^ duplicate argument
+   |                     |
+   |                     previously here
+
+error: aborting due to 2 previous errors
+
diff --git a/tests/ui/did_you_mean/issue-105225.fixed b/tests/ui/did_you_mean/issue-105225.fixed
new file mode 100644
index 00000000000..f756be615a1
--- /dev/null
+++ b/tests/ui/did_you_mean/issue-105225.fixed
@@ -0,0 +1,21 @@
+// run-rustfix
+
+fn main() {
+    let x = "x";
+    let y = "y";
+
+    println!("{x}", );
+    //~^ ERROR: redundant argument
+
+    println!("{x} {}", x, );
+    //~^ ERROR: redundant argument
+
+    println!("{} {x}", x, );
+    //~^ ERROR: redundant argument
+
+    println!("{x} {y}", );
+    //~^ ERROR: redundant arguments
+
+    println!("{} {} {x} {y} {}", x, x, x, );
+    //~^ ERROR: redundant arguments
+}
diff --git a/tests/ui/did_you_mean/issue-105225.rs b/tests/ui/did_you_mean/issue-105225.rs
new file mode 100644
index 00000000000..91cdf0eb28f
--- /dev/null
+++ b/tests/ui/did_you_mean/issue-105225.rs
@@ -0,0 +1,21 @@
+// run-rustfix
+
+fn main() {
+    let x = "x";
+    let y = "y";
+
+    println!("{x}", x);
+    //~^ ERROR: redundant argument
+
+    println!("{x} {}", x, x);
+    //~^ ERROR: redundant argument
+
+    println!("{} {x}", x, x);
+    //~^ ERROR: redundant argument
+
+    println!("{x} {y}", x, y);
+    //~^ ERROR: redundant arguments
+
+    println!("{} {} {x} {y} {}", x, x, x, y, y);
+    //~^ ERROR: redundant arguments
+}
diff --git a/tests/ui/did_you_mean/issue-105225.stderr b/tests/ui/did_you_mean/issue-105225.stderr
new file mode 100644
index 00000000000..5fb46222bee
--- /dev/null
+++ b/tests/ui/did_you_mean/issue-105225.stderr
@@ -0,0 +1,72 @@
+error: redundant argument
+  --> $DIR/issue-105225.rs:7:21
+   |
+LL |     println!("{x}", x);
+   |                     ^ help: this can be removed
+   |
+note: the formatting specifier is referencing the binding already
+  --> $DIR/issue-105225.rs:7:16
+   |
+LL |     println!("{x}", x);
+   |                ^
+
+error: redundant argument
+  --> $DIR/issue-105225.rs:10:27
+   |
+LL |     println!("{x} {}", x, x);
+   |                           ^ help: this can be removed
+   |
+note: the formatting specifier is referencing the binding already
+  --> $DIR/issue-105225.rs:10:16
+   |
+LL |     println!("{x} {}", x, x);
+   |                ^
+
+error: redundant argument
+  --> $DIR/issue-105225.rs:13:27
+   |
+LL |     println!("{} {x}", x, x);
+   |                           ^ help: this can be removed
+   |
+note: the formatting specifier is referencing the binding already
+  --> $DIR/issue-105225.rs:13:19
+   |
+LL |     println!("{} {x}", x, x);
+   |                   ^
+
+error: redundant arguments
+  --> $DIR/issue-105225.rs:16:25
+   |
+LL |     println!("{x} {y}", x, y);
+   |                         ^  ^
+   |
+note: the formatting specifiers are referencing the bindings already
+  --> $DIR/issue-105225.rs:16:16
+   |
+LL |     println!("{x} {y}", x, y);
+   |                ^   ^
+help: this can be removed
+   |
+LL -     println!("{x} {y}", x, y);
+LL +     println!("{x} {y}", );
+   |
+
+error: redundant arguments
+  --> $DIR/issue-105225.rs:19:43
+   |
+LL |     println!("{} {} {x} {y} {}", x, x, x, y, y);
+   |                                           ^  ^
+   |
+note: the formatting specifiers are referencing the bindings already
+  --> $DIR/issue-105225.rs:19:26
+   |
+LL |     println!("{} {} {x} {y} {}", x, x, x, y, y);
+   |                          ^
+help: this can be removed
+   |
+LL -     println!("{} {} {x} {y} {}", x, x, x, y, y);
+LL +     println!("{} {} {x} {y} {}", x, x, x, );
+   |
+
+error: aborting due to 5 previous errors
+