Skip to content

Commit 465e297

Browse files
committed
FormatArgsExpn: Find comma spans and ignore weird proc macro spans
1 parent 5825ae7 commit 465e297

9 files changed

+276
-110
lines changed

clippy_lints/src/format_args.rs

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
22
use clippy_utils::macros::FormatParamKind::{Implicit, Named, Numbered, Starred};
33
use clippy_utils::macros::{is_format_macro, FormatArgsExpn, FormatParam, FormatParamUsage};
4-
use clippy_utils::source::{expand_past_previous_comma, snippet_opt};
4+
use clippy_utils::source::snippet_opt;
55
use clippy_utils::ty::implements_trait;
66
use clippy_utils::{is_diag_trait_item, meets_msrv, msrvs};
77
use if_chain::if_chain;
88
use itertools::Itertools;
99
use rustc_errors::Applicability;
10-
use rustc_hir::{Expr, ExprKind, HirId, Path, QPath};
10+
use rustc_hir::{Expr, ExprKind, HirId, QPath};
1111
use rustc_lint::{LateContext, LateLintPass};
1212
use rustc_middle::ty::adjustment::{Adjust, Adjustment};
1313
use rustc_middle::ty::Ty;
@@ -169,7 +169,7 @@ fn check_uninlined_args(cx: &LateContext<'_>, args: &FormatArgsExpn<'_>, call_si
169169
// we cannot remove any other arguments in the format string,
170170
// because the index numbers might be wrong after inlining.
171171
// Example of an un-inlinable format: print!("{}{1}", foo, 2)
172-
if !args.params().all(|p| check_one_arg(cx, &p, &mut fixes)) || fixes.is_empty() {
172+
if !args.params().all(|p| check_one_arg(args, &p, &mut fixes)) || fixes.is_empty() {
173173
return;
174174
}
175175

@@ -196,19 +196,18 @@ fn check_uninlined_args(cx: &LateContext<'_>, args: &FormatArgsExpn<'_>, call_si
196196
);
197197
}
198198

199-
fn check_one_arg(cx: &LateContext<'_>, param: &FormatParam<'_>, fixes: &mut Vec<(Span, String)>) -> bool {
199+
fn check_one_arg(args: &FormatArgsExpn<'_>, param: &FormatParam<'_>, fixes: &mut Vec<(Span, String)>) -> bool {
200200
if matches!(param.kind, Implicit | Starred | Named(_) | Numbered)
201201
&& let ExprKind::Path(QPath::Resolved(None, path)) = param.value.kind
202-
&& let Path { span, segments, .. } = path
203-
&& let [segment] = segments
202+
&& let [segment] = path.segments
203+
&& let Some(arg_span) = args.value_with_prev_comma_span(param.value.hir_id)
204204
{
205205
let replacement = match param.usage {
206206
FormatParamUsage::Argument => segment.ident.name.to_string(),
207207
FormatParamUsage::Width => format!("{}$", segment.ident.name),
208208
FormatParamUsage::Precision => format!(".{}$", segment.ident.name),
209209
};
210210
fixes.push((param.span, replacement));
211-
let arg_span = expand_past_previous_comma(cx, *span);
212211
fixes.push((arg_span, String::new()));
213212
true // successful inlining, continue checking
214213
} else {

clippy_lints/src/write.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,11 @@ fn check_literal(cx: &LateContext<'_>, format_args: &FormatArgsExpn<'_>, name: &
475475
value.span,
476476
"literal with an empty format string",
477477
|diag| {
478-
if let Some(replacement) = replacement {
478+
if let Some(replacement) = replacement
479479
// `format!("{}", "a")`, `format!("{named}", named = "b")
480480
// ~~~~~ ~~~~~~~~~~~~~
481-
let value_span = expand_past_previous_comma(cx, value.span);
482-
481+
&& let Some(value_span) = format_args.value_with_prev_comma_span(value.hir_id)
482+
{
483483
let replacement = replacement.replace('{', "{{").replace('}', "}}");
484484
diag.multipart_suggestion(
485485
"try this",

clippy_utils/src/macros.rs

+104-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use rustc_parse_format::{self as rpf, Alignment};
1616
use rustc_span::def_id::DefId;
1717
use rustc_span::hygiene::{self, MacroKind, SyntaxContext};
1818
use rustc_span::{sym, BytePos, ExpnData, ExpnId, ExpnKind, Pos, Span, SpanData, Symbol};
19+
use std::iter::{once, zip};
1920
use std::ops::ControlFlow;
2021

2122
const FORMAT_MACRO_DIAG_ITEMS: &[Symbol] = &[
@@ -412,7 +413,8 @@ impl FormatString {
412413
}
413414

414415
struct FormatArgsValues<'tcx> {
415-
/// See `FormatArgsExpn::value_args`
416+
/// Values passed after the format string and implicit captures. `[1, z + 2, x]` for
417+
/// `format!("{x} {} {y}", 1, z + 2)`.
416418
value_args: Vec<&'tcx Expr<'tcx>>,
417419
/// Maps an `rt::v1::Argument::position` or an `rt::v1::Count::Param` to its index in
418420
/// `value_args`
@@ -675,6 +677,68 @@ impl<'tcx> Count<'tcx> {
675677
}
676678
}
677679

680+
/// Gets the spans of the commas inbetween the format string and explicit args, not including any
681+
/// trailing comma
682+
///
683+
/// ```ignore
684+
/// format!("{} {}", a, b)
685+
/// // ^ ^
686+
/// ```
687+
///
688+
/// Ensures that the format string and values aren't coming from a proc macro that sets the output
689+
/// span to that of its input
690+
fn comma_spans(cx: &LateContext<'_>, explicit_values: &[&Expr<'_>], fmt_span: Span) -> Option<Vec<Span>> {
691+
// `format!("{} {} {c}", "one", "two", c = "three")`
692+
// ^^^^^ ^^^^^ ^^^^^^^
693+
let value_spans = explicit_values
694+
.iter()
695+
.map(|val| hygiene::walk_chain(val.span, fmt_span.ctxt()));
696+
697+
// `format!("{} {} {c}", "one", "two", c = "three")`
698+
// ^^ ^^ ^^^^^^
699+
let between_spans = once(fmt_span)
700+
.chain(value_spans)
701+
.tuple_windows()
702+
.map(|(start, end)| start.between(end));
703+
704+
let mut comma_spans = Vec::new();
705+
for between_span in between_spans {
706+
let mut offset = 0;
707+
let mut seen_comma = false;
708+
709+
for token in tokenize(&snippet_opt(cx, between_span)?) {
710+
match token.kind {
711+
TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } | TokenKind::Whitespace => {},
712+
TokenKind::Comma if !seen_comma => {
713+
seen_comma = true;
714+
715+
let base = between_span.data();
716+
comma_spans.push(Span::new(
717+
base.lo + BytePos(offset),
718+
base.lo + BytePos(offset + 1),
719+
base.ctxt,
720+
base.parent,
721+
));
722+
},
723+
// named arguments, `start_val, name = end_val`
724+
// ^^^^^^^^^ between_span
725+
TokenKind::Ident | TokenKind::Eq if seen_comma => {},
726+
// An unexpected token usually indicates the format string or a value came from a proc macro output that
727+
// sets the span of its output to an input, e.g. `println!(some_proc_macro!("input"), ..)` that emits a
728+
// string literal with the span set to that of `"input"`
729+
_ => return None,
730+
}
731+
offset += token.len;
732+
}
733+
734+
if !seen_comma {
735+
return None;
736+
}
737+
}
738+
739+
Some(comma_spans)
740+
}
741+
678742
/// Specification for the formatting of an argument in the format string. See
679743
/// <https://doc.rust-lang.org/std/fmt/index.html#formatting-parameters> for the precise meanings.
680744
#[derive(Debug)]
@@ -765,9 +829,17 @@ pub struct FormatArgsExpn<'tcx> {
765829
/// Has an added newline due to `println!()`/`writeln!()`/etc. The last format string part will
766830
/// include this added newline.
767831
pub newline: bool,
768-
/// Values passed after the format string and implicit captures. `[1, z + 2, x]` for
832+
/// Spans of the commas between the format string and explicit values, excluding any trailing
833+
/// comma
834+
///
835+
/// ```ignore
836+
/// format!("..", 1, 2, 3,)
837+
/// // ^ ^ ^
838+
/// ```
839+
comma_spans: Vec<Span>,
840+
/// Explicit values passed after the format string, ignoring implicit captures. `[1, z + 2]` for
769841
/// `format!("{x} {} {y}", 1, z + 2)`.
770-
value_args: Vec<&'tcx Expr<'tcx>>,
842+
explicit_values: Vec<&'tcx Expr<'tcx>>,
771843
}
772844

773845
impl<'tcx> FormatArgsExpn<'tcx> {
@@ -845,11 +917,22 @@ impl<'tcx> FormatArgsExpn<'tcx> {
845917
})
846918
.collect::<Option<Vec<_>>>()?;
847919

920+
let mut explicit_values = values.value_args;
921+
// remove values generated for implicitly captured vars
922+
let len = explicit_values
923+
.iter()
924+
.take_while(|val| !format_string.span.contains(val.span))
925+
.count();
926+
explicit_values.truncate(len);
927+
928+
let comma_spans = comma_spans(cx, &explicit_values, format_string.span)?;
929+
848930
Some(Self {
849931
format_string,
850932
args,
851-
value_args: values.value_args,
852933
newline,
934+
comma_spans,
935+
explicit_values,
853936
})
854937
} else {
855938
None
@@ -875,7 +958,7 @@ impl<'tcx> FormatArgsExpn<'tcx> {
875958

876959
/// Source callsite span of all inputs
877960
pub fn inputs_span(&self) -> Span {
878-
match *self.value_args {
961+
match *self.explicit_values {
879962
[] => self.format_string.span,
880963
[.., last] => self
881964
.format_string
@@ -884,6 +967,22 @@ impl<'tcx> FormatArgsExpn<'tcx> {
884967
}
885968
}
886969

970+
/// Get the span of a value expanded to the previous comma, e.g. for the value `10`
971+
///
972+
/// ```ignore
973+
/// format("{}.{}", 10, 11)
974+
/// // ^^^^
975+
/// ```
976+
pub fn value_with_prev_comma_span(&self, value_id: HirId) -> Option<Span> {
977+
for (comma_span, value) in zip(&self.comma_spans, &self.explicit_values) {
978+
if value.hir_id == value_id {
979+
return Some(comma_span.to(hygiene::walk_chain(value.span, comma_span.ctxt())));
980+
}
981+
}
982+
983+
None
984+
}
985+
887986
/// Iterator of all format params, both values and those referenced by `width`/`precision`s.
888987
pub fn params(&'tcx self) -> impl Iterator<Item = FormatParam<'tcx>> {
889988
self.args

tests/ui/expect_fun_call.fixed

+6
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,10 @@ fn main() {
101101
let opt_ref = &opt;
102102
opt_ref.unwrap_or_else(|| panic!("{:?}", opt_ref));
103103
}
104+
105+
let format_capture: Option<i32> = None;
106+
format_capture.unwrap_or_else(|| panic!("{error_code}"));
107+
108+
let format_capture_and_value: Option<i32> = None;
109+
format_capture_and_value.unwrap_or_else(|| panic!("{error_code}, {}", 1));
104110
}

tests/ui/expect_fun_call.rs

+6
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,10 @@ fn main() {
101101
let opt_ref = &opt;
102102
opt_ref.expect(&format!("{:?}", opt_ref));
103103
}
104+
105+
let format_capture: Option<i32> = None;
106+
format_capture.expect(&format!("{error_code}"));
107+
108+
let format_capture_and_value: Option<i32> = None;
109+
format_capture_and_value.expect(&format!("{error_code}, {}", 1));
104110
}

tests/ui/expect_fun_call.stderr

+13-1
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,17 @@ error: use of `expect` followed by a function call
7878
LL | opt_ref.expect(&format!("{:?}", opt_ref));
7979
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `unwrap_or_else(|| panic!("{:?}", opt_ref))`
8080

81-
error: aborting due to 13 previous errors
81+
error: use of `expect` followed by a function call
82+
--> $DIR/expect_fun_call.rs:106:20
83+
|
84+
LL | format_capture.expect(&format!("{error_code}"));
85+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `unwrap_or_else(|| panic!("{error_code}"))`
86+
87+
error: use of `expect` followed by a function call
88+
--> $DIR/expect_fun_call.rs:109:30
89+
|
90+
LL | format_capture_and_value.expect(&format!("{error_code}, {}", 1));
91+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `unwrap_or_else(|| panic!("{error_code}, {}", 1))`
92+
93+
error: aborting due to 15 previous errors
8294

tests/ui/uninlined_format_args.fixed

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// aux-build:proc_macro_with_span.rs
12
// run-rustfix
23

34
#![allow(clippy::eq_op)]
@@ -8,18 +9,15 @@
89
#![warn(clippy::uninlined_format_args)]
910
#![feature(custom_inner_attributes)]
1011

12+
extern crate proc_macro_with_span;
13+
use proc_macro_with_span::with_span;
14+
1115
macro_rules! no_param_str {
1216
() => {
1317
"{}"
1418
};
1519
}
1620

17-
macro_rules! pass_through {
18-
($expr:expr) => {
19-
$expr
20-
};
21-
}
22-
2321
macro_rules! my_println {
2422
($($args:tt),*) => {{
2523
println!($($args),*)
@@ -144,11 +142,14 @@ fn tester(fn_arg: i32) {
144142
);
145143
println!(no_param_str!(), local_i32);
146144

147-
// FIXME: bugs!
148-
// println!(pass_through!("foo={local_i32}"), local_i32 = local_i32);
149-
// println!(pass_through!("foo={}"), local_i32);
150-
// println!(indoc!("foo={}"), local_i32);
151-
// printdoc!("foo={}", local_i32);
145+
println!(
146+
"{val}",
147+
);
148+
println!("{val}");
149+
println!("{val}");
150+
151+
println!(with_span!("{0} {1}" "{1} {0}"), local_i32, local_f64);
152+
println!("{}", with_span!(span val));
152153
}
153154

154155
fn main() {

tests/ui/uninlined_format_args.rs

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// aux-build:proc_macro_with_span.rs
12
// run-rustfix
23

34
#![allow(clippy::eq_op)]
@@ -8,18 +9,15 @@
89
#![warn(clippy::uninlined_format_args)]
910
#![feature(custom_inner_attributes)]
1011

12+
extern crate proc_macro_with_span;
13+
use proc_macro_with_span::with_span;
14+
1115
macro_rules! no_param_str {
1216
() => {
1317
"{}"
1418
};
1519
}
1620

17-
macro_rules! pass_through {
18-
($expr:expr) => {
19-
$expr
20-
};
21-
}
22-
2321
macro_rules! my_println {
2422
($($args:tt),*) => {{
2523
println!($($args),*)
@@ -147,11 +145,16 @@ fn tester(fn_arg: i32) {
147145
);
148146
println!(no_param_str!(), local_i32);
149147

150-
// FIXME: bugs!
151-
// println!(pass_through!("foo={local_i32}"), local_i32 = local_i32);
152-
// println!(pass_through!("foo={}"), local_i32);
153-
// println!(indoc!("foo={}"), local_i32);
154-
// printdoc!("foo={}", local_i32);
148+
println!(
149+
"{}",
150+
// comment with a comma , in it
151+
val,
152+
);
153+
println!("{}", /* comment with a comma , in it */ val);
154+
println!("{named}", named = val);
155+
156+
println!(with_span!("{0} {1}" "{1} {0}"), local_i32, local_f64);
157+
println!("{}", with_span!(span val));
155158
}
156159

157160
fn main() {

0 commit comments

Comments
 (0)