Skip to content

Commit 9226066

Browse files
committed
FormatArgsExpn: Find comma spans and ignore weird proc macro spans
1 parent 425e1ea commit 9226066

9 files changed

+262
-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`
@@ -765,12 +767,82 @@ pub struct FormatArgsExpn<'tcx> {
765767
/// Has an added newline due to `println!()`/`writeln!()`/etc. The last format string part will
766768
/// include this added newline.
767769
pub newline: bool,
768-
/// Values passed after the format string and implicit captures. `[1, z + 2, x]` for
770+
/// Spans of the commas between the format string and explicit values, excluding any trailing
771+
/// comma
772+
///
773+
/// ```ignore
774+
/// format!("..", 1, 2, 3,)
775+
/// // ^ ^ ^
776+
/// ```
777+
comma_spans: Vec<Span>,
778+
/// Explicit values passed after the format string, ignoring implicit captures. `[1, z + 2]` for
769779
/// `format!("{x} {} {y}", 1, z + 2)`.
770-
value_args: Vec<&'tcx Expr<'tcx>>,
780+
explicit_values: Vec<&'tcx Expr<'tcx>>,
771781
}
772782

773783
impl<'tcx> FormatArgsExpn<'tcx> {
784+
/// Gets the spans of the commas inbetween the format string and explicit args, not including
785+
/// any trailing comma
786+
///
787+
/// ```ignore
788+
/// format!("{} {}", a, b)
789+
/// // ^ ^
790+
/// ```
791+
///
792+
/// Ensures that the format string and values aren't coming from a proc macro that sets the
793+
/// output span to that of its input
794+
fn comma_spans(cx: &LateContext<'_>, explicit_values: &[&Expr<'_>], fmt_span: Span) -> Option<Vec<Span>> {
795+
// `format!("{} {} {c}", "one", "two", c = "three")`
796+
// ^^^^^ ^^^^^ ^^^^^^^
797+
let value_spans = explicit_values
798+
.iter()
799+
.map(|val| hygiene::walk_chain(val.span, fmt_span.ctxt()));
800+
801+
// `format!("{} {} {c}", "one", "two", c = "three")`
802+
// ^^ ^^ ^^^^^^
803+
let between_spans = once(fmt_span)
804+
.chain(value_spans)
805+
.tuple_windows()
806+
.map(|(start, end)| start.between(end));
807+
808+
let mut comma_spans = Vec::new();
809+
for between_span in between_spans {
810+
let mut offset = 0;
811+
let mut seen_comma = false;
812+
813+
for token in tokenize(&snippet_opt(cx, between_span)?) {
814+
match token.kind {
815+
TokenKind::LineComment { .. } | TokenKind::BlockComment { .. } | TokenKind::Whitespace => {},
816+
TokenKind::Comma if !seen_comma => {
817+
seen_comma = true;
818+
819+
let base = between_span.data();
820+
comma_spans.push(Span::new(
821+
base.lo + BytePos(offset),
822+
base.lo + BytePos(offset + 1),
823+
base.ctxt,
824+
base.parent,
825+
));
826+
},
827+
// named arguments, `start_val, name = end_val`
828+
// ^^^^^^^^^ between_span
829+
TokenKind::Ident | TokenKind::Eq if seen_comma => {},
830+
// An unexpected token usually indicates the format string or a value came from a proc macro output
831+
// that sets the span of its output to an input, e.g. `println!(some_proc_macro!("input"), ..)` that
832+
// emits a string literal with the span set to that of `"input"`
833+
_ => return None,
834+
}
835+
offset += token.len;
836+
}
837+
838+
if !seen_comma {
839+
return None;
840+
}
841+
}
842+
843+
Some(comma_spans)
844+
}
845+
774846
pub fn parse(cx: &LateContext<'_>, expr: &'tcx Expr<'tcx>) -> Option<Self> {
775847
let macro_name = macro_backtrace(expr.span)
776848
.map(|macro_call| cx.tcx.item_name(macro_call.def_id))
@@ -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 = Self::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
@@ -100,4 +100,10 @@ fn main() {
100100
let opt_ref = &opt;
101101
opt_ref.unwrap_or_else(|| panic!("{:?}", opt_ref));
102102
}
103+
104+
let format_capture: Option<i32> = None;
105+
format_capture.unwrap_or_else(|| panic!("{error_code}"));
106+
107+
let format_capture_and_value: Option<i32> = None;
108+
format_capture_and_value.unwrap_or_else(|| panic!("{error_code}, {}", 1));
103109
}

tests/ui/expect_fun_call.rs

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

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:105: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:108: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

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1+
// aux-build:proc_macro_with_span.rs
12
// run-rustfix
23
#![feature(custom_inner_attributes)]
34
#![warn(clippy::uninlined_format_args)]
45
#![allow(named_arguments_used_positionally, unused_imports, unused_macros, unused_variables)]
56
#![allow(clippy::eq_op, clippy::format_in_format_args, clippy::print_literal)]
67

8+
extern crate proc_macro_with_span;
9+
use proc_macro_with_span::with_span;
10+
711
macro_rules! no_param_str {
812
() => {
913
"{}"
1014
};
1115
}
1216

13-
macro_rules! pass_through {
14-
($expr:expr) => {
15-
$expr
16-
};
17-
}
18-
1917
macro_rules! my_println {
2018
($($args:tt),*) => {{
2119
println!($($args),*)
@@ -140,11 +138,13 @@ fn tester(fn_arg: i32) {
140138
);
141139
println!(no_param_str!(), local_i32);
142140

143-
// FIXME: bugs!
144-
// println!(pass_through!("foo={local_i32}"), local_i32 = local_i32);
145-
// println!(pass_through!("foo={}"), local_i32);
146-
// println!(indoc!("foo={}"), local_i32);
147-
// printdoc!("foo={}", local_i32);
141+
println!(
142+
"{val}",
143+
);
144+
println!("{val}");
145+
146+
println!(with_span!("{0} {1}" "{1} {0}"), local_i32, local_f64);
147+
println!("{}", with_span!(span val));
148148
}
149149

150150
fn main() {

tests/ui/uninlined_format_args.rs

+13-11
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1+
// aux-build:proc_macro_with_span.rs
12
// run-rustfix
23
#![feature(custom_inner_attributes)]
34
#![warn(clippy::uninlined_format_args)]
45
#![allow(named_arguments_used_positionally, unused_imports, unused_macros, unused_variables)]
56
#![allow(clippy::eq_op, clippy::format_in_format_args, clippy::print_literal)]
67

8+
extern crate proc_macro_with_span;
9+
use proc_macro_with_span::with_span;
10+
711
macro_rules! no_param_str {
812
() => {
913
"{}"
1014
};
1115
}
1216

13-
macro_rules! pass_through {
14-
($expr:expr) => {
15-
$expr
16-
};
17-
}
18-
1917
macro_rules! my_println {
2018
($($args:tt),*) => {{
2119
println!($($args),*)
@@ -143,11 +141,15 @@ fn tester(fn_arg: i32) {
143141
);
144142
println!(no_param_str!(), local_i32);
145143

146-
// FIXME: bugs!
147-
// println!(pass_through!("foo={local_i32}"), local_i32 = local_i32);
148-
// println!(pass_through!("foo={}"), local_i32);
149-
// println!(indoc!("foo={}"), local_i32);
150-
// printdoc!("foo={}", local_i32);
144+
println!(
145+
"{}",
146+
// comment with a comma , in it
147+
val,
148+
);
149+
println!("{}", /* comment with a comma , in it */ val);
150+
151+
println!(with_span!("{0} {1}" "{1} {0}"), local_i32, local_f64);
152+
println!("{}", with_span!(span val));
151153
}
152154

153155
fn main() {

0 commit comments

Comments
 (0)