Skip to content

write_literal: Fix index of the remaining positional arguments #11576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 28, 2023
Merged
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
94 changes: 66 additions & 28 deletions clippy_lints/src/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ use clippy_utils::macros::{find_format_args, format_arg_removal_span, root_macro
use clippy_utils::source::{expand_past_previous_comma, snippet_opt};
use clippy_utils::{is_in_cfg_test, is_in_test_function};
use rustc_ast::token::LitKind;
use rustc_ast::{FormatArgPosition, FormatArgs, FormatArgsPiece, FormatOptions, FormatPlaceholder, FormatTrait};
use rustc_ast::{
FormatArgPosition, FormatArgPositionKind, FormatArgs, FormatArgsPiece, FormatOptions, FormatPlaceholder,
FormatTrait,
};
use rustc_errors::Applicability;
use rustc_hir::{Expr, Impl, Item, ItemKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::{sym, BytePos};
use rustc_span::{sym, BytePos, Span};

declare_clippy_lint! {
/// ### What it does
Expand Down Expand Up @@ -450,13 +453,25 @@ fn check_empty_string(cx: &LateContext<'_>, format_args: &FormatArgs, macro_call
fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
let arg_index = |argument: &FormatArgPosition| argument.index.unwrap_or_else(|pos| pos);

let lint_name = if name.starts_with("write") {
WRITE_LITERAL
} else {
PRINT_LITERAL
};

let mut counts = vec![0u32; format_args.arguments.all_args().len()];
for piece in &format_args.template {
if let FormatArgsPiece::Placeholder(placeholder) = piece {
counts[arg_index(&placeholder.argument)] += 1;
}
}

let mut suggestion: Vec<(Span, String)> = vec![];
// holds index of replaced positional arguments; used to decrement the index of the remaining
// positional arguments.
let mut replaced_position: Vec<usize> = vec![];
let mut sug_span: Option<Span> = None;

for piece in &format_args.template {
if let FormatArgsPiece::Placeholder(FormatPlaceholder {
argument,
Expand Down Expand Up @@ -493,12 +508,6 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
_ => continue,
};

let lint = if name.starts_with("write") {
WRITE_LITERAL
} else {
PRINT_LITERAL
};

let Some(format_string_snippet) = snippet_opt(cx, format_args.span) else { continue };
let format_string_is_raw = format_string_snippet.starts_with('r');

Expand All @@ -519,29 +528,58 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgs, name: &str) {
},
};

span_lint_and_then(
cx,
lint,
arg.expr.span,
"literal with an empty format string",
|diag| {
if let Some(replacement) = replacement
// `format!("{}", "a")`, `format!("{named}", named = "b")
// ~~~~~ ~~~~~~~~~~~~~
&& let Some(removal_span) = format_arg_removal_span(format_args, index)
{
let replacement = replacement.replace('{', "{{").replace('}', "}}");
diag.multipart_suggestion(
"try",
vec![(*placeholder_span, replacement), (removal_span, String::new())],
Applicability::MachineApplicable,
);
}
},
);
sug_span = Some(sug_span.unwrap_or(arg.expr.span).to(arg.expr.span));

if let Some((_, index)) = positional_arg_piece_span(piece) {
replaced_position.push(index);
}

if let Some(replacement) = replacement
// `format!("{}", "a")`, `format!("{named}", named = "b")
// ~~~~~ ~~~~~~~~~~~~~
&& let Some(removal_span) = format_arg_removal_span(format_args, index) {
let replacement = replacement.replace('{', "{{").replace('}', "}}");
suggestion.push((*placeholder_span, replacement));
suggestion.push((removal_span, String::new()));
}
}
}

// 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))));
}
}
}

if let Some(span) = sug_span {
span_lint_and_then(cx, lint_name, span, "literal with an empty format string", |diag| {
if !suggestion.is_empty() {
diag.multipart_suggestion("try", suggestion, Applicability::MachineApplicable);
}
});
}
}

/// Extract Span and its index from the given `piece`, iff it's positional argument.
fn positional_arg_piece_span(piece: &FormatArgsPiece) -> Option<(Span, usize)> {
match piece {
FormatArgsPiece::Placeholder(FormatPlaceholder {
argument:
FormatArgPosition {
index: Ok(index),
kind: FormatArgPositionKind::Number,
..
},
span: Some(span),
..
}) => Some((*span, *index)),
_ => None,
}
}

/// Removes the raw marker, `#`s and quotes from a str, and returns if the literal is raw
Expand Down
4 changes: 0 additions & 4 deletions tests/ui/print_literal.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,14 @@ fn main() {
// throw a warning
println!("hello world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string
println!("world hello");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string

// named args shouldn't change anything either
println!("hello world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string
println!("world hello");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string

// The string literal from `file!()` has a callsite span that isn't marked as coming from an
// expansion
Expand Down
4 changes: 0 additions & 4 deletions tests/ui/print_literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,14 @@ fn main() {
// throw a warning
println!("{0} {1}", "hello", "world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string
println!("{1} {0}", "hello", "world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string

// named args shouldn't change anything either
println!("{foo} {bar}", foo = "hello", bar = "world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string
println!("{bar} {foo}", foo = "hello", bar = "world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string

// The string literal from `file!()` has a callsite span that isn't marked as coming from an
// expansion
Expand Down
72 changes: 12 additions & 60 deletions tests/ui/print_literal.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -52,97 +52,49 @@ error: literal with an empty format string
--> $DIR/print_literal.rs:40:25
|
LL | println!("{0} {1}", "hello", "world");
| ^^^^^^^
| ^^^^^^^^^^^^^^^^
|
help: try
|
LL - println!("{0} {1}", "hello", "world");
LL + println!("hello {1}", "world");
LL + println!("hello world");
|

error: literal with an empty format string
--> $DIR/print_literal.rs:40:34
|
LL | println!("{0} {1}", "hello", "world");
| ^^^^^^^
|
help: try
|
LL - println!("{0} {1}", "hello", "world");
LL + println!("{0} world", "hello");
|

error: literal with an empty format string
--> $DIR/print_literal.rs:43:34
--> $DIR/print_literal.rs:42:25
|
LL | println!("{1} {0}", "hello", "world");
| ^^^^^^^
| ^^^^^^^^^^^^^^^^
|
help: try
|
LL - println!("{1} {0}", "hello", "world");
LL + println!("world {0}", "hello");
|

error: literal with an empty format string
--> $DIR/print_literal.rs:43:25
|
LL | println!("{1} {0}", "hello", "world");
| ^^^^^^^
|
help: try
|
LL - println!("{1} {0}", "hello", "world");
LL + println!("{1} hello", "world");
|

error: literal with an empty format string
--> $DIR/print_literal.rs:48:35
|
LL | println!("{foo} {bar}", foo = "hello", bar = "world");
| ^^^^^^^
|
help: try
|
LL - println!("{foo} {bar}", foo = "hello", bar = "world");
LL + println!("hello {bar}", bar = "world");
LL + println!("world hello");
|

error: literal with an empty format string
--> $DIR/print_literal.rs:48:50
--> $DIR/print_literal.rs:46:35
|
LL | println!("{foo} {bar}", foo = "hello", bar = "world");
| ^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^^
|
help: try
|
LL - println!("{foo} {bar}", foo = "hello", bar = "world");
LL + println!("{foo} world", foo = "hello");
LL + println!("hello world");
|

error: literal with an empty format string
--> $DIR/print_literal.rs:51:50
|
LL | println!("{bar} {foo}", foo = "hello", bar = "world");
| ^^^^^^^
|
help: try
|
LL - println!("{bar} {foo}", foo = "hello", bar = "world");
LL + println!("world {foo}", foo = "hello");
|

error: literal with an empty format string
--> $DIR/print_literal.rs:51:35
--> $DIR/print_literal.rs:48:35
|
LL | println!("{bar} {foo}", foo = "hello", bar = "world");
| ^^^^^^^
| ^^^^^^^^^^^^^^^^^^^^^^
|
help: try
|
LL - println!("{bar} {foo}", foo = "hello", bar = "world");
LL + println!("{bar} hello", bar = "world");
LL + println!("world hello");
|

error: aborting due to 12 previous errors
error: aborting due to 8 previous errors

14 changes: 10 additions & 4 deletions tests/ui/write_literal.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,22 @@ fn main() {
// throw a warning
writeln!(v, "hello world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string
writeln!(v, "world hello");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string

// named args shouldn't change anything either
writeln!(v, "hello world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string
writeln!(v, "world hello");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string

// #10128
writeln!(v, "hello {0} world", 2);
//~^ ERROR: literal with an empty format string
writeln!(v, "world {0} hello", 2);
//~^ ERROR: literal with an empty format string
writeln!(v, "hello {0} {1}, {bar}", 2, 3, bar = 4);
//~^ ERROR: literal with an empty format string
writeln!(v, "hello {0} {1}, world {2}", 2, 3, 4);
//~^ ERROR: literal with an empty format string
}
14 changes: 10 additions & 4 deletions tests/ui/write_literal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,22 @@ fn main() {
// throw a warning
writeln!(v, "{0} {1}", "hello", "world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string
writeln!(v, "{1} {0}", "hello", "world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string

// named args shouldn't change anything either
writeln!(v, "{foo} {bar}", foo = "hello", bar = "world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string
writeln!(v, "{bar} {foo}", foo = "hello", bar = "world");
//~^ ERROR: literal with an empty format string
//~| ERROR: literal with an empty format string

// #10128
writeln!(v, "{0} {1} {2}", "hello", 2, "world");
//~^ ERROR: literal with an empty format string
writeln!(v, "{2} {1} {0}", "hello", 2, "world");
//~^ ERROR: literal with an empty format string
writeln!(v, "{0} {1} {2}, {bar}", "hello", 2, 3, bar = 4);
//~^ ERROR: literal with an empty format string
writeln!(v, "{0} {1} {2}, {3} {4}", "hello", 2, 3, "world", 4);
//~^ ERROR: literal with an empty format string
}
Loading