From 24dcde85e45c997e2d71e3c7bfd3009270d96ec1 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 6 Feb 2026 14:26:36 -0500 Subject: [PATCH 1/9] Add unnecessary_trailing_comma lint Suggest removing an unnecessary trailing comma before the closing parenthesis in single-line format-like macro invocations (e.g. println!, format!, write!). The lint currently only runs on format-like macros because it relies on format-argument parsing; arbitrary user macros are not supported to avoid incorrect suggestions. - Lint is in the `style` group (allow-by-default) - Single-line only: multi-line macro invocations are not linted - Machine-applicable fix: removes the trailing comma --- CHANGELOG.md | 6 +++ clippy_lints/src/declared_lints.rs | 1 + clippy_lints/src/format_args.rs | 55 ++++++++++++++++++++++ tests/ui/println_empty_string.fixed | 2 +- tests/ui/println_empty_string.rs | 2 +- tests/ui/unnecessary_trailing_comma.fixed | 45 ++++++++++++++++++ tests/ui/unnecessary_trailing_comma.rs | 45 ++++++++++++++++++ tests/ui/unnecessary_trailing_comma.stderr | 23 +++++++++ 8 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 tests/ui/unnecessary_trailing_comma.fixed create mode 100644 tests/ui/unnecessary_trailing_comma.rs create mode 100644 tests/ui/unnecessary_trailing_comma.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 795eba1dfeaf..1e8688819a88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ document. [92b4b68...master](https://github.com/rust-lang/rust-clippy/compare/92b4b68...master) +### New Lints + +* Added [`unnecessary_trailing_comma`] to `style` (single-line format-like macros only) + [#13965](https://github.com/rust-lang/rust-clippy/issues/13965) + ## Rust 1.93 Current stable, released 2026-01-22 @@ -7136,6 +7141,7 @@ Released 2018-09-13 [`unnecessary_sort_by`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_sort_by [`unnecessary_struct_initialization`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_struct_initialization [`unnecessary_to_owned`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_to_owned +[`unnecessary_trailing_comma`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_trailing_comma [`unnecessary_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_unwrap [`unnecessary_wraps`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_wraps [`unneeded_field_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#unneeded_field_pattern diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index a04d133b0d72..18974e8210a6 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -173,6 +173,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::format_args::TO_STRING_IN_FORMAT_ARGS_INFO, crate::format_args::UNINLINED_FORMAT_ARGS_INFO, crate::format_args::UNNECESSARY_DEBUG_FORMATTING_INFO, + crate::format_args::UNNECESSARY_TRAILING_COMMA_INFO, crate::format_args::UNUSED_FORMAT_SPECS_INFO, crate::format_impl::PRINT_IN_FORMAT_IMPL_INFO, crate::format_impl::RECURSIVE_FORMAT_IMPL_INFO, diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 5fb1a0b80f1a..e797ab0eab39 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -229,6 +229,34 @@ declare_clippy_lint! { "formatting a pointer" } +declare_clippy_lint! { + /// ### What it does + /// Suggests removing an unnecessary trailing comma before the closing parenthesis in + /// single-line macro invocations. + /// + /// ### Why is this bad? + /// The trailing comma is redundant and removing it keeps the code cleaner. + /// + /// ### Known limitations + /// This lint currently only runs on format-like macros (e.g. `format!`, `println!`, + /// `write!`) because it relies on format-argument parsing; applying it to arbitrary + /// user macros could cause incorrect suggestions. It may be extended to other + /// macros in the future. Only single-line macro invocations are linted. + /// + /// ### Example + /// ```no_run + /// println!("Foo={}", 1,); + /// ``` + /// Use instead: + /// ```no_run + /// println!("Foo={}", 1); + /// ``` + #[clippy::version = "1.95.0"] + pub UNNECESSARY_TRAILING_COMMA, + style, + "unnecessary trailing comma before closing parenthesis" +} + impl_lint_pass!(FormatArgs<'_> => [ FORMAT_IN_FORMAT_ARGS, TO_STRING_IN_FORMAT_ARGS, @@ -236,6 +264,7 @@ impl_lint_pass!(FormatArgs<'_> => [ UNNECESSARY_DEBUG_FORMATTING, UNUSED_FORMAT_SPECS, POINTER_FORMAT, + UNNECESSARY_TRAILING_COMMA, ]); #[expect(clippy::struct_field_names)] @@ -280,6 +309,7 @@ impl<'tcx> LateLintPass<'tcx> for FormatArgs<'tcx> { has_pointer_format: &mut self.has_pointer_format, }; + linter.check_trailing_comma(); linter.check_templates(); if self.msrv.meets(cx, msrvs::FORMAT_ARGS_CAPTURE) { @@ -302,6 +332,31 @@ struct FormatArgsExpr<'a, 'tcx> { } impl<'tcx> FormatArgsExpr<'_, 'tcx> { + /// Check if there is a comma after the last format macro arg. + #[allow(clippy::result_large_err, reason = "due to span_to_source()")] + fn check_trailing_comma(&self) { + let sm = self.cx.sess().source_map(); + let span = self.macro_call.span.source_callsite(); + if !sm.is_multiline(span) + && let span = sm.span_extend_to_prev_char_before(span.shrink_to_hi(), ')', false) + && let Ok(span) = sm.span_extend_prev_while(span.shrink_to_lo(), |c| c.is_whitespace() || c == ',') + && let Ok(true) = sm.span_to_source(span, |src, start, end| { + Ok(src.get(start..end).is_some_and(|s| s.contains(','))) + }) + { + let name = self.cx.tcx.item_name(self.macro_call.def_id); + span_lint_and_sugg( + self.cx, + UNNECESSARY_TRAILING_COMMA, + span, + format!("unnecessary trailing comma in `{name}!` macro"), + "remove the trailing comma", + String::new(), + Applicability::MachineApplicable, + ); + } + } + fn check_templates(&mut self) { for piece in &self.format_args.template { if let FormatArgsPiece::Placeholder(placeholder) = piece diff --git a/tests/ui/println_empty_string.fixed b/tests/ui/println_empty_string.fixed index 6b1039ee8020..2c2901bc715a 100644 --- a/tests/ui/println_empty_string.fixed +++ b/tests/ui/println_empty_string.fixed @@ -1,4 +1,4 @@ -#![allow(clippy::match_single_binding)] +#![allow(clippy::match_single_binding, clippy::unnecessary_trailing_comma)] fn main() { println!(); diff --git a/tests/ui/println_empty_string.rs b/tests/ui/println_empty_string.rs index db3b8e1a0eac..bc2971f54f2c 100644 --- a/tests/ui/println_empty_string.rs +++ b/tests/ui/println_empty_string.rs @@ -1,4 +1,4 @@ -#![allow(clippy::match_single_binding)] +#![allow(clippy::match_single_binding, clippy::unnecessary_trailing_comma)] fn main() { println!(); diff --git a/tests/ui/unnecessary_trailing_comma.fixed b/tests/ui/unnecessary_trailing_comma.fixed new file mode 100644 index 000000000000..6e861af64a8e --- /dev/null +++ b/tests/ui/unnecessary_trailing_comma.fixed @@ -0,0 +1,45 @@ +// run-rustfix +#![warn(clippy::unnecessary_trailing_comma)] + +fn main() {} + +// fmt breaks - https://github.com/rust-lang/rustfmt/issues/6797 +#[rustfmt::skip] +fn simple() { + println!("Foo"); //~ unnecessary_trailing_comma + println!("Foo={}", 1); //~ unnecessary_trailing_comma + println!(concat!("b", "o", "o")); //~ unnecessary_trailing_comma + + // This should eventually work, but requires more work + println!(concat!("Foo", "=", "{}"), 1,); + println!("No params", /*"a,){ */); + println!("No params" /* "a,){*/, /*"a,){ */); + + // No trailing comma - no lint + println!("{}", 1); + println!(concat!("b", "o", "o")); + println!(concat!("Foo", "=", "{}"), 1); + + // Multi-line macro - must NOT lint (single-line only) + println!( + "very long string to prevent fmt from making it into a single line: {}", + 1, + ); +} + +// The macro invocation itself should never be fixed +// The call to println! on the other hand might be ok to suggest in the future + +macro_rules! from_macro { + (0,) => { + println!("Foo",); + }; + (1,) => { + println!("Foo={}", 1,); + }; +} + +fn from_macro() { + from_macro!(0,); + from_macro!(1,); +} diff --git a/tests/ui/unnecessary_trailing_comma.rs b/tests/ui/unnecessary_trailing_comma.rs new file mode 100644 index 000000000000..d632d38de2ef --- /dev/null +++ b/tests/ui/unnecessary_trailing_comma.rs @@ -0,0 +1,45 @@ +// run-rustfix +#![warn(clippy::unnecessary_trailing_comma)] + +fn main() {} + +// fmt breaks - https://github.com/rust-lang/rustfmt/issues/6797 +#[rustfmt::skip] +fn simple() { + println!("Foo" , ); //~ unnecessary_trailing_comma + println!("Foo={}", 1,); //~ unnecessary_trailing_comma + println!(concat!("b", "o", "o"),); //~ unnecessary_trailing_comma + + // This should eventually work, but requires more work + println!(concat!("Foo", "=", "{}"), 1,); + println!("No params", /*"a,){ */); + println!("No params" /* "a,){*/, /*"a,){ */); + + // No trailing comma - no lint + println!("{}", 1); + println!(concat!("b", "o", "o")); + println!(concat!("Foo", "=", "{}"), 1); + + // Multi-line macro - must NOT lint (single-line only) + println!( + "very long string to prevent fmt from making it into a single line: {}", + 1, + ); +} + +// The macro invocation itself should never be fixed +// The call to println! on the other hand might be ok to suggest in the future + +macro_rules! from_macro { + (0,) => { + println!("Foo",); + }; + (1,) => { + println!("Foo={}", 1,); + }; +} + +fn from_macro() { + from_macro!(0,); + from_macro!(1,); +} diff --git a/tests/ui/unnecessary_trailing_comma.stderr b/tests/ui/unnecessary_trailing_comma.stderr new file mode 100644 index 000000000000..37afcbca2aba --- /dev/null +++ b/tests/ui/unnecessary_trailing_comma.stderr @@ -0,0 +1,23 @@ +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:9:19 + | +LL | println!("Foo" , ); + | ^^^ help: remove the trailing comma + | + = note: `-D clippy::unnecessary-trailing-comma` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::unnecessary_trailing_comma)]` + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:10:25 + | +LL | println!("Foo={}", 1,); + | ^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:11:36 + | +LL | println!(concat!("b", "o", "o"),); + | ^ help: remove the trailing comma + +error: aborting due to 3 previous errors + From 81ee1047030e49c5e4338d941678cdc1fe125e19 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 7 Feb 2026 14:22:59 -0500 Subject: [PATCH 2/9] work with every type of macro delimeters --- clippy_lints/src/format_args.rs | 7 ++++++- tests/ui/unnecessary_trailing_comma.fixed | 2 ++ tests/ui/unnecessary_trailing_comma.rs | 2 ++ tests/ui/unnecessary_trailing_comma.stderr | 18 +++++++++++++++--- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index e797ab0eab39..70d3bacff995 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -338,7 +338,12 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { let sm = self.cx.sess().source_map(); let span = self.macro_call.span.source_callsite(); if !sm.is_multiline(span) - && let span = sm.span_extend_to_prev_char_before(span.shrink_to_hi(), ')', false) + && let span = span.shrink_to_hi() + && let Some(span) = [')', ']', '}'].into_iter().find_map(|c| { + // remove one of the allowed closing macro delimeters + let s = sm.span_extend_to_prev_char_before(span, c, false); + if s != span { Some(s) } else { None } + }) && let Ok(span) = sm.span_extend_prev_while(span.shrink_to_lo(), |c| c.is_whitespace() || c == ',') && let Ok(true) = sm.span_to_source(span, |src, start, end| { Ok(src.get(start..end).is_some_and(|s| s.contains(','))) diff --git a/tests/ui/unnecessary_trailing_comma.fixed b/tests/ui/unnecessary_trailing_comma.fixed index 6e861af64a8e..6deee8e2708b 100644 --- a/tests/ui/unnecessary_trailing_comma.fixed +++ b/tests/ui/unnecessary_trailing_comma.fixed @@ -7,6 +7,8 @@ fn main() {} #[rustfmt::skip] fn simple() { println!("Foo"); //~ unnecessary_trailing_comma + println!{"Foo"}; //~ unnecessary_trailing_comma + println!["Foo"]; //~ unnecessary_trailing_comma println!("Foo={}", 1); //~ unnecessary_trailing_comma println!(concat!("b", "o", "o")); //~ unnecessary_trailing_comma diff --git a/tests/ui/unnecessary_trailing_comma.rs b/tests/ui/unnecessary_trailing_comma.rs index d632d38de2ef..70a7086585e9 100644 --- a/tests/ui/unnecessary_trailing_comma.rs +++ b/tests/ui/unnecessary_trailing_comma.rs @@ -7,6 +7,8 @@ fn main() {} #[rustfmt::skip] fn simple() { println!("Foo" , ); //~ unnecessary_trailing_comma + println!{"Foo" , }; //~ unnecessary_trailing_comma + println!["Foo" , ]; //~ unnecessary_trailing_comma println!("Foo={}", 1,); //~ unnecessary_trailing_comma println!(concat!("b", "o", "o"),); //~ unnecessary_trailing_comma diff --git a/tests/ui/unnecessary_trailing_comma.stderr b/tests/ui/unnecessary_trailing_comma.stderr index 37afcbca2aba..84c30f243de4 100644 --- a/tests/ui/unnecessary_trailing_comma.stderr +++ b/tests/ui/unnecessary_trailing_comma.stderr @@ -8,16 +8,28 @@ LL | println!("Foo" , ); = help: to override `-D warnings` add `#[allow(clippy::unnecessary_trailing_comma)]` error: unnecessary trailing comma in `println!` macro - --> tests/ui/unnecessary_trailing_comma.rs:10:25 + --> tests/ui/unnecessary_trailing_comma.rs:10:19 + | +LL | println!{"Foo" , }; + | ^^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:11:19 + | +LL | println!["Foo" , ]; + | ^^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:12:25 | LL | println!("Foo={}", 1,); | ^ help: remove the trailing comma error: unnecessary trailing comma in `println!` macro - --> tests/ui/unnecessary_trailing_comma.rs:11:36 + --> tests/ui/unnecessary_trailing_comma.rs:13:36 | LL | println!(concat!("b", "o", "o"),); | ^ help: remove the trailing comma -error: aborting due to 3 previous errors +error: aborting due to 5 previous errors From be96d79023c079271018415f6a079f949e52afbf Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 7 Feb 2026 14:28:13 -0500 Subject: [PATCH 3/9] fix ci issue --- clippy_lints/src/format_args.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 70d3bacff995..4373641391f2 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -340,9 +340,9 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { if !sm.is_multiline(span) && let span = span.shrink_to_hi() && let Some(span) = [')', ']', '}'].into_iter().find_map(|c| { - // remove one of the allowed closing macro delimeters + // remove one of the allowed closing macro delimiters let s = sm.span_extend_to_prev_char_before(span, c, false); - if s != span { Some(s) } else { None } + if s == span { None } else { Some(s) } }) && let Ok(span) = sm.span_extend_prev_while(span.shrink_to_lo(), |c| c.is_whitespace() || c == ',') && let Ok(true) = sm.span_to_source(span, |src, start, end| { From 144bdae0239097bb1c0fbc5c774ee5fe996df381 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 7 Feb 2026 19:38:05 -0500 Subject: [PATCH 4/9] fix span computation --- clippy_lints/src/format_args.rs | 38 +++++--- tests/ui/unnecessary_trailing_comma.fixed | 37 +++++++- tests/ui/unnecessary_trailing_comma.rs | 39 +++++++- tests/ui/unnecessary_trailing_comma.stderr | 104 +++++++++++++++++++-- 4 files changed, 194 insertions(+), 24 deletions(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 4373641391f2..350d070ed814 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -28,7 +28,7 @@ use rustc_middle::ty::adjustment::{Adjust, Adjustment, DerefAdjustKind}; use rustc_middle::ty::{self, GenericArg, List, TraitRef, Ty, TyCtxt, Upcast}; use rustc_session::impl_lint_pass; use rustc_span::edition::Edition::Edition2021; -use rustc_span::{Span, Symbol, sym}; +use rustc_span::{BytePos, Pos, Span, SpanSnippetError, Symbol, sym}; use rustc_trait_selection::infer::TyCtxtInferExt; use rustc_trait_selection::traits::{Obligation, ObligationCause, Selection, SelectionContext}; @@ -338,22 +338,38 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { let sm = self.cx.sess().source_map(); let span = self.macro_call.span.source_callsite(); if !sm.is_multiline(span) - && let span = span.shrink_to_hi() - && let Some(span) = [')', ']', '}'].into_iter().find_map(|c| { - // remove one of the allowed closing macro delimiters - let s = sm.span_extend_to_prev_char_before(span, c, false); - if s == span { None } else { Some(s) } - }) - && let Ok(span) = sm.span_extend_prev_while(span.shrink_to_lo(), |c| c.is_whitespace() || c == ',') - && let Ok(true) = sm.span_to_source(span, |src, start, end| { - Ok(src.get(start..end).is_some_and(|s| s.contains(','))) + && let Ok(removal_span) = sm.span_to_source(span, |s, start, end| { + // This fn returns a span between the last non-whitespace character + // and the closing parenthesis, but only if it contains a ',' char. + // Iterates in reverse, checking last char is a closing parenthesis, + // then looking for a comma before it, ignoring whitespace. + if let Some(text) = s.get(start..end) + && let mut chars = text.char_indices().rev() + && let Some((last_char_index, ')' | ']' | '}')) = chars.next() + { + let mut has_comma = false; + while let Some((index, c)) = chars.next() { + if c == ',' { + has_comma = true; + } else if c.is_whitespace() { + continue; + } else if has_comma { + return Ok(span + .with_lo(span.lo() + BytePos::from_usize(index + c.len_utf8())) + .with_hi(span.lo() + BytePos::from_usize(last_char_index))); + } else { + break; + } + } + } + Err(SpanSnippetError::IllFormedSpan(span)) }) { let name = self.cx.tcx.item_name(self.macro_call.def_id); span_lint_and_sugg( self.cx, UNNECESSARY_TRAILING_COMMA, - span, + removal_span, format!("unnecessary trailing comma in `{name}!` macro"), "remove the trailing comma", String::new(), diff --git a/tests/ui/unnecessary_trailing_comma.fixed b/tests/ui/unnecessary_trailing_comma.fixed index 6deee8e2708b..d296fb967d31 100644 --- a/tests/ui/unnecessary_trailing_comma.fixed +++ b/tests/ui/unnecessary_trailing_comma.fixed @@ -6,11 +6,26 @@ fn main() {} // fmt breaks - https://github.com/rust-lang/rustfmt/issues/6797 #[rustfmt::skip] fn simple() { + println!["Foo(,)"]; println!("Foo"); //~ unnecessary_trailing_comma println!{"Foo"}; //~ unnecessary_trailing_comma println!["Foo"]; //~ unnecessary_trailing_comma - println!("Foo={}", 1); //~ unnecessary_trailing_comma + println!("Foo={}", 1); //~ unnecessary_trailing_comma println!(concat!("b", "o", "o")); //~ unnecessary_trailing_comma + println!("Foo(,)"); //~ unnecessary_trailing_comma + println!("Foo[,]"); //~ unnecessary_trailing_comma + println!["Foo(,)"]; //~ unnecessary_trailing_comma + println!["Foo[,]"]; //~ unnecessary_trailing_comma + println!["Foo{{,}}"]; //~ unnecessary_trailing_comma + println!{"Foo{{,}}"}; //~ unnecessary_trailing_comma + println!{"Foo(,)"}; //~ unnecessary_trailing_comma + println!{"Foo[,]"}; //~ unnecessary_trailing_comma + println!["Foo(,"]; //~ unnecessary_trailing_comma + println!["Foo[,"]; //~ unnecessary_trailing_comma + println!["Foo{{,}}"]; //~ unnecessary_trailing_comma + println!{"Foo{{,}}"}; //~ unnecessary_trailing_comma + println!{"Foo(,"}; //~ unnecessary_trailing_comma + println!{"Foo[,"}; //~ unnecessary_trailing_comma // This should eventually work, but requires more work println!(concat!("Foo", "=", "{}"), 1,); @@ -22,6 +37,26 @@ fn simple() { println!(concat!("b", "o", "o")); println!(concat!("Foo", "=", "{}"), 1); + println!("Foo" ); + println!{"Foo" }; + println!["Foo" ]; + println!("Foo={}", 1); + println!(concat!("b", "o", "o")); + println!("Foo(,)"); + println!("Foo[,]"); + println!["Foo(,)"]; + println!["Foo[,]"]; + println!["Foo{{,}}"]; + println!{"Foo{{,}}"}; + println!{"Foo(,)"}; + println!{"Foo[,]"}; + println!["Foo(,"]; + println!["Foo[,"]; + println!["Foo{{,}}"]; + println!{"Foo{{,}}"}; + println!{"Foo(,"}; + println!{"Foo[,"}; + // Multi-line macro - must NOT lint (single-line only) println!( "very long string to prevent fmt from making it into a single line: {}", diff --git a/tests/ui/unnecessary_trailing_comma.rs b/tests/ui/unnecessary_trailing_comma.rs index 70a7086585e9..fc8a5de52896 100644 --- a/tests/ui/unnecessary_trailing_comma.rs +++ b/tests/ui/unnecessary_trailing_comma.rs @@ -6,11 +6,26 @@ fn main() {} // fmt breaks - https://github.com/rust-lang/rustfmt/issues/6797 #[rustfmt::skip] fn simple() { + println!["Foo(,)"]; println!("Foo" , ); //~ unnecessary_trailing_comma println!{"Foo" , }; //~ unnecessary_trailing_comma println!["Foo" , ]; //~ unnecessary_trailing_comma - println!("Foo={}", 1,); //~ unnecessary_trailing_comma - println!(concat!("b", "o", "o"),); //~ unnecessary_trailing_comma + println!("Foo={}", 1 , ); //~ unnecessary_trailing_comma + println!(concat!("b", "o", "o") , ); //~ unnecessary_trailing_comma + println!("Foo(,)",); //~ unnecessary_trailing_comma + println!("Foo[,]" , ); //~ unnecessary_trailing_comma + println!["Foo(,)", ]; //~ unnecessary_trailing_comma + println!["Foo[,]", ]; //~ unnecessary_trailing_comma + println!["Foo{{,}}", ]; //~ unnecessary_trailing_comma + println!{"Foo{{,}}", }; //~ unnecessary_trailing_comma + println!{"Foo(,)", }; //~ unnecessary_trailing_comma + println!{"Foo[,]", }; //~ unnecessary_trailing_comma + println!["Foo(,", ]; //~ unnecessary_trailing_comma + println!["Foo[,", ]; //~ unnecessary_trailing_comma + println!["Foo{{,}}", ]; //~ unnecessary_trailing_comma + println!{"Foo{{,}}", }; //~ unnecessary_trailing_comma + println!{"Foo(,", }; //~ unnecessary_trailing_comma + println!{"Foo[,", }; //~ unnecessary_trailing_comma // This should eventually work, but requires more work println!(concat!("Foo", "=", "{}"), 1,); @@ -22,6 +37,26 @@ fn simple() { println!(concat!("b", "o", "o")); println!(concat!("Foo", "=", "{}"), 1); + println!("Foo" ); + println!{"Foo" }; + println!["Foo" ]; + println!("Foo={}", 1); + println!(concat!("b", "o", "o")); + println!("Foo(,)"); + println!("Foo[,]"); + println!["Foo(,)"]; + println!["Foo[,]"]; + println!["Foo{{,}}"]; + println!{"Foo{{,}}"}; + println!{"Foo(,)"}; + println!{"Foo[,]"}; + println!["Foo(,"]; + println!["Foo[,"]; + println!["Foo{{,}}"]; + println!{"Foo{{,}}"}; + println!{"Foo(,"}; + println!{"Foo[,"}; + // Multi-line macro - must NOT lint (single-line only) println!( "very long string to prevent fmt from making it into a single line: {}", diff --git a/tests/ui/unnecessary_trailing_comma.stderr b/tests/ui/unnecessary_trailing_comma.stderr index 84c30f243de4..195e17c799ff 100644 --- a/tests/ui/unnecessary_trailing_comma.stderr +++ b/tests/ui/unnecessary_trailing_comma.stderr @@ -1,5 +1,5 @@ error: unnecessary trailing comma in `println!` macro - --> tests/ui/unnecessary_trailing_comma.rs:9:19 + --> tests/ui/unnecessary_trailing_comma.rs:10:19 | LL | println!("Foo" , ); | ^^^ help: remove the trailing comma @@ -8,28 +8,112 @@ LL | println!("Foo" , ); = help: to override `-D warnings` add `#[allow(clippy::unnecessary_trailing_comma)]` error: unnecessary trailing comma in `println!` macro - --> tests/ui/unnecessary_trailing_comma.rs:10:19 + --> tests/ui/unnecessary_trailing_comma.rs:11:19 | LL | println!{"Foo" , }; | ^^^ help: remove the trailing comma error: unnecessary trailing comma in `println!` macro - --> tests/ui/unnecessary_trailing_comma.rs:11:19 + --> tests/ui/unnecessary_trailing_comma.rs:12:19 | LL | println!["Foo" , ]; | ^^^ help: remove the trailing comma error: unnecessary trailing comma in `println!` macro - --> tests/ui/unnecessary_trailing_comma.rs:12:25 + --> tests/ui/unnecessary_trailing_comma.rs:13:27 + | +LL | println!("Foo={}", 1 , ); + | ^^^^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:14:36 + | +LL | println!(concat!("b", "o", "o") , ); + | ^^^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:15:22 + | +LL | println!("Foo(,)",); + | ^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:16:22 + | +LL | println!("Foo[,]" , ); + | ^^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:17:22 + | +LL | println!["Foo(,)", ]; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:18:22 + | +LL | println!["Foo[,]", ]; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:19:24 + | +LL | println!["Foo{{,}}", ]; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:20:24 + | +LL | println!{"Foo{{,}}", }; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:21:22 + | +LL | println!{"Foo(,)", }; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:22:22 + | +LL | println!{"Foo[,]", }; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:23:21 + | +LL | println!["Foo(,", ]; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:24:21 + | +LL | println!["Foo[,", ]; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:25:24 + | +LL | println!["Foo{{,}}", ]; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:26:24 + | +LL | println!{"Foo{{,}}", }; + | ^^ help: remove the trailing comma + +error: unnecessary trailing comma in `println!` macro + --> tests/ui/unnecessary_trailing_comma.rs:27:21 | -LL | println!("Foo={}", 1,); - | ^ help: remove the trailing comma +LL | println!{"Foo(,", }; + | ^^ help: remove the trailing comma error: unnecessary trailing comma in `println!` macro - --> tests/ui/unnecessary_trailing_comma.rs:13:36 + --> tests/ui/unnecessary_trailing_comma.rs:28:21 | -LL | println!(concat!("b", "o", "o"),); - | ^ help: remove the trailing comma +LL | println!{"Foo[,", }; + | ^^ help: remove the trailing comma -error: aborting due to 5 previous errors +error: aborting due to 19 previous errors From 257c25490cd808d1bbdb9c57331d6432f4c796e8 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 8 Feb 2026 04:17:40 -0500 Subject: [PATCH 5/9] address clippy lints --- clippy_lints/src/format_args.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 350d070ed814..76d07742d34f 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -348,11 +348,11 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { && let Some((last_char_index, ')' | ']' | '}')) = chars.next() { let mut has_comma = false; - while let Some((index, c)) = chars.next() { + for (index, c) in chars { if c == ',' { has_comma = true; } else if c.is_whitespace() { - continue; + // keep iterating } else if has_comma { return Ok(span .with_lo(span.lo() + BytePos::from_usize(index + c.len_utf8())) From 7716e7cc78b7d169ff1bff2eb1d71bc365cc3e39 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Wed, 11 Feb 2026 15:44:06 -0500 Subject: [PATCH 6/9] switch to pedantic --- clippy_lints/src/format_args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 76d07742d34f..0c2adfc2bb77 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -253,7 +253,7 @@ declare_clippy_lint! { /// ``` #[clippy::version = "1.95.0"] pub UNNECESSARY_TRAILING_COMMA, - style, + pedantic, "unnecessary trailing comma before closing parenthesis" } From 74d308a1966deb0ad9104003240f6fd5098940e6 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 13 Feb 2026 17:31:18 -0500 Subject: [PATCH 7/9] per feedback --- clippy_lints/src/format_args.rs | 47 ++++++---------------- tests/ui/unnecessary_trailing_comma.stderr | 38 ++++++++--------- 2 files changed, 31 insertions(+), 54 deletions(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 0c2adfc2bb77..bbbd148570ac 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -28,7 +28,7 @@ use rustc_middle::ty::adjustment::{Adjust, Adjustment, DerefAdjustKind}; use rustc_middle::ty::{self, GenericArg, List, TraitRef, Ty, TyCtxt, Upcast}; use rustc_session::impl_lint_pass; use rustc_span::edition::Edition::Edition2021; -use rustc_span::{BytePos, Pos, Span, SpanSnippetError, Symbol, sym}; +use rustc_span::{BytePos, Pos, Span, Symbol, sym}; use rustc_trait_selection::infer::TyCtxtInferExt; use rustc_trait_selection::traits::{Obligation, ObligationCause, Selection, SelectionContext}; @@ -235,7 +235,8 @@ declare_clippy_lint! { /// single-line macro invocations. /// /// ### Why is this bad? - /// The trailing comma is redundant and removing it keeps the code cleaner. + /// The trailing comma is redundant and removing it is more consistent with how + /// `rustfmt` formats regular function calls. /// /// ### Known limitations /// This lint currently only runs on format-like macros (e.g. `format!`, `println!`, @@ -333,44 +334,20 @@ struct FormatArgsExpr<'a, 'tcx> { impl<'tcx> FormatArgsExpr<'_, 'tcx> { /// Check if there is a comma after the last format macro arg. - #[allow(clippy::result_large_err, reason = "due to span_to_source()")] fn check_trailing_comma(&self) { - let sm = self.cx.sess().source_map(); - let span = self.macro_call.span.source_callsite(); - if !sm.is_multiline(span) - && let Ok(removal_span) = sm.span_to_source(span, |s, start, end| { - // This fn returns a span between the last non-whitespace character - // and the closing parenthesis, but only if it contains a ',' char. - // Iterates in reverse, checking last char is a closing parenthesis, - // then looking for a comma before it, ignoring whitespace. - if let Some(text) = s.get(start..end) - && let mut chars = text.char_indices().rev() - && let Some((last_char_index, ')' | ']' | '}')) = chars.next() - { - let mut has_comma = false; - for (index, c) in chars { - if c == ',' { - has_comma = true; - } else if c.is_whitespace() { - // keep iterating - } else if has_comma { - return Ok(span - .with_lo(span.lo() + BytePos::from_usize(index + c.len_utf8())) - .with_hi(span.lo() + BytePos::from_usize(last_char_index))); - } else { - break; - } - } - } - Err(SpanSnippetError::IllFormedSpan(span)) - }) + let span = self.macro_call.span; + if let Some(src) = span.get_source_text(self.cx) + && let Some(src) = src.strip_suffix([')', ']', '}']) + && let src = src.trim_end_matches(|c: char| c.is_whitespace() && c != '\n') + && let Some(src) = src.strip_suffix(',') { - let name = self.cx.tcx.item_name(self.macro_call.def_id); + let src = src.trim_end_matches(|c: char| c.is_whitespace() && c != '\n'); span_lint_and_sugg( self.cx, UNNECESSARY_TRAILING_COMMA, - removal_span, - format!("unnecessary trailing comma in `{name}!` macro"), + span.with_lo(span.lo() + BytePos::from_usize(src.len())) + .with_hi(span.hi() - BytePos(1)), + "unnecessary trailing comma", "remove the trailing comma", String::new(), Applicability::MachineApplicable, diff --git a/tests/ui/unnecessary_trailing_comma.stderr b/tests/ui/unnecessary_trailing_comma.stderr index 195e17c799ff..06fd5b1861a5 100644 --- a/tests/ui/unnecessary_trailing_comma.stderr +++ b/tests/ui/unnecessary_trailing_comma.stderr @@ -1,4 +1,4 @@ -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:10:19 | LL | println!("Foo" , ); @@ -7,109 +7,109 @@ LL | println!("Foo" , ); = note: `-D clippy::unnecessary-trailing-comma` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::unnecessary_trailing_comma)]` -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:11:19 | LL | println!{"Foo" , }; | ^^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:12:19 | LL | println!["Foo" , ]; | ^^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:13:27 | LL | println!("Foo={}", 1 , ); | ^^^^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:14:36 | LL | println!(concat!("b", "o", "o") , ); | ^^^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:15:22 | LL | println!("Foo(,)",); | ^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:16:22 | LL | println!("Foo[,]" , ); | ^^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:17:22 | LL | println!["Foo(,)", ]; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:18:22 | LL | println!["Foo[,]", ]; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:19:24 | LL | println!["Foo{{,}}", ]; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:20:24 | LL | println!{"Foo{{,}}", }; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:21:22 | LL | println!{"Foo(,)", }; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:22:22 | LL | println!{"Foo[,]", }; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:23:21 | LL | println!["Foo(,", ]; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:24:21 | LL | println!["Foo[,", ]; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:25:24 | LL | println!["Foo{{,}}", ]; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:26:24 | LL | println!{"Foo{{,}}", }; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:27:21 | LL | println!{"Foo(,", }; | ^^ help: remove the trailing comma -error: unnecessary trailing comma in `println!` macro +error: unnecessary trailing comma --> tests/ui/unnecessary_trailing_comma.rs:28:21 | LL | println!{"Foo[,", }; From 700c4ee32c6caed091b99ed3d0a315ad096d5d53 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 15 Feb 2026 02:46:52 -0500 Subject: [PATCH 8/9] ignore multiline cases --- clippy_lints/src/format_args.rs | 5 +++-- tests/ui/unnecessary_trailing_comma.fixed | 4 ++++ tests/ui/unnecessary_trailing_comma.rs | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index bbbd148570ac..15a0679538c5 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -337,11 +337,12 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { fn check_trailing_comma(&self) { let span = self.macro_call.span; if let Some(src) = span.get_source_text(self.cx) + && !src.contains('\n') && let Some(src) = src.strip_suffix([')', ']', '}']) - && let src = src.trim_end_matches(|c: char| c.is_whitespace() && c != '\n') + && let src = src.trim_end_matches(|c: char| c.is_whitespace()) && let Some(src) = src.strip_suffix(',') { - let src = src.trim_end_matches(|c: char| c.is_whitespace() && c != '\n'); + let src = src.trim_end_matches(|c: char| c.is_whitespace()); span_lint_and_sugg( self.cx, UNNECESSARY_TRAILING_COMMA, diff --git a/tests/ui/unnecessary_trailing_comma.fixed b/tests/ui/unnecessary_trailing_comma.fixed index d296fb967d31..499d9ee1ca23 100644 --- a/tests/ui/unnecessary_trailing_comma.fixed +++ b/tests/ui/unnecessary_trailing_comma.fixed @@ -62,6 +62,10 @@ fn simple() { "very long string to prevent fmt from making it into a single line: {}", 1, ); + + print!("{}" + , 1 + ,); } // The macro invocation itself should never be fixed diff --git a/tests/ui/unnecessary_trailing_comma.rs b/tests/ui/unnecessary_trailing_comma.rs index fc8a5de52896..15dea27b887b 100644 --- a/tests/ui/unnecessary_trailing_comma.rs +++ b/tests/ui/unnecessary_trailing_comma.rs @@ -62,6 +62,10 @@ fn simple() { "very long string to prevent fmt from making it into a single line: {}", 1, ); + + print!("{}" + , 1 + ,); } // The macro invocation itself should never be fixed From 3c01f64a00c73e4bee25fc3d6dcc61702ab2a5c8 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 15 Feb 2026 22:40:41 -0500 Subject: [PATCH 9/9] fix: optimize per suggestion --- clippy_lints/src/format_args.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clippy_lints/src/format_args.rs b/clippy_lints/src/format_args.rs index 15a0679538c5..3eb358917a0e 100644 --- a/clippy_lints/src/format_args.rs +++ b/clippy_lints/src/format_args.rs @@ -337,12 +337,12 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> { fn check_trailing_comma(&self) { let span = self.macro_call.span; if let Some(src) = span.get_source_text(self.cx) - && !src.contains('\n') && let Some(src) = src.strip_suffix([')', ']', '}']) - && let src = src.trim_end_matches(|c: char| c.is_whitespace()) + && let src = src.trim_end_matches(|c: char| c.is_whitespace() && c != '\n') && let Some(src) = src.strip_suffix(',') + && let src = src.trim_end_matches(|c: char| c.is_whitespace() && c != '\n') + && !src.ends_with('\n') { - let src = src.trim_end_matches(|c: char| c.is_whitespace()); span_lint_and_sugg( self.cx, UNNECESSARY_TRAILING_COMMA,