Skip to content

Commit c6ec5f4

Browse files
committed
Auto merge of #50610 - estebank:fmt-str, r=Kimundi
Improve format string errors Point at format string position inside the formatting string: ``` error: invalid format string: unmatched `}` found --> $DIR/format-string-error.rs:21:22 | LL | let _ = format!("}"); | ^ unmatched `}` in format string ``` Explain that argument names can't start with an underscore: ``` error: invalid format string: invalid argument name `_foo` --> $DIR/format-string-error.rs:15:23 | LL | let _ = format!("{_foo}", _foo = 6usize); | ^^^^ invalid argument name in format string | = note: argument names cannot start with an underscore ``` Fix #23476. The more accurate spans will only be seen when using `format!` directly, when using `println!` the diagnostics machinery makes the span be the entire statement.
2 parents e315056 + 3f6b3bb commit c6ec5f4

File tree

5 files changed

+132
-21
lines changed

5 files changed

+132
-21
lines changed

src/libfmt_macros/lib.rs

+68-14
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ pub enum Count<'a> {
127127
CountImplied,
128128
}
129129

130+
pub struct ParseError {
131+
pub description: string::String,
132+
pub note: Option<string::String>,
133+
pub label: string::String,
134+
pub start: usize,
135+
pub end: usize,
136+
}
137+
130138
/// The parser structure for interpreting the input format string. This is
131139
/// modeled as an iterator over `Piece` structures to form a stream of tokens
132140
/// being output.
@@ -137,7 +145,7 @@ pub struct Parser<'a> {
137145
input: &'a str,
138146
cur: iter::Peekable<str::CharIndices<'a>>,
139147
/// Error messages accumulated during parsing
140-
pub errors: Vec<(string::String, Option<string::String>)>,
148+
pub errors: Vec<ParseError>,
141149
/// Current position of implicit positional argument pointer
142150
curarg: usize,
143151
}
@@ -160,12 +168,17 @@ impl<'a> Iterator for Parser<'a> {
160168
}
161169
'}' => {
162170
self.cur.next();
171+
let pos = pos + 1;
163172
if self.consume('}') {
164-
Some(String(self.string(pos + 1)))
173+
Some(String(self.string(pos)))
165174
} else {
166-
self.err_with_note("unmatched `}` found",
167-
"if you intended to print `}`, \
168-
you can escape it using `}}`");
175+
self.err_with_note(
176+
"unmatched `}` found",
177+
"unmatched `}`",
178+
"if you intended to print `}`, you can escape it using `}}`",
179+
pos,
180+
pos,
181+
);
169182
None
170183
}
171184
}
@@ -191,15 +204,40 @@ impl<'a> Parser<'a> {
191204
/// Notifies of an error. The message doesn't actually need to be of type
192205
/// String, but I think it does when this eventually uses conditions so it
193206
/// might as well start using it now.
194-
fn err(&mut self, msg: &str) {
195-
self.errors.push((msg.to_owned(), None));
207+
fn err<S1: Into<string::String>, S2: Into<string::String>>(
208+
&mut self,
209+
description: S1,
210+
label: S2,
211+
start: usize,
212+
end: usize,
213+
) {
214+
self.errors.push(ParseError {
215+
description: description.into(),
216+
note: None,
217+
label: label.into(),
218+
start,
219+
end,
220+
});
196221
}
197222

198223
/// Notifies of an error. The message doesn't actually need to be of type
199224
/// String, but I think it does when this eventually uses conditions so it
200225
/// might as well start using it now.
201-
fn err_with_note(&mut self, msg: &str, note: &str) {
202-
self.errors.push((msg.to_owned(), Some(note.to_owned())));
226+
fn err_with_note<S1: Into<string::String>, S2: Into<string::String>, S3: Into<string::String>>(
227+
&mut self,
228+
description: S1,
229+
label: S2,
230+
note: S3,
231+
start: usize,
232+
end: usize,
233+
) {
234+
self.errors.push(ParseError {
235+
description: description.into(),
236+
note: Some(note.into()),
237+
label: label.into(),
238+
start,
239+
end,
240+
});
203241
}
204242

205243
/// Optionally consumes the specified character. If the character is not at
@@ -222,19 +260,26 @@ impl<'a> Parser<'a> {
222260
/// found, an error is emitted.
223261
fn must_consume(&mut self, c: char) {
224262
self.ws();
225-
if let Some(&(_, maybe)) = self.cur.peek() {
263+
if let Some(&(pos, maybe)) = self.cur.peek() {
226264
if c == maybe {
227265
self.cur.next();
228266
} else {
229-
self.err(&format!("expected `{:?}`, found `{:?}`", c, maybe));
267+
self.err(format!("expected `{:?}`, found `{:?}`", c, maybe),
268+
format!("expected `{}`", c),
269+
pos + 1,
270+
pos + 1);
230271
}
231272
} else {
232-
let msg = &format!("expected `{:?}` but string was terminated", c);
273+
let msg = format!("expected `{:?}` but string was terminated", c);
274+
let pos = self.input.len() + 1; // point at closing `"`
233275
if c == '}' {
234276
self.err_with_note(msg,
235-
"if you intended to print `{`, you can escape it using `{{`");
277+
format!("expected `{:?}`", c),
278+
"if you intended to print `{`, you can escape it using `{{`",
279+
pos,
280+
pos);
236281
} else {
237-
self.err(msg);
282+
self.err(msg, format!("expected `{:?}`", c), pos, pos);
238283
}
239284
}
240285
}
@@ -300,6 +345,15 @@ impl<'a> Parser<'a> {
300345
} else {
301346
match self.cur.peek() {
302347
Some(&(_, c)) if c.is_alphabetic() => Some(ArgumentNamed(self.word())),
348+
Some(&(pos, c)) if c == '_' => {
349+
let invalid_name = self.string(pos);
350+
self.err_with_note(format!("invalid argument name `{}`", invalid_name),
351+
"invalid argument name",
352+
"argument names cannot start with an underscore",
353+
pos + 1, // add 1 to account for leading `{`
354+
pos + 1 + invalid_name.len());
355+
Some(ArgumentNamed(invalid_name))
356+
},
303357

304358
// This is an `ArgumentNext`.
305359
// Record the fact and do the resolution after parsing the

src/libsyntax_ext/format.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -767,9 +767,12 @@ pub fn expand_preparsed_format_args(ecx: &mut ExtCtxt,
767767
}
768768

769769
if !parser.errors.is_empty() {
770-
let (err, note) = parser.errors.remove(0);
771-
let mut e = cx.ecx.struct_span_err(cx.fmtsp, &format!("invalid format string: {}", err));
772-
if let Some(note) = note {
770+
let err = parser.errors.remove(0);
771+
let sp = cx.fmtsp.from_inner_byte_pos(err.start, err.end);
772+
let mut e = cx.ecx.struct_span_err(sp, &format!("invalid format string: {}",
773+
err.description));
774+
e.span_label(sp, err.label + " in format string");
775+
if let Some(note) = err.note {
773776
e.note(&note);
774777
}
775778
e.emit();

src/libsyntax_pos/lib.rs

+7
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,13 @@ impl Span {
428428
)
429429
}
430430

431+
pub fn from_inner_byte_pos(self, start: usize, end: usize) -> Span {
432+
let span = self.data();
433+
Span::new(span.lo + BytePos::from_usize(start),
434+
span.lo + BytePos::from_usize(end),
435+
span.ctxt)
436+
}
437+
431438
#[inline]
432439
pub fn apply_mark(self, mark: Mark) -> Span {
433440
let span = self.data();

src/test/ui/fmt/format-string-error.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,14 @@ fn main() {
1212
println!("{");
1313
println!("{{}}");
1414
println!("}");
15+
let _ = format!("{_foo}", _foo = 6usize);
16+
//~^ ERROR invalid format string: invalid argument name `_foo`
17+
let _ = format!("{_}", _ = 6usize);
18+
//~^ ERROR invalid format string: invalid argument name `_`
19+
let _ = format!("{");
20+
//~^ ERROR invalid format string: expected `'}'` but string was terminated
21+
let _ = format!("}");
22+
//~^ ERROR invalid format string: unmatched `}` found
23+
let _ = format!("{\\}");
24+
//~^ ERROR invalid format string: expected `'}'`, found `'\\'`
1525
}
16-

src/test/ui/fmt/format-string-error.stderr

+41-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ error: invalid format string: expected `'}'` but string was terminated
22
--> $DIR/format-string-error.rs:12:5
33
|
44
LL | println!("{");
5-
| ^^^^^^^^^^^^^^
5+
| ^^^^^^^^^^^^^^ expected `'}'` in format string
66
|
77
= note: if you intended to print `{`, you can escape it using `{{`
88
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
@@ -11,10 +11,48 @@ error: invalid format string: unmatched `}` found
1111
--> $DIR/format-string-error.rs:14:5
1212
|
1313
LL | println!("}");
14-
| ^^^^^^^^^^^^^^
14+
| ^^^^^^^^^^^^^^ unmatched `}` in format string
1515
|
1616
= note: if you intended to print `}`, you can escape it using `}}`
1717
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
1818

19-
error: aborting due to 2 previous errors
19+
error: invalid format string: invalid argument name `_foo`
20+
--> $DIR/format-string-error.rs:15:23
21+
|
22+
LL | let _ = format!("{_foo}", _foo = 6usize);
23+
| ^^^^ invalid argument name in format string
24+
|
25+
= note: argument names cannot start with an underscore
26+
27+
error: invalid format string: invalid argument name `_`
28+
--> $DIR/format-string-error.rs:17:23
29+
|
30+
LL | let _ = format!("{_}", _ = 6usize);
31+
| ^ invalid argument name in format string
32+
|
33+
= note: argument names cannot start with an underscore
34+
35+
error: invalid format string: expected `'}'` but string was terminated
36+
--> $DIR/format-string-error.rs:19:23
37+
|
38+
LL | let _ = format!("{");
39+
| ^ expected `'}'` in format string
40+
|
41+
= note: if you intended to print `{`, you can escape it using `{{`
42+
43+
error: invalid format string: unmatched `}` found
44+
--> $DIR/format-string-error.rs:21:22
45+
|
46+
LL | let _ = format!("}");
47+
| ^ unmatched `}` in format string
48+
|
49+
= note: if you intended to print `}`, you can escape it using `}}`
50+
51+
error: invalid format string: expected `'}'`, found `'/'`
52+
--> $DIR/format-string-error.rs:23:23
53+
|
54+
LL | let _ = format!("{/}");
55+
| ^ expected `}` in format string
56+
57+
error: aborting due to 7 previous errors
2058

0 commit comments

Comments
 (0)