Skip to content

Commit 4233a13

Browse files
committed
suggest a positional formatting argument instead of a captured argument
1 parent e4417cf commit 4233a13

File tree

5 files changed

+148
-15
lines changed

5 files changed

+148
-15
lines changed

compiler/rustc_builtin_macros/src/format.rs

+31-8
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@ struct Context<'a, 'b> {
250250
unused_names_lint: PositionalNamedArgsLint,
251251
}
252252

253+
pub struct FormatArg {
254+
expr: P<ast::Expr>,
255+
named: bool,
256+
}
257+
253258
/// Parses the arguments from the given list of tokens, returning the diagnostic
254259
/// if there's a parse error so we can continue parsing other format!
255260
/// expressions.
@@ -263,8 +268,8 @@ fn parse_args<'a>(
263268
ecx: &mut ExtCtxt<'a>,
264269
sp: Span,
265270
tts: TokenStream,
266-
) -> PResult<'a, (P<ast::Expr>, Vec<P<ast::Expr>>, FxHashMap<Symbol, (usize, Span)>)> {
267-
let mut args = Vec::<P<ast::Expr>>::new();
271+
) -> PResult<'a, (P<ast::Expr>, Vec<FormatArg>, FxHashMap<Symbol, (usize, Span)>)> {
272+
let mut args = Vec::<FormatArg>::new();
268273
let mut names = FxHashMap::<Symbol, (usize, Span)>::default();
269274

270275
let mut p = ecx.new_parser_from_tts(tts);
@@ -332,7 +337,7 @@ fn parse_args<'a>(
332337
let e = p.parse_expr()?;
333338
if let Some((prev, _)) = names.get(&ident.name) {
334339
ecx.struct_span_err(e.span, &format!("duplicate argument named `{}`", ident))
335-
.span_label(args[*prev].span, "previously here")
340+
.span_label(args[*prev].expr.span, "previously here")
336341
.span_label(e.span, "duplicate argument")
337342
.emit();
338343
continue;
@@ -344,7 +349,7 @@ fn parse_args<'a>(
344349
// args. And remember the names.
345350
let slot = args.len();
346351
names.insert(ident.name, (slot, ident.span));
347-
args.push(e);
352+
args.push(FormatArg { expr: e, named: true });
348353
}
349354
_ => {
350355
let e = p.parse_expr()?;
@@ -355,11 +360,11 @@ fn parse_args<'a>(
355360
);
356361
err.span_label(e.span, "positional arguments must be before named arguments");
357362
for pos in names.values() {
358-
err.span_label(args[pos.0].span, "named argument");
363+
err.span_label(args[pos.0].expr.span, "named argument");
359364
}
360365
err.emit();
361366
}
362-
args.push(e);
367+
args.push(FormatArg { expr: e, named: false });
363368
}
364369
}
365370
}
@@ -1180,7 +1185,7 @@ pub fn expand_preparsed_format_args(
11801185
ecx: &mut ExtCtxt<'_>,
11811186
sp: Span,
11821187
efmt: P<ast::Expr>,
1183-
args: Vec<P<ast::Expr>>,
1188+
args: Vec<FormatArg>,
11841189
names: FxHashMap<Symbol, (usize, Span)>,
11851190
append_newline: bool,
11861191
) -> P<ast::Expr> {
@@ -1270,6 +1275,24 @@ pub fn expand_preparsed_format_args(
12701275
e.span_label(fmt_span.from_inner(InnerSpan::new(span.start, span.end)), label);
12711276
}
12721277
}
1278+
if err.should_be_replaced_with_positional_argument {
1279+
let captured_arg_span =
1280+
fmt_span.from_inner(InnerSpan::new(err.span.start, err.span.end));
1281+
let positional_args = args.iter().filter(|arg| !arg.named).collect::<Vec<_>>();
1282+
let mut suggestions = vec![(captured_arg_span, positional_args.len().to_string())];
1283+
if let Ok(arg) = ecx.source_map().span_to_snippet(captured_arg_span) {
1284+
let span = match positional_args.last() {
1285+
Some(arg) => arg.expr.span,
1286+
None => fmt_sp,
1287+
};
1288+
suggestions.push((span.shrink_to_hi(), format!(", {}", arg)))
1289+
}
1290+
e.multipart_suggestion_verbose(
1291+
"consider using a positional formatting argument instead",
1292+
suggestions,
1293+
Applicability::MachineApplicable,
1294+
);
1295+
}
12731296
e.emit();
12741297
return DummyResult::raw_expr(sp, true);
12751298
}
@@ -1284,7 +1307,7 @@ pub fn expand_preparsed_format_args(
12841307

12851308
let mut cx = Context {
12861309
ecx,
1287-
args,
1310+
args: args.into_iter().map(|arg| arg.expr).collect(),
12881311
num_captured_args: 0,
12891312
arg_types,
12901313
arg_unique_types,

compiler/rustc_parse_format/src/lib.rs

+41-7
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ pub struct ParseError {
175175
pub label: string::String,
176176
pub span: InnerSpan,
177177
pub secondary_label: Option<(string::String, InnerSpan)>,
178+
pub should_be_replaced_with_positional_argument: bool,
178179
}
179180

180181
/// The parser structure for interpreting the input format string. This is
@@ -228,13 +229,19 @@ impl<'a> Iterator for Parser<'a> {
228229
Some(String(self.string(pos + 1)))
229230
} else {
230231
let arg = self.argument(lbrace_end);
231-
if let Some(rbrace_byte_idx) = self.must_consume('}') {
232-
let lbrace_inner_offset = self.to_span_index(pos);
233-
let rbrace_inner_offset = self.to_span_index(rbrace_byte_idx);
234-
if self.is_literal {
235-
self.arg_places.push(
236-
lbrace_inner_offset.to(InnerOffset(rbrace_inner_offset.0 + 1)),
237-
);
232+
match self.must_consume('}') {
233+
Some(rbrace_byte_idx) => {
234+
let lbrace_inner_offset = self.to_span_index(pos);
235+
let rbrace_inner_offset = self.to_span_index(rbrace_byte_idx);
236+
if self.is_literal {
237+
self.arg_places.push(
238+
lbrace_inner_offset
239+
.to(InnerOffset(rbrace_inner_offset.0 + 1)),
240+
);
241+
}
242+
}
243+
None => {
244+
self.suggest_positional_arg_instead_of_captured_arg(arg);
238245
}
239246
}
240247
Some(NextArgument(arg))
@@ -313,6 +320,7 @@ impl<'a> Parser<'a> {
313320
label: label.into(),
314321
span,
315322
secondary_label: None,
323+
should_be_replaced_with_positional_argument: false,
316324
});
317325
}
318326

@@ -336,6 +344,7 @@ impl<'a> Parser<'a> {
336344
label: label.into(),
337345
span,
338346
secondary_label: None,
347+
should_be_replaced_with_positional_argument: false,
339348
});
340349
}
341350

@@ -407,6 +416,7 @@ impl<'a> Parser<'a> {
407416
label,
408417
span: pos.to(pos),
409418
secondary_label,
419+
should_be_replaced_with_positional_argument: false,
410420
});
411421
None
412422
}
@@ -434,6 +444,7 @@ impl<'a> Parser<'a> {
434444
label,
435445
span: pos.to(pos),
436446
secondary_label,
447+
should_be_replaced_with_positional_argument: false,
437448
});
438449
} else {
439450
self.err(description, format!("expected `{:?}`", c), pos.to(pos));
@@ -750,6 +761,29 @@ impl<'a> Parser<'a> {
750761
}
751762
if found { Some(cur) } else { None }
752763
}
764+
765+
fn suggest_positional_arg_instead_of_captured_arg(&mut self, arg: Argument<'a>) {
766+
if let Some(end) = self.consume_pos('.') {
767+
let byte_pos = self.to_span_index(end);
768+
let start = InnerOffset(byte_pos.0 + 1);
769+
let field = self.argument(start);
770+
if let ArgumentNamed(_) = arg.position {
771+
if let ArgumentNamed(_) = field.position {
772+
self.errors.insert(
773+
0,
774+
ParseError {
775+
description: "field access isn't supported".to_string(),
776+
note: None,
777+
label: "not supported".to_string(),
778+
span: InnerSpan::new(arg.position_span.start, field.position_span.end),
779+
secondary_label: None,
780+
should_be_replaced_with_positional_argument: true,
781+
},
782+
);
783+
}
784+
}
785+
}
786+
}
753787
}
754788

755789
/// Finds the indices of all characters that have been processed and differ between the actual
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// run-rustfix
2+
3+
#[derive(Debug)]
4+
struct Foo {
5+
field: usize,
6+
}
7+
8+
fn main() {
9+
let foo = Foo { field: 0 };
10+
let bar = 3;
11+
format!("{0}", foo.field); //~ ERROR invalid format string: field access isn't supported
12+
format!("{1} {} {bar}", "aa", foo.field); //~ ERROR invalid format string: field access isn't supported
13+
format!("{2} {} {1} {bar}", "aa", "bb", foo.field); //~ ERROR invalid format string: field access isn't supported
14+
format!("{1} {} {baz}", "aa", foo.field, baz = 3); //~ ERROR invalid format string: field access isn't supported
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// run-rustfix
2+
3+
#[derive(Debug)]
4+
struct Foo {
5+
field: usize,
6+
}
7+
8+
fn main() {
9+
let foo = Foo { field: 0 };
10+
let bar = 3;
11+
format!("{foo.field}"); //~ ERROR invalid format string: field access isn't supported
12+
format!("{foo.field} {} {bar}", "aa"); //~ ERROR invalid format string: field access isn't supported
13+
format!("{foo.field} {} {1} {bar}", "aa", "bb"); //~ ERROR invalid format string: field access isn't supported
14+
format!("{foo.field} {} {baz}", "aa", baz = 3); //~ ERROR invalid format string: field access isn't supported
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
error: invalid format string: field access isn't supported
2+
--> $DIR/struct-field-as-captured-argument.rs:11:15
3+
|
4+
LL | format!("{foo.field}");
5+
| ^^^^^^^^^ not supported in format string
6+
|
7+
help: consider using a positional formatting argument instead
8+
|
9+
LL | format!("{0}", foo.field);
10+
| ~ +++++++++++
11+
12+
error: invalid format string: field access isn't supported
13+
--> $DIR/struct-field-as-captured-argument.rs:12:15
14+
|
15+
LL | format!("{foo.field} {} {bar}", "aa");
16+
| ^^^^^^^^^ not supported in format string
17+
|
18+
help: consider using a positional formatting argument instead
19+
|
20+
LL | format!("{1} {} {bar}", "aa", foo.field);
21+
| ~ +++++++++++
22+
23+
error: invalid format string: field access isn't supported
24+
--> $DIR/struct-field-as-captured-argument.rs:13:15
25+
|
26+
LL | format!("{foo.field} {} {1} {bar}", "aa", "bb");
27+
| ^^^^^^^^^ not supported in format string
28+
|
29+
help: consider using a positional formatting argument instead
30+
|
31+
LL | format!("{2} {} {1} {bar}", "aa", "bb", foo.field);
32+
| ~ +++++++++++
33+
34+
error: invalid format string: field access isn't supported
35+
--> $DIR/struct-field-as-captured-argument.rs:14:15
36+
|
37+
LL | format!("{foo.field} {} {baz}", "aa", baz = 3);
38+
| ^^^^^^^^^ not supported in format string
39+
|
40+
help: consider using a positional formatting argument instead
41+
|
42+
LL | format!("{1} {} {baz}", "aa", foo.field, baz = 3);
43+
| ~ +++++++++++
44+
45+
error: aborting due to 4 previous errors
46+

0 commit comments

Comments
 (0)