diff --git a/clippy_lints/src/write.rs b/clippy_lints/src/write.rs index a8758b65c919..d9a007635cae 100644 --- a/clippy_lints/src/write.rs +++ b/clippy_lints/src/write.rs @@ -5,8 +5,8 @@ use clippy_utils::source::{SpanRangeExt, expand_past_previous_comma}; use clippy_utils::{is_in_test, sym}; use rustc_ast::token::LitKind; use rustc_ast::{ - FormatArgPosition, FormatArgPositionKind, FormatArgs, FormatArgsPiece, FormatOptions, FormatPlaceholder, - FormatTrait, + FormatArgPosition, FormatArgPositionKind, FormatArgs, FormatArgsPiece, FormatCount, FormatOptions, + FormatPlaceholder, FormatTrait, }; use rustc_errors::Applicability; use rustc_hir::{Expr, Impl, Item, ItemKind}; @@ -556,12 +556,7 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) { // Decrement the index of the remaining by the number of replaced positional arguments if !suggestion.is_empty() { for piece in &format_args.template { - if let Some((span, index)) = positional_arg_piece_span(piece) - && suggestion.iter().all(|(s, _)| *s != span) - { - let decrement = replaced_position.iter().filter(|i| **i < index).count(); - suggestion.push((span, format!("{{{}}}", index.saturating_sub(decrement)))); - } + relocalize_format_args_indexes(piece, &mut suggestion, &replaced_position); } } @@ -574,7 +569,7 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) { } } -/// Extract Span and its index from the given `piece`, iff it's positional argument. +/// Extract Span and its index from the given `piece`, if it's positional argument. fn positional_arg_piece_span(piece: &FormatArgsPiece) -> Option<(Span, usize)> { match piece { FormatArgsPiece::Placeholder(FormatPlaceholder { @@ -591,6 +586,57 @@ fn positional_arg_piece_span(piece: &FormatArgsPiece) -> Option<(Span, usize)> { } } +/// Relocalizes the indexes of positional arguments in the format string +fn relocalize_format_args_indexes( + piece: &FormatArgsPiece, + suggestion: &mut Vec<(Span, String)>, + replaced_position: &[usize], +) { + if let FormatArgsPiece::Placeholder(FormatPlaceholder { + argument: + FormatArgPosition { + index: Ok(index), + // Only consider positional arguments + kind: FormatArgPositionKind::Number, + span: Some(span), + }, + format_options, + .. + }) = piece + { + if suggestion.iter().any(|(s, _)| s.overlaps(*span)) { + // If the span is already in the suggestion, we don't need to process it again + return; + } + + // lambda to get the decremented index based on the replaced positions + let decremented_index = |index: usize| -> usize { + let decrement = replaced_position.iter().filter(|&&i| i < index).count(); + index - decrement + }; + + suggestion.push((*span, decremented_index(*index).to_string())); + + // If there are format options, we need to handle them as well + if *format_options != FormatOptions::default() { + // lambda to process width and precision format counts and add them to the suggestion + let mut process_format_count = |count: &Option, formatter: &dyn Fn(usize) -> String| { + if let Some(FormatCount::Argument(FormatArgPosition { + index: Ok(format_arg_index), + kind: FormatArgPositionKind::Number, + span: Some(format_arg_span), + })) = count + { + suggestion.push((*format_arg_span, formatter(decremented_index(*format_arg_index)))); + } + }; + + process_format_count(&format_options.width, &|index: usize| format!("{index}$")); + process_format_count(&format_options.precision, &|index: usize| format!(".{index}$")); + } + } +} + /// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw /// /// `r#"a"#` -> (`a`, true) diff --git a/tests/ui/print_literal.fixed b/tests/ui/print_literal.fixed index 24c45a4a61b9..ebfe19c700ee 100644 --- a/tests/ui/print_literal.fixed +++ b/tests/ui/print_literal.fixed @@ -94,3 +94,14 @@ fn issue_13959() { " ); } + +fn issue_14930() { + println!("Hello x is {0:2$.1$}", 0.01, 2, 3); + //~^ print_literal + println!("Hello x is {0:2$.1$}", 0.01, 2, 3); + //~^ print_literal + println!("Hello x is {0:2$.1$}", 0.01, 2, 3); + //~^ print_literal + println!("Hello x is {0:2$.1$}", 0.01, 2, 3); + //~^ print_literal +} diff --git a/tests/ui/print_literal.rs b/tests/ui/print_literal.rs index 42ae589ca639..8f3d9be06985 100644 --- a/tests/ui/print_literal.rs +++ b/tests/ui/print_literal.rs @@ -95,3 +95,14 @@ fn issue_13959() { "# ); } + +fn issue_14930() { + println!("Hello {3} is {0:2$.1$}", 0.01, 2, 3, "x"); + //~^ print_literal + println!("Hello {2} is {0:3$.1$}", 0.01, 2, "x", 3); + //~^ print_literal + println!("Hello {1} is {0:3$.2$}", 0.01, "x", 2, 3); + //~^ print_literal + println!("Hello {0} is {1:3$.2$}", "x", 0.01, 2, 3); + //~^ print_literal +} diff --git a/tests/ui/print_literal.stderr b/tests/ui/print_literal.stderr index da663000686f..1c378017d151 100644 --- a/tests/ui/print_literal.stderr +++ b/tests/ui/print_literal.stderr @@ -229,5 +229,53 @@ LL + bar LL ~ " | -error: aborting due to 18 previous errors +error: literal with an empty format string + --> tests/ui/print_literal.rs:100:52 + | +LL | println!("Hello {3} is {0:2$.1$}", 0.01, 2, 3, "x"); + | ^^^ + | +help: try + | +LL - println!("Hello {3} is {0:2$.1$}", 0.01, 2, 3, "x"); +LL + println!("Hello x is {0:2$.1$}", 0.01, 2, 3); + | + +error: literal with an empty format string + --> tests/ui/print_literal.rs:102:49 + | +LL | println!("Hello {2} is {0:3$.1$}", 0.01, 2, "x", 3); + | ^^^ + | +help: try + | +LL - println!("Hello {2} is {0:3$.1$}", 0.01, 2, "x", 3); +LL + println!("Hello x is {0:2$.1$}", 0.01, 2, 3); + | + +error: literal with an empty format string + --> tests/ui/print_literal.rs:104:46 + | +LL | println!("Hello {1} is {0:3$.2$}", 0.01, "x", 2, 3); + | ^^^ + | +help: try + | +LL - println!("Hello {1} is {0:3$.2$}", 0.01, "x", 2, 3); +LL + println!("Hello x is {0:2$.1$}", 0.01, 2, 3); + | + +error: literal with an empty format string + --> tests/ui/print_literal.rs:106:40 + | +LL | println!("Hello {0} is {1:3$.2$}", "x", 0.01, 2, 3); + | ^^^ + | +help: try + | +LL - println!("Hello {0} is {1:3$.2$}", "x", 0.01, 2, 3); +LL + println!("Hello x is {0:2$.1$}", 0.01, 2, 3); + | + +error: aborting due to 22 previous errors diff --git a/tests/ui/write_literal.fixed b/tests/ui/write_literal.fixed index e84f768e33d0..29352fd468ea 100644 --- a/tests/ui/write_literal.fixed +++ b/tests/ui/write_literal.fixed @@ -87,3 +87,15 @@ fn issue_13959() { " ); } + +fn issue_14930() { + let mut v = Vec::new(); + writeln!(v, "Hello x is {0:2$.1$}", 0.01, 2, 3); + //~^ write_literal + writeln!(v, "Hello x is {0:2$.1$}", 0.01, 2, 3); + //~^ write_literal + writeln!(v, "Hello x is {0:2$.1$}", 0.01, 2, 3); + //~^ write_literal + writeln!(v, "Hello x is {0:2$.1$}", 0.01, 2, 3); + //~^ write_literal +} diff --git a/tests/ui/write_literal.rs b/tests/ui/write_literal.rs index fc29fcbede7e..928727527592 100644 --- a/tests/ui/write_literal.rs +++ b/tests/ui/write_literal.rs @@ -88,3 +88,15 @@ fn issue_13959() { "# ); } + +fn issue_14930() { + let mut v = Vec::new(); + writeln!(v, "Hello {3} is {0:2$.1$}", 0.01, 2, 3, "x"); + //~^ write_literal + writeln!(v, "Hello {2} is {0:3$.1$}", 0.01, 2, "x", 3); + //~^ write_literal + writeln!(v, "Hello {1} is {0:3$.2$}", 0.01, "x", 2, 3); + //~^ write_literal + writeln!(v, "Hello {0} is {1:3$.2$}", "x", 0.01, 2, 3); + //~^ write_literal +} diff --git a/tests/ui/write_literal.stderr b/tests/ui/write_literal.stderr index d53c2a7de2e0..ca37406c8114 100644 --- a/tests/ui/write_literal.stderr +++ b/tests/ui/write_literal.stderr @@ -181,5 +181,53 @@ LL + bar LL ~ " | -error: aborting due to 14 previous errors +error: literal with an empty format string + --> tests/ui/write_literal.rs:94:55 + | +LL | writeln!(v, "Hello {3} is {0:2$.1$}", 0.01, 2, 3, "x"); + | ^^^ + | +help: try + | +LL - writeln!(v, "Hello {3} is {0:2$.1$}", 0.01, 2, 3, "x"); +LL + writeln!(v, "Hello x is {0:2$.1$}", 0.01, 2, 3); + | + +error: literal with an empty format string + --> tests/ui/write_literal.rs:96:52 + | +LL | writeln!(v, "Hello {2} is {0:3$.1$}", 0.01, 2, "x", 3); + | ^^^ + | +help: try + | +LL - writeln!(v, "Hello {2} is {0:3$.1$}", 0.01, 2, "x", 3); +LL + writeln!(v, "Hello x is {0:2$.1$}", 0.01, 2, 3); + | + +error: literal with an empty format string + --> tests/ui/write_literal.rs:98:49 + | +LL | writeln!(v, "Hello {1} is {0:3$.2$}", 0.01, "x", 2, 3); + | ^^^ + | +help: try + | +LL - writeln!(v, "Hello {1} is {0:3$.2$}", 0.01, "x", 2, 3); +LL + writeln!(v, "Hello x is {0:2$.1$}", 0.01, 2, 3); + | + +error: literal with an empty format string + --> tests/ui/write_literal.rs:100:43 + | +LL | writeln!(v, "Hello {0} is {1:3$.2$}", "x", 0.01, 2, 3); + | ^^^ + | +help: try + | +LL - writeln!(v, "Hello {0} is {1:3$.2$}", "x", 0.01, 2, 3); +LL + writeln!(v, "Hello x is {0:2$.1$}", 0.01, 2, 3); + | + +error: aborting due to 18 previous errors