Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
78 changes: 77 additions & 1 deletion clippy_lints/src/format_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -229,13 +229,42 @@ 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,
pedantic,
"unnecessary trailing comma before closing parenthesis"
}

impl_lint_pass!(FormatArgs<'_> => [
FORMAT_IN_FORMAT_ARGS,
TO_STRING_IN_FORMAT_ARGS,
UNINLINED_FORMAT_ARGS,
UNNECESSARY_DEBUG_FORMATTING,
UNUSED_FORMAT_SPECS,
POINTER_FORMAT,
UNNECESSARY_TRAILING_COMMA,
]);

#[expect(clippy::struct_field_names)]
Expand Down Expand Up @@ -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) {
Expand All @@ -302,6 +332,52 @@ 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 name = self.cx.tcx.item_name(self.macro_call.def_id);
span_lint_and_sugg(
self.cx,
UNNECESSARY_TRAILING_COMMA,
removal_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
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/println_empty_string.fixed
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![allow(clippy::match_single_binding)]
#![allow(clippy::match_single_binding, clippy::unnecessary_trailing_comma)]

fn main() {
println!();
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/println_empty_string.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![allow(clippy::match_single_binding)]
#![allow(clippy::match_single_binding, clippy::unnecessary_trailing_comma)]

fn main() {
println!();
Expand Down
82 changes: 82 additions & 0 deletions tests/ui/unnecessary_trailing_comma.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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(,)"];
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(,)"); //~ 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,);
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);

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: {}",
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,);
}
82 changes: 82 additions & 0 deletions tests/ui/unnecessary_trailing_comma.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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(,)"];
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(,)",); //~ 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,);
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);

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: {}",
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,);
}
Loading