From b54eb711d610368927ef915d895b23aa93d3a00d Mon Sep 17 00:00:00 2001 From: Linus Kirkwood Date: Sun, 19 Apr 2026 17:32:01 +1000 Subject: [PATCH] Lint nested `format_args!` for uninlined args Previously a `format_args!` call or a macro marked `clippy::format_args` would not be linted for uninlined arguments when one of the arguments was in turn a `format_args!` call. --- clippy_lints/src/format_args.rs | 44 ++++++++++++++++++++++----- clippy_utils/src/macros.rs | 18 +++++++++++ tests/ui/uninlined_format_args.fixed | 11 ++++--- tests/ui/uninlined_format_args.rs | 5 +-- tests/ui/uninlined_format_args.stderr | 38 ++++++++++++++++++++++- 5 files changed, 100 insertions(+), 16 deletions(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 12cf82916739..f0fccb674111 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -295,7 +295,7 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs<'tcx> { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { if let Some(macro_call) = root_macro_call_first_node(cx, expr) && is_format_macro(cx, macro_call.def_id) - && let Some(format_args) = self.format_args.get(cx, expr, macro_call.expn) + && let Some(mut format_args) = self.format_args.get(cx, expr, macro_call.expn) { let mut linter = FormatArgsExpr { cx, @@ -312,8 +312,34 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs<'tcx> { linter.check_trailing_comma(); linter.check_templates(); - if self.msrv.meets(cx, msrvs::FORMAT_ARGS_CAPTURE) { - linter.check_uninlined_args(); + if !self.msrv.meets(cx, msrvs::FORMAT_ARGS_CAPTURE) { + return; + } + + let mut uninlined_queue = vec![]; + loop { + if linter.check_uninlined_args() { + break; + } + + uninlined_queue.extend(self.format_args.get_nested(format_args)); + if let Some(inner_format_args) = uninlined_queue.pop() { + format_args = inner_format_args; + + linter = FormatArgsExpr { + cx, + expr, + macro_call: ¯o_call, + format_args, + ignore_mixed: self.ignore_mixed, + msrv: &self.msrv, + ty_msrv_map: &self.ty_msrv_map, + has_derived_debug: &mut self.has_derived_debug, + has_pointer_format: &mut self.has_pointer_format, + }; + } else { + break; + } } } } @@ -442,16 +468,16 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { } } - fn check_uninlined_args(&self) { + fn check_uninlined_args(&self) -> bool { if self.format_args.span.from_expansion() { - return; + return false; } if self.macro_call.span.edition() < Edition2021 && (is_panic(self.cx, self.macro_call.def_id) || is_assert_macro(self.cx, self.macro_call.def_id)) { // panic!, assert!, and debug_assert! before 2021 edition considers a single string argument as // non-format - return; + return false; } let mut fixes = Vec::new(); @@ -462,12 +488,12 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { // Example of an un-inlinable format: print!("{}{1}", foo, 2) for (pos, usage) in self.format_arg_positions() { if !self.check_one_arg(pos, usage, &mut fixes) { - return; + return !fixes.is_empty(); } } if fixes.is_empty() { - return; + return false; } // multiline span display suggestion is sometimes broken: https://github.com/rust-lang/rust/pull/102729#discussion_r988704308 @@ -494,6 +520,8 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { ); }, ); + + true } fn check_one_arg(&self, pos: &FormatArgPosition, usage: FormatParamUsage, fixes: &mut Vec<(Span, String)>) -> bool { diff --git a/clippy_utils/src/macros.rs b/clippy_utils/src/macros.rs index 4162595ffe81..f81893c9d3d0 100644 --- a/clippy_utils/src/macros.rs +++ b/clippy_utils/src/macros.rs @@ -433,6 +433,24 @@ impl FormatArgsStorage { self.0.get()?.get(&format_args_expr.span.with_parent(None)) } + /// Returns AST [`FormatArgs`] nodes if there are any nested inside `parent_format_args`. + pub fn get_nested(&self, parent_format_args: &FormatArgs) -> Vec<&FormatArgs> { + let mut nested = Vec::new(); + let Some(format_args_map) = self.0.get() else { + return nested; + }; + + for arg in parent_format_args.arguments.all_args() { + if matches!(arg.expr.kind, rustc_ast::ExprKind::FormatArgs(_)) + && let Some(format_args) = format_args_map.get(&arg.expr.span.with_parent(None)) + { + nested.push(format_args); + } + } + + nested + } + /// Should only be called by `FormatArgsCollector` pub fn set(&self, format_args: FxHashMap) { self.0 diff --git a/tests/ui/uninlined_format_args.fixed b/tests/ui/uninlined_format_args.fixed index 2e84bbc106ac..f5d47e003a8b 100644 --- a/tests/ui/uninlined_format_args.fixed +++ b/tests/ui/uninlined_format_args.fixed @@ -376,9 +376,10 @@ fn nested_format_args_user() { let local_i32 = 1; let local_f64 = 2.0; - // false negative: should warn but currently doesn't because the inner format_args - // is not processed when it's used as an argument to another format_args - nested_format_args!("{}", local_i32); - nested_format_args!("val='{}'", local_i32); - nested_format_args!("{:.1}", local_f64); + nested_format_args!("{local_i32}"); + //~^ uninlined_format_args + nested_format_args!("val='{local_i32}'"); + //~^ uninlined_format_args + nested_format_args!("{local_f64:.1}"); + //~^ uninlined_format_args } diff --git a/tests/ui/uninlined_format_args.rs b/tests/ui/uninlined_format_args.rs index 4368ce864474..752b54a160b0 100644 --- a/tests/ui/uninlined_format_args.rs +++ b/tests/ui/uninlined_format_args.rs @@ -381,9 +381,10 @@ fn nested_format_args_user() { let local_i32 = 1; let local_f64 = 2.0; - // false negative: should warn but currently doesn't because the inner format_args - // is not processed when it's used as an argument to another format_args nested_format_args!("{}", local_i32); + //~^ uninlined_format_args nested_format_args!("val='{}'", local_i32); + //~^ uninlined_format_args nested_format_args!("{:.1}", local_f64); + //~^ uninlined_format_args } diff --git a/tests/ui/uninlined_format_args.stderr b/tests/ui/uninlined_format_args.stderr index 45994fdc9588..11a8464bd91b 100644 --- a/tests/ui/uninlined_format_args.stderr +++ b/tests/ui/uninlined_format_args.stderr @@ -895,5 +895,41 @@ LL - usr_println!(true, "{:.1}", local_f64); LL + usr_println!(true, "{local_f64:.1}"); | -error: aborting due to 75 previous errors +error: variables can be used directly in the `format!` string + --> tests/ui/uninlined_format_args.rs:384:5 + | +LL | nested_format_args!("{}", local_i32); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: change this to + | +LL - nested_format_args!("{}", local_i32); +LL + nested_format_args!("{local_i32}"); + | + +error: variables can be used directly in the `format!` string + --> tests/ui/uninlined_format_args.rs:386:5 + | +LL | nested_format_args!("val='{}'", local_i32); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: change this to + | +LL - nested_format_args!("val='{}'", local_i32); +LL + nested_format_args!("val='{local_i32}'"); + | + +error: variables can be used directly in the `format!` string + --> tests/ui/uninlined_format_args.rs:388:5 + | +LL | nested_format_args!("{:.1}", local_f64); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: change this to + | +LL - nested_format_args!("{:.1}", local_f64); +LL + nested_format_args!("{local_f64:.1}"); + | + +error: aborting due to 78 previous errors