Skip to content

Commit 159dcb2

Browse files
committed
On format!() arg count mismatch provide extra info
When positional width and precision formatting flags are present in a formatting string that has an argument count mismatch, provide extra information pointing at them making it easiser to understand where the problem may lay: ``` error: 4 positional arguments in format string, but there are 3 arguments --> $DIR/ifmt-bad-arg.rs:78:15 | LL | println!("{} {:.*} {}", 1, 3.2, 4); | ^^ ^^--^ ^^ --- this parameter corresponds to the precision flag | | | this precision flag adds an extra required argument at position 1, which is why there are 4 arguments expected | = note: positional arguments are zero-based = note: for information about formatting flags, visit https://doc.rust-lang.org/std/fmt/index.html error: 4 positional arguments in format string, but there are 3 arguments --> $DIR/ifmt-bad-arg.rs:81:15 | LL | println!("{} {:07$.*} {}", 1, 3.2, 4); | ^^ ^^-----^ ^^ --- this parameter corresponds to the precision flag | | | | | this precision flag adds an extra required argument at position 1, which is why there are 4 arguments expected | this width flag expects an `usize` argument at position 7, but there are 3 arguments | = note: positional arguments are zero-based = note: for information about formatting flags, visit https://doc.rust-lang.org/std/fmt/index.html error: 3 positional arguments in format string, but there are 3 arguments --> $DIR/ifmt-bad-arg.rs:84:15 | LL | println!("{} {:07$} {}", 1, 3.2, 4); | ^^ ^^---^ ^^ | | | this width flag expects an `usize` argument at position 7, but there are 3 arguments | = note: positional arguments are zero-based = note: for information about formatting flags, visit https://doc.rust-lang.org/std/fmt/index.html ```
1 parent c798dff commit 159dcb2

File tree

4 files changed

+206
-40
lines changed

4 files changed

+206
-40
lines changed

src/libfmt_macros/lib.rs

+52-24
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,20 @@ pub struct Argument<'a> {
5959
/// Specification for the formatting of an argument in the format string.
6060
#[derive(Copy, Clone, PartialEq)]
6161
pub struct FormatSpec<'a> {
62-
/// Optionally specified character to fill alignment with
62+
/// Optionally specified character to fill alignment with.
6363
pub fill: Option<char>,
64-
/// Optionally specified alignment
64+
/// Optionally specified alignment.
6565
pub align: Alignment,
66-
/// Packed version of various flags provided
66+
/// Packed version of various flags provided.
6767
pub flags: u32,
68-
/// The integer precision to use
68+
/// The integer precision to use.
6969
pub precision: Count,
70-
/// The string width requested for the resulting format
70+
/// The span of the precision formatting flag (for diagnostics).
71+
pub precision_span: Option<InnerSpan>,
72+
/// The string width requested for the resulting format.
7173
pub width: Count,
74+
/// The span of the width formatting flag (for diagnostics).
75+
pub width_span: Option<InnerSpan>,
7276
/// The descriptor string representing the name of the format desired for
7377
/// this argument, this can be empty or any number of characters, although
7478
/// it is required to be one word.
@@ -285,19 +289,24 @@ impl<'a> Parser<'a> {
285289
}
286290

287291
/// Optionally consumes the specified character. If the character is not at
288-
/// the current position, then the current iterator isn't moved and false is
289-
/// returned, otherwise the character is consumed and true is returned.
292+
/// the current position, then the current iterator isn't moved and `false` is
293+
/// returned, otherwise the character is consumed and `true` is returned.
290294
fn consume(&mut self, c: char) -> bool {
291-
if let Some(&(_, maybe)) = self.cur.peek() {
295+
self.consume_pos(c).is_some()
296+
}
297+
298+
/// Optionally consumes the specified character. If the character is not at
299+
/// the current position, then the current iterator isn't moved and `None` is
300+
/// returned, otherwise the character is consumed and the current position is
301+
/// returned.
302+
fn consume_pos(&mut self, c: char) -> Option<usize> {
303+
if let Some(&(pos, maybe)) = self.cur.peek() {
292304
if c == maybe {
293305
self.cur.next();
294-
true
295-
} else {
296-
false
306+
return Some(pos);
297307
}
298-
} else {
299-
false
300308
}
309+
None
301310
}
302311

303312
fn to_span_index(&self, pos: usize) -> InnerOffset {
@@ -465,7 +474,9 @@ impl<'a> Parser<'a> {
465474
align: AlignUnknown,
466475
flags: 0,
467476
precision: CountImplied,
477+
precision_span: None,
468478
width: CountImplied,
479+
width_span: None,
469480
ty: &self.input[..0],
470481
};
471482
if !self.consume(':') {
@@ -502,6 +513,11 @@ impl<'a> Parser<'a> {
502513
}
503514
// Width and precision
504515
let mut havewidth = false;
516+
517+
let mut width_span_start = 0;
518+
if let Some((pos, '0')) = self.cur.peek() {
519+
width_span_start = *pos;
520+
}
505521
if self.consume('0') {
506522
// small ambiguity with '0$' as a format string. In theory this is a
507523
// '0' flag and then an ill-formatted format string with just a '$'
@@ -515,17 +531,28 @@ impl<'a> Parser<'a> {
515531
}
516532
}
517533
if !havewidth {
518-
spec.width = self.count();
534+
if width_span_start == 0 {
535+
if let Some((pos, _)) = self.cur.peek() {
536+
width_span_start = *pos;
537+
}
538+
}
539+
let (w, sp) = self.count(width_span_start);
540+
spec.width = w;
541+
spec.width_span = sp;
519542
}
520-
if self.consume('.') {
521-
if self.consume('*') {
543+
if let Some(start) = self.consume_pos('.') {
544+
if let Some(end) = self.consume_pos('*') {
522545
// Resolve `CountIsNextParam`.
523546
// We can do this immediately as `position` is resolved later.
524547
let i = self.curarg;
525548
self.curarg += 1;
526549
spec.precision = CountIsParam(i);
550+
spec.precision_span =
551+
Some(self.to_span_index(start).to(self.to_span_index(end + 1)));
527552
} else {
528-
spec.precision = self.count();
553+
let (p, sp) = self.count(start);
554+
spec.precision = p;
555+
spec.precision_span = sp;
529556
}
530557
}
531558
// Optional radix followed by the actual format specifier
@@ -554,24 +581,25 @@ impl<'a> Parser<'a> {
554581
/// Parses a Count parameter at the current position. This does not check
555582
/// for 'CountIsNextParam' because that is only used in precision, not
556583
/// width.
557-
fn count(&mut self) -> Count {
584+
fn count(&mut self, start: usize) -> (Count, Option<InnerSpan>) {
558585
if let Some(i) = self.integer() {
559-
if self.consume('$') {
560-
CountIsParam(i)
586+
if let Some(end) = self.consume_pos('$') {
587+
let span = self.to_span_index(start).to(self.to_span_index(end + 1));
588+
(CountIsParam(i), Some(span))
561589
} else {
562-
CountIs(i)
590+
(CountIs(i), None)
563591
}
564592
} else {
565593
let tmp = self.cur.clone();
566594
let word = self.word();
567595
if word.is_empty() {
568596
self.cur = tmp;
569-
CountImplied
597+
(CountImplied, None)
570598
} else if self.consume('$') {
571-
CountIsName(Symbol::intern(word))
599+
(CountIsName(Symbol::intern(word)), None)
572600
} else {
573601
self.cur = tmp;
574-
CountImplied
602+
(CountImplied, None)
575603
}
576604
}
577605
}

src/libsyntax_ext/format.rs

+92-15
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ struct Context<'a, 'b> {
109109
invalid_refs: Vec<(usize, usize)>,
110110
/// Spans of all the formatting arguments, in order.
111111
arg_spans: Vec<Span>,
112+
/// All the formatting arguments that have formatting flags set, in order for diagnostics.
113+
arg_with_formatting: Vec<parse::FormatSpec<'a>>,
112114
/// Whether this formatting string is a literal or it comes from a macro.
113115
is_literal: bool,
114116
}
@@ -279,14 +281,20 @@ impl<'a, 'b> Context<'a, 'b> {
279281
.iter()
280282
.map(|(r, pos)| (r.to_string(), self.arg_spans.get(*pos)));
281283

284+
let mut zero_based_note = false;
285+
282286
if self.names.is_empty() && !numbered_position_args {
287+
let count = self.pieces.len() + self.arg_with_formatting
288+
.iter()
289+
.filter(|fmt| fmt.precision_span.is_some())
290+
.count();
283291
e = self.ecx.mut_span_err(
284292
sp,
285293
&format!(
286294
"{} positional argument{} in format string, but {}",
287-
self.pieces.len(),
288-
if self.pieces.len() > 1 { "s" } else { "" },
289-
self.describe_num_args()
295+
count,
296+
if count > 1 { "s" } else { "" },
297+
self.describe_num_args(),
290298
),
291299
);
292300
} else {
@@ -317,9 +325,70 @@ impl<'a, 'b> Context<'a, 'b> {
317325
&format!("invalid reference to positional {} ({})",
318326
arg_list,
319327
self.describe_num_args()));
320-
e.note("positional arguments are zero-based");
328+
zero_based_note = true;
321329
};
322330

331+
for fmt in &self.arg_with_formatting {
332+
if let Some(span) = fmt.precision_span {
333+
let span = self.fmtsp.from_inner(span);
334+
match fmt.precision {
335+
parse::CountIsParam(pos) if pos > self.args.len() => {
336+
e.span_label(span, &format!(
337+
"this precision flag expects an `usize` argument at position {}, \
338+
but {}",
339+
pos,
340+
self.describe_num_args(),
341+
));
342+
zero_based_note = true;
343+
}
344+
parse::CountIsParam(pos) => {
345+
let count = self.pieces.len() + self.arg_with_formatting
346+
.iter()
347+
.filter(|fmt| fmt.precision_span.is_some())
348+
.count();
349+
e.span_label(span, &format!(
350+
"this precision flag adds an extra required argument at position {}, \
351+
which is why there {} expected",
352+
pos,
353+
if count == 1 {
354+
"is 1 argument".to_string()
355+
} else {
356+
format!("are {} arguments", count)
357+
},
358+
));
359+
e.span_label(
360+
self.args[pos].span,
361+
"this parameter corresponds to the precision flag",
362+
);
363+
zero_based_note = true;
364+
}
365+
_ => {}
366+
}
367+
}
368+
if let Some(span) = fmt.width_span {
369+
let span = self.fmtsp.from_inner(span);
370+
match fmt.width {
371+
parse::CountIsParam(pos) if pos > self.args.len() => {
372+
e.span_label(span, &format!(
373+
"this width flag expects an `usize` argument at position {}, \
374+
but {}",
375+
pos,
376+
self.describe_num_args(),
377+
));
378+
zero_based_note = true;
379+
}
380+
_ => {}
381+
}
382+
}
383+
}
384+
if zero_based_note {
385+
e.note("positional arguments are zero-based");
386+
}
387+
if !self.arg_with_formatting.is_empty() {
388+
e.note("for information about formatting flags, visit \
389+
https://doc.rust-lang.org/std/fmt/index.html");
390+
}
391+
323392
e.emit();
324393
}
325394

@@ -435,10 +504,11 @@ impl<'a, 'b> Context<'a, 'b> {
435504

436505
/// Builds a static `rt::Argument` from a `parse::Piece` or append
437506
/// to the `literal` string.
438-
fn build_piece(&mut self,
439-
piece: &parse::Piece<'_>,
440-
arg_index_consumed: &mut Vec<usize>)
441-
-> Option<P<ast::Expr>> {
507+
fn build_piece(
508+
&mut self,
509+
piece: &parse::Piece<'a>,
510+
arg_index_consumed: &mut Vec<usize>,
511+
) -> Option<P<ast::Expr>> {
442512
let sp = self.macsp;
443513
match *piece {
444514
parse::String(s) => {
@@ -496,7 +566,9 @@ impl<'a, 'b> Context<'a, 'b> {
496566
align: parse::AlignUnknown,
497567
flags: 0,
498568
precision: parse::CountImplied,
569+
precision_span: None,
499570
width: parse::CountImplied,
571+
width_span: None,
500572
ty: arg.format.ty,
501573
},
502574
};
@@ -506,6 +578,9 @@ impl<'a, 'b> Context<'a, 'b> {
506578
let pos_simple =
507579
arg.position.index() == simple_arg.position.index();
508580

581+
if arg.format.precision_span.is_some() || arg.format.width_span.is_some() {
582+
self.arg_with_formatting.push(arg.format); //'liself.fmtsp.from_inner(span));
583+
}
509584
if !pos_simple || arg.format != simple_arg.format || fill != ' ' {
510585
self.all_pieces_simple = false;
511586
}
@@ -530,7 +605,7 @@ impl<'a, 'b> Context<'a, 'b> {
530605
let path = self.ecx.path_global(sp, Context::rtpath(self.ecx, "FormatSpec"));
531606
let fmt = self.ecx.expr_struct(
532607
sp,
533-
path,
608+
path,
534609
vec![
535610
self.ecx.field_imm(sp, self.ecx.ident_of("fill"), fill),
536611
self.ecx.field_imm(sp, self.ecx.ident_of("align"), align),
@@ -657,12 +732,13 @@ impl<'a, 'b> Context<'a, 'b> {
657732
self.ecx.expr_call_global(self.macsp, path, fn_args)
658733
}
659734

660-
fn format_arg(ecx: &ExtCtxt<'_>,
661-
macsp: Span,
662-
mut sp: Span,
663-
ty: &ArgumentType,
664-
arg: ast::Ident)
665-
-> P<ast::Expr> {
735+
fn format_arg(
736+
ecx: &ExtCtxt<'_>,
737+
macsp: Span,
738+
mut sp: Span,
739+
ty: &ArgumentType,
740+
arg: ast::Ident,
741+
) -> P<ast::Expr> {
666742
sp = sp.apply_mark(ecx.current_expansion.id);
667743
let arg = ecx.expr_ident(sp, arg);
668744
let trait_ = match *ty {
@@ -941,6 +1017,7 @@ pub fn expand_preparsed_format_args(
9411017
fmtsp: fmt.span,
9421018
invalid_refs: Vec::new(),
9431019
arg_spans,
1020+
arg_with_formatting: Vec::new(),
9441021
is_literal,
9451022
};
9461023

src/test/ui/if/ifmt-bad-arg.rs

+8
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,12 @@ ninth number: {
7575
tenth number: {}",
7676
1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
7777
//~^^ ERROR: invalid format string
78+
println!("{} {:.*} {}", 1, 3.2, 4);
79+
//~^ ERROR 4 positional arguments in format string, but there are 3 arguments
80+
//~| ERROR mismatched types
81+
println!("{} {:07$.*} {}", 1, 3.2, 4);
82+
//~^ ERROR 4 positional arguments in format string, but there are 3 arguments
83+
//~| ERROR mismatched types
84+
println!("{} {:07$} {}", 1, 3.2, 4);
85+
//~^ ERROR 3 positional arguments in format string, but there are 3 arguments
7886
}

src/test/ui/if/ifmt-bad-arg.stderr

+54-1
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,58 @@ LL | tenth number: {}",
220220
|
221221
= note: if you intended to print `{`, you can escape it using `{{`
222222

223-
error: aborting due to 28 previous errors
223+
error: 4 positional arguments in format string, but there are 3 arguments
224+
--> $DIR/ifmt-bad-arg.rs:78:15
225+
|
226+
LL | println!("{} {:.*} {}", 1, 3.2, 4);
227+
| ^^ ^^--^ ^^ --- this parameter corresponds to the precision flag
228+
| |
229+
| this precision flag adds an extra required argument at position 1, which is why there are 4 arguments expected
230+
|
231+
= note: positional arguments are zero-based
232+
= note: for information about formatting flags, visit https://doc.rust-lang.org/std/fmt/index.html
233+
234+
error: 4 positional arguments in format string, but there are 3 arguments
235+
--> $DIR/ifmt-bad-arg.rs:81:15
236+
|
237+
LL | println!("{} {:07$.*} {}", 1, 3.2, 4);
238+
| ^^ ^^-----^ ^^ --- this parameter corresponds to the precision flag
239+
| | |
240+
| | this precision flag adds an extra required argument at position 1, which is why there are 4 arguments expected
241+
| this width flag expects an `usize` argument at position 7, but there are 3 arguments
242+
|
243+
= note: positional arguments are zero-based
244+
= note: for information about formatting flags, visit https://doc.rust-lang.org/std/fmt/index.html
245+
246+
error: 3 positional arguments in format string, but there are 3 arguments
247+
--> $DIR/ifmt-bad-arg.rs:84:15
248+
|
249+
LL | println!("{} {:07$} {}", 1, 3.2, 4);
250+
| ^^ ^^---^ ^^
251+
| |
252+
| this width flag expects an `usize` argument at position 7, but there are 3 arguments
253+
|
254+
= note: positional arguments are zero-based
255+
= note: for information about formatting flags, visit https://doc.rust-lang.org/std/fmt/index.html
256+
257+
error[E0308]: mismatched types
258+
--> $DIR/ifmt-bad-arg.rs:78:32
259+
|
260+
LL | println!("{} {:.*} {}", 1, 3.2, 4);
261+
| ^^^ expected usize, found floating-point number
262+
|
263+
= note: expected type `&usize`
264+
found type `&{float}`
265+
266+
error[E0308]: mismatched types
267+
--> $DIR/ifmt-bad-arg.rs:81:35
268+
|
269+
LL | println!("{} {:07$.*} {}", 1, 3.2, 4);
270+
| ^^^ expected usize, found floating-point number
271+
|
272+
= note: expected type `&usize`
273+
found type `&{float}`
274+
275+
error: aborting due to 33 previous errors
224276

277+
For more information about this error, try `rustc --explain E0308`.

0 commit comments

Comments
 (0)