Rollup 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.
This commit is contained in:
Mark Simulacrum 2018-05-17 13:51:21 -06:00 committed by GitHub
commit b3734bd78f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 21 deletions

View file

@ -127,6 +127,14 @@ pub enum Count<'a> {
CountImplied, CountImplied,
} }
pub struct ParseError {
pub description: string::String,
pub note: Option<string::String>,
pub label: string::String,
pub start: usize,
pub end: usize,
}
/// The parser structure for interpreting the input format string. This is /// The parser structure for interpreting the input format string. This is
/// modeled as an iterator over `Piece` structures to form a stream of tokens /// modeled as an iterator over `Piece` structures to form a stream of tokens
/// being output. /// being output.
@ -137,7 +145,7 @@ pub struct Parser<'a> {
input: &'a str, input: &'a str,
cur: iter::Peekable<str::CharIndices<'a>>, cur: iter::Peekable<str::CharIndices<'a>>,
/// Error messages accumulated during parsing /// Error messages accumulated during parsing
pub errors: Vec<(string::String, Option<string::String>)>, pub errors: Vec<ParseError>,
/// Current position of implicit positional argument pointer /// Current position of implicit positional argument pointer
curarg: usize, curarg: usize,
} }
@ -160,12 +168,17 @@ impl<'a> Iterator for Parser<'a> {
} }
'}' => { '}' => {
self.cur.next(); self.cur.next();
let pos = pos + 1;
if self.consume('}') { if self.consume('}') {
Some(String(self.string(pos + 1))) Some(String(self.string(pos)))
} else { } else {
self.err_with_note("unmatched `}` found", self.err_with_note(
"if you intended to print `}`, \ "unmatched `}` found",
you can escape it using `}}`"); "unmatched `}`",
"if you intended to print `}`, you can escape it using `}}`",
pos,
pos,
);
None None
} }
} }
@ -191,15 +204,40 @@ impl<'a> Parser<'a> {
/// Notifies of an error. The message doesn't actually need to be of type /// Notifies of an error. The message doesn't actually need to be of type
/// String, but I think it does when this eventually uses conditions so it /// String, but I think it does when this eventually uses conditions so it
/// might as well start using it now. /// might as well start using it now.
fn err(&mut self, msg: &str) { fn err<S1: Into<string::String>, S2: Into<string::String>>(
self.errors.push((msg.to_owned(), None)); &mut self,
description: S1,
label: S2,
start: usize,
end: usize,
) {
self.errors.push(ParseError {
description: description.into(),
note: None,
label: label.into(),
start,
end,
});
} }
/// Notifies of an error. The message doesn't actually need to be of type /// Notifies of an error. The message doesn't actually need to be of type
/// String, but I think it does when this eventually uses conditions so it /// String, but I think it does when this eventually uses conditions so it
/// might as well start using it now. /// might as well start using it now.
fn err_with_note(&mut self, msg: &str, note: &str) { fn err_with_note<S1: Into<string::String>, S2: Into<string::String>, S3: Into<string::String>>(
self.errors.push((msg.to_owned(), Some(note.to_owned()))); &mut self,
description: S1,
label: S2,
note: S3,
start: usize,
end: usize,
) {
self.errors.push(ParseError {
description: description.into(),
note: Some(note.into()),
label: label.into(),
start,
end,
});
} }
/// Optionally consumes the specified character. If the character is not at /// Optionally consumes the specified character. If the character is not at
@ -222,19 +260,26 @@ impl<'a> Parser<'a> {
/// found, an error is emitted. /// found, an error is emitted.
fn must_consume(&mut self, c: char) { fn must_consume(&mut self, c: char) {
self.ws(); self.ws();
if let Some(&(_, maybe)) = self.cur.peek() { if let Some(&(pos, maybe)) = self.cur.peek() {
if c == maybe { if c == maybe {
self.cur.next(); self.cur.next();
} else { } else {
self.err(&format!("expected `{:?}`, found `{:?}`", c, maybe)); self.err(format!("expected `{:?}`, found `{:?}`", c, maybe),
format!("expected `{}`", c),
pos + 1,
pos + 1);
} }
} else { } else {
let msg = &format!("expected `{:?}` but string was terminated", c); let msg = format!("expected `{:?}` but string was terminated", c);
let pos = self.input.len() + 1; // point at closing `"`
if c == '}' { if c == '}' {
self.err_with_note(msg, self.err_with_note(msg,
"if you intended to print `{`, you can escape it using `{{`"); format!("expected `{:?}`", c),
"if you intended to print `{`, you can escape it using `{{`",
pos,
pos);
} else { } else {
self.err(msg); self.err(msg, format!("expected `{:?}`", c), pos, pos);
} }
} }
} }
@ -300,6 +345,15 @@ impl<'a> Parser<'a> {
} else { } else {
match self.cur.peek() { match self.cur.peek() {
Some(&(_, c)) if c.is_alphabetic() => Some(ArgumentNamed(self.word())), Some(&(_, c)) if c.is_alphabetic() => Some(ArgumentNamed(self.word())),
Some(&(pos, c)) if c == '_' => {
let invalid_name = self.string(pos);
self.err_with_note(format!("invalid argument name `{}`", invalid_name),
"invalid argument name",
"argument names cannot start with an underscore",
pos + 1, // add 1 to account for leading `{`
pos + 1 + invalid_name.len());
Some(ArgumentNamed(invalid_name))
},
// This is an `ArgumentNext`. // This is an `ArgumentNext`.
// Record the fact and do the resolution after parsing the // Record the fact and do the resolution after parsing the

View file

@ -767,9 +767,12 @@ pub fn expand_preparsed_format_args(ecx: &mut ExtCtxt,
} }
if !parser.errors.is_empty() { if !parser.errors.is_empty() {
let (err, note) = parser.errors.remove(0); let err = parser.errors.remove(0);
let mut e = cx.ecx.struct_span_err(cx.fmtsp, &format!("invalid format string: {}", err)); let sp = cx.fmtsp.from_inner_byte_pos(err.start, err.end);
if let Some(note) = note { let mut e = cx.ecx.struct_span_err(sp, &format!("invalid format string: {}",
err.description));
e.span_label(sp, err.label + " in format string");
if let Some(note) = err.note {
e.note(&note); e.note(&note);
} }
e.emit(); e.emit();

View file

@ -428,6 +428,13 @@ impl Span {
) )
} }
pub fn from_inner_byte_pos(self, start: usize, end: usize) -> Span {
let span = self.data();
Span::new(span.lo + BytePos::from_usize(start),
span.lo + BytePos::from_usize(end),
span.ctxt)
}
#[inline] #[inline]
pub fn apply_mark(self, mark: Mark) -> Span { pub fn apply_mark(self, mark: Mark) -> Span {
let span = self.data(); let span = self.data();

View file

@ -12,5 +12,14 @@ fn main() {
println!("{"); println!("{");
println!("{{}}"); println!("{{}}");
println!("}"); println!("}");
let _ = format!("{_foo}", _foo = 6usize);
//~^ ERROR invalid format string: invalid argument name `_foo`
let _ = format!("{_}", _ = 6usize);
//~^ ERROR invalid format string: invalid argument name `_`
let _ = format!("{");
//~^ ERROR invalid format string: expected `'}'` but string was terminated
let _ = format!("}");
//~^ ERROR invalid format string: unmatched `}` found
let _ = format!("{\\}");
//~^ ERROR invalid format string: expected `'}'`, found `'\\'`
} }

View file

@ -2,7 +2,7 @@ error: invalid format string: expected `'}'` but string was terminated
--> $DIR/format-string-error.rs:12:5 --> $DIR/format-string-error.rs:12:5
| |
LL | println!("{"); LL | println!("{");
| ^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^ expected `'}'` in format string
| |
= note: if you intended to print `{`, you can escape it using `{{` = note: if you intended to print `{`, you can escape it using `{{`
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info) = 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
--> $DIR/format-string-error.rs:14:5 --> $DIR/format-string-error.rs:14:5
| |
LL | println!("}"); LL | println!("}");
| ^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^ unmatched `}` in format string
| |
= note: if you intended to print `}`, you can escape it using `}}` = note: if you intended to print `}`, you can escape it using `}}`
= note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info) = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
error: aborting due to 2 previous errors 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
error: invalid format string: invalid argument name `_`
--> $DIR/format-string-error.rs:17:23
|
LL | let _ = format!("{_}", _ = 6usize);
| ^ invalid argument name in format string
|
= note: argument names cannot start with an underscore
error: invalid format string: expected `'}'` but string was terminated
--> $DIR/format-string-error.rs:19:23
|
LL | let _ = format!("{");
| ^ expected `'}'` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: unmatched `}` found
--> $DIR/format-string-error.rs:21:22
|
LL | let _ = format!("}");
| ^ unmatched `}` in format string
|
= note: if you intended to print `}`, you can escape it using `}}`
error: invalid format string: expected `'}'`, found `'/'`
--> $DIR/format-string-error.rs:23:23
|
LL | let _ = format!("{/}");
| ^ expected `}` in format string
error: aborting due to 7 previous errors