Various changes to string format diagnostics

- Point at opening mismatched formatting brace
- Account for differences between raw and regular strings
- Account for differences between the code snippet and `InternedString`
- Add more tests
This commit is contained in:
Esteban Küber 2018-12-20 22:33:16 -08:00
parent 79d8a0fcef
commit 862ebc4c38
9 changed files with 415 additions and 60 deletions

View File

@ -123,8 +123,9 @@ pub struct ParseError {
pub description: string::String,
pub note: Option<string::String>,
pub label: string::String,
pub start: usize,
pub end: usize,
pub start: SpanIndex,
pub end: SpanIndex,
pub secondary_label: Option<(string::String, SpanIndex, SpanIndex)>,
}
/// The parser structure for interpreting the input format string. This is
@ -142,22 +143,39 @@ pub struct Parser<'a> {
curarg: usize,
/// `Some(raw count)` when the string is "raw", used to position spans correctly
style: Option<usize>,
/// How many newlines have been seen in the string so far, to adjust the error spans
seen_newlines: usize,
/// Start and end byte offset of every successfully parsed argument
pub arg_places: Vec<(usize, usize)>,
/// Characters that need to be shifted
skips: Vec<usize>,
/// Span offset of the last opening brace seen, used for error reporting
last_opening_brace_pos: Option<SpanIndex>,
/// Wether the source string is comes from `println!` as opposed to `format!` or `print!`
append_newline: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct SpanIndex(usize);
impl SpanIndex {
pub fn unwrap(self) -> usize {
self.0
}
}
impl<'a> Iterator for Parser<'a> {
type Item = Piece<'a>;
fn next(&mut self) -> Option<Piece<'a>> {
let raw = self.style.map(|raw| raw + self.seen_newlines).unwrap_or(0);
let raw = self.raw();
if let Some(&(pos, c)) = self.cur.peek() {
match c {
'{' => {
let curr_last_brace = self.last_opening_brace_pos;
self.last_opening_brace_pos = Some(self.to_span_index(pos));
self.cur.next();
if self.consume('{') {
self.last_opening_brace_pos = curr_last_brace;
Some(String(self.string(pos + 1)))
} else {
let arg = self.argument();
@ -174,7 +192,7 @@ impl<'a> Iterator for Parser<'a> {
if self.consume('}') {
Some(String(self.string(pos + 1)))
} else {
let err_pos = pos + raw + 1;
let err_pos = self.to_span_index(pos);
self.err_with_note(
"unmatched `}` found",
"unmatched `}`",
@ -186,7 +204,6 @@ impl<'a> Iterator for Parser<'a> {
}
}
'\n' => {
self.seen_newlines += 1;
Some(String(self.string(pos)))
}
_ => Some(String(self.string(pos))),
@ -199,15 +216,22 @@ impl<'a> Iterator for Parser<'a> {
impl<'a> Parser<'a> {
/// Creates a new parser for the given format string
pub fn new(s: &'a str, style: Option<usize>) -> Parser<'a> {
pub fn new(
s: &'a str,
style: Option<usize>,
skips: Vec<usize>,
append_newline: bool,
) -> Parser<'a> {
Parser {
input: s,
cur: s.char_indices().peekable(),
errors: vec![],
curarg: 0,
style,
seen_newlines: 0,
arg_places: vec![],
skips,
last_opening_brace_pos: None,
append_newline,
}
}
@ -218,8 +242,8 @@ impl<'a> Parser<'a> {
&mut self,
description: S1,
label: S2,
start: usize,
end: usize,
start: SpanIndex,
end: SpanIndex,
) {
self.errors.push(ParseError {
description: description.into(),
@ -227,6 +251,7 @@ impl<'a> Parser<'a> {
label: label.into(),
start,
end,
secondary_label: None,
});
}
@ -238,8 +263,8 @@ impl<'a> Parser<'a> {
description: S1,
label: S2,
note: S3,
start: usize,
end: usize,
start: SpanIndex,
end: SpanIndex,
) {
self.errors.push(ParseError {
description: description.into(),
@ -247,6 +272,7 @@ impl<'a> Parser<'a> {
label: label.into(),
start,
end,
secondary_label: None,
});
}
@ -266,47 +292,86 @@ impl<'a> Parser<'a> {
}
}
fn raw(&self) -> usize {
self.style.map(|raw| raw + 1).unwrap_or(0)
}
fn to_span_index(&self, pos: usize) -> SpanIndex {
let mut pos = pos;
for skip in &self.skips {
if pos > *skip {
pos += 1;
} else if pos == *skip && self.raw() == 0 {
pos += 1;
} else {
break;
}
}
SpanIndex(self.raw() + pos + 1)
}
/// Forces consumption of the specified character. If the character is not
/// found, an error is emitted.
fn must_consume(&mut self, c: char) -> Option<usize> {
self.ws();
let raw = self.style.unwrap_or(0);
let padding = raw + self.seen_newlines;
if let Some(&(pos, maybe)) = self.cur.peek() {
if c == maybe {
self.cur.next();
Some(pos)
} else {
let pos = pos + raw + 1;
self.err(format!("expected `{:?}`, found `{:?}`", c, maybe),
format!("expected `{}`", c),
pos,
pos);
let pos = self.to_span_index(pos);
let description = format!("expected `'}}'`, found `{:?}`", maybe);
let label = "expected `}`".to_owned();
let (note, secondary_label) = if c == '}' {
(Some("if you intended to print `{`, you can escape it using `{{`".to_owned()),
self.last_opening_brace_pos.map(|pos| {
("because of this opening brace".to_owned(), pos, pos)
}))
} else {
(None, None)
};
self.errors.push(ParseError {
description,
note,
label,
start: pos,
end: pos,
secondary_label,
});
None
}
} else {
let msg = format!("expected `{:?}` but string was terminated", c);
// point at closing `"`, unless the last char is `\n` to account for `println`
let pos = match self.input.chars().last() {
Some('\n') => self.input.len(),
_ => self.input.len() + 1,
};
let description = format!("expected `{:?}` but string was terminated", c);
// point at closing `"`
let pos = self.input.len() - if self.append_newline { 1 } else { 0 };
let pos = self.to_span_index(pos);
if c == '}' {
self.err_with_note(msg,
format!("expected `{:?}`", c),
"if you intended to print `{`, you can escape it using `{{`",
pos + padding,
pos + padding);
let label = format!("expected `{:?}`", c);
let (note, secondary_label) = if c == '}' {
(Some("if you intended to print `{`, you can escape it using `{{`".to_owned()),
self.last_opening_brace_pos.map(|pos| {
("because of this opening brace".to_owned(), pos, pos)
}))
} else {
(None, None)
};
self.errors.push(ParseError {
description,
note,
label,
start: pos,
end: pos,
secondary_label,
});
} else {
self.err(msg, format!("expected `{:?}`", c), pos, pos);
self.err(description, format!("expected `{:?}`", c), pos, pos);
}
None
}
}
/// Consumes all whitespace characters until the first non-whitespace
/// character
/// Consumes all whitespace characters until the first non-whitespace character
fn ws(&mut self) {
while let Some(&(_, c)) = self.cur.peek() {
if c.is_whitespace() {
@ -334,8 +399,7 @@ impl<'a> Parser<'a> {
&self.input[start..self.input.len()]
}
/// Parses an Argument structure, or what's contained within braces inside
/// the format string
/// Parses an Argument structure, or what's contained within braces inside the format string
fn argument(&mut self) -> Argument<'a> {
let pos = self.position();
let format = self.format();
@ -371,8 +435,8 @@ impl<'a> Parser<'a> {
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());
self.to_span_index(pos),
self.to_span_index(pos + invalid_name.len()));
Some(ArgumentNamed(invalid_name))
},
@ -553,7 +617,7 @@ mod tests {
use super::*;
fn same(fmt: &'static str, p: &[Piece<'static>]) {
let parser = Parser::new(fmt, None);
let parser = Parser::new(fmt, None, vec![], false);
assert!(parser.collect::<Vec<Piece<'static>>>() == p);
}
@ -569,7 +633,7 @@ mod tests {
}
fn musterr(s: &str) {
let mut p = Parser::new(s, None);
let mut p = Parser::new(s, None, vec![], false);
p.next();
assert!(!p.errors.is_empty());
}

View File

@ -234,7 +234,7 @@ impl<'a, 'gcx, 'tcx> OnUnimplementedFormatString {
{
let name = tcx.item_name(trait_def_id);
let generics = tcx.generics_of(trait_def_id);
let parser = Parser::new(&self.0, None);
let parser = Parser::new(&self.0, None, vec![], false);
let mut result = Ok(());
for token in parser {
match token {
@ -293,7 +293,7 @@ impl<'a, 'gcx, 'tcx> OnUnimplementedFormatString {
}).collect::<FxHashMap<String, String>>();
let empty_string = String::new();
let parser = Parser::new(&self.0, None);
let parser = Parser::new(&self.0, None, vec![], false);
parser.map(|p|
match p {
Piece::String(s) => s,

View File

@ -765,18 +765,74 @@ pub fn expand_preparsed_format_args(ecx: &mut ExtCtxt,
}
};
let is_literal = match ecx.source_map().span_to_snippet(fmt_sp) {
Ok(ref s) if s.starts_with("\"") || s.starts_with("r#") => true,
_ => false,
let (is_literal, fmt_snippet) = match ecx.source_map().span_to_snippet(fmt_sp) {
Ok(s) => (s.starts_with("\"") || s.starts_with("r#"), Some(s)),
_ => (false, None),
};
let fmt_str = &*fmt.node.0.as_str();
let str_style = match fmt.node.1 {
ast::StrStyle::Cooked => None,
ast::StrStyle::Raw(raw) => Some(raw as usize),
ast::StrStyle::Raw(raw) => {
Some(raw as usize)
},
};
let mut parser = parse::Parser::new(fmt_str, str_style);
/// Find the indices of all characters that have been processed and differ between the actual
/// written code (code snippet) and the `InternedString` that get's processed in the `Parser`
/// in order to properly synthethise the intra-string `Span`s for error diagnostics.
fn find_skips(snippet: &str, is_raw: bool) -> Vec<usize> {
let mut eat_ws = false;
let mut s = snippet.chars().enumerate().peekable();
let mut skips = vec![];
while let Some((pos, c)) = s.next() {
match (c, s.peek()) {
// skip whitespace and empty lines ending in '\\'
('\\', Some((next_pos, '\n'))) if !is_raw => {
eat_ws = true;
skips.push(pos);
skips.push(*next_pos);
let _ = s.next();
}
('\\', Some((next_pos, '\n'))) |
('\\', Some((next_pos, 'n'))) |
('\\', Some((next_pos, 't'))) if eat_ws => {
skips.push(pos);
skips.push(*next_pos);
let _ = s.next();
}
(' ', _) |
('\n', _) |
('\t', _) if eat_ws => {
skips.push(pos);
}
('\\', Some((next_pos, 'n'))) |
('\\', Some((next_pos, 't'))) |
('\\', Some((next_pos, '\\'))) |
('\\', Some((next_pos, '\''))) |
('\\', Some((next_pos, '\"'))) => {
skips.push(*next_pos);
let _ = s.next();
}
_ if eat_ws => { // `take_while(|c| c.is_whitespace())`
eat_ws = false;
}
_ => {}
}
}
skips
}
let skips = if let (true, Some(ref snippet)) = (is_literal, fmt_snippet.as_ref()) {
let r_start = str_style.map(|r| r + 1).unwrap_or(0);
let r_end = str_style.map(|r| r).unwrap_or(0);
let s = &snippet[r_start + 1..snippet.len() - r_end - 1];
find_skips(s, str_style.is_some())
} else {
vec![]
};
let fmt_str = &*fmt.node.0.as_str(); // for the suggestions below
let mut parser = parse::Parser::new(fmt_str, str_style, skips.clone(), append_newline);
let mut unverified_pieces = Vec::new();
while let Some(piece) = parser.next() {
@ -789,12 +845,17 @@ pub fn expand_preparsed_format_args(ecx: &mut ExtCtxt,
if !parser.errors.is_empty() {
let err = parser.errors.remove(0);
let sp = fmt.span.from_inner_byte_pos(err.start, err.end);
let mut e = ecx.struct_span_err(sp, &format!("invalid format string: {}", err.description));
let sp = fmt.span.from_inner_byte_pos(err.start.unwrap(), err.end.unwrap());
let mut e = 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);
}
if let Some((label, start, end)) = err.secondary_label {
let sp = fmt.span.from_inner_byte_pos(start.unwrap(), end.unwrap());
e.span_label(sp, label);
}
e.emit();
return DummyResult::raw_expr(sp);
}

View File

@ -0,0 +1,70 @@
// ignore-tidy-tab
fn main() {
format!("{
a");
//~^ ERROR invalid format string
format!("{ \
b");
//~^ ERROR invalid format string
format!(r#"{ \
rawc"#);
//~^^^ ERROR invalid format string
format!(r#"{ \n
\n
rawd"#);
//~^^^ ERROR invalid format string
format!("{ \n
\n
e");
//~^ ERROR invalid format string
format!("
{
a");
//~^ ERROR invalid format string
format!("
{
a
");
//~^^ ERROR invalid format string
format!(" \
{ \
\
b");
//~^ ERROR invalid format string
format!(" \
{ \
\
b \
");
//~^^^ ERROR invalid format string
format!(r#"
raw { \
c"#);
//~^^^ ERROR invalid format string
format!(r#"
raw { \n
\n
d"#);
//~^^^ ERROR invalid format string
format!("
{ \n
\n
e");
//~^ ERROR invalid format string
format!("
{asdf
}
", asdf=1);
// ok - this is supported
format!("
{
asdf}
", asdf=1);
//~^^ ERROR invalid format string
}

View File

@ -0,0 +1,137 @@
error: invalid format string: expected `'}'`, found `'a'`
--> $DIR/format-string-error-2.rs:5:5
|
LL | format!("{
| - because of this opening brace
LL | a");
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'b'`
--> $DIR/format-string-error-2.rs:9:5
|
LL | format!("{ /
| - because of this opening brace
LL |
LL | b");
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'/'`
--> $DIR/format-string-error-2.rs:11:18
|
LL | format!(r#"{ /
| - ^ expected `}` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'/'`
--> $DIR/format-string-error-2.rs:15:18
|
LL | format!(r#"{ /n
| - ^ expected `}` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'e'`
--> $DIR/format-string-error-2.rs:21:5
|
LL | format!("{ /n
| - because of this opening brace
LL | /n
LL | e");
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'a'`
--> $DIR/format-string-error-2.rs:25:5
|
LL | {
| - because of this opening brace
LL | a");
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'a'`
--> $DIR/format-string-error-2.rs:29:5
|
LL | {
| - because of this opening brace
LL | a
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'b'`
--> $DIR/format-string-error-2.rs:35:5
|
LL | { /
| - because of this opening brace
LL | /
LL | b");
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'b'`
--> $DIR/format-string-error-2.rs:40:5
|
LL | { /
| - because of this opening brace
LL | /
LL | b /
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'/'`
--> $DIR/format-string-error-2.rs:45:8
|
LL | raw { /
| - ^ expected `}` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'/'`
--> $DIR/format-string-error-2.rs:50:8
|
LL | raw { /n
| - ^ expected `}` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'e'`
--> $DIR/format-string-error-2.rs:57:5
|
LL | { /n
| - because of this opening brace
LL | /n
LL | e");
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'`, found `'a'`
--> $DIR/format-string-error-2.rs:67:5
|
LL | {
| - because of this opening brace
LL | asdf}
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: aborting due to 13 previous errors

View File

@ -31,7 +31,7 @@ fn main() {
{
"###);
//~^^ ERROR invalid format string
//~^ ERROR invalid format string
let _ = format!(r###"

View File

@ -2,7 +2,9 @@ error: invalid format string: expected `'}'` but string was terminated
--> $DIR/format-string-error.rs:4:16
|
LL | println!("{");
| ^ expected `'}'` in format string
| -^ expected `'}'` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
@ -34,7 +36,9 @@ error: invalid format string: expected `'}'` but string was terminated
--> $DIR/format-string-error.rs:13:23
|
LL | let _ = format!("{");
| ^ expected `'}'` in format string
| -^ expected `'}'` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
@ -50,13 +54,19 @@ error: invalid format string: expected `'}'`, found `'/'`
--> $DIR/format-string-error.rs:17:23
|
LL | let _ = format!("{/}");
| ^ expected `}` in format string
| -^ expected `}` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'` but string was terminated
--> $DIR/format-string-error.rs:19:29
--> $DIR/format-string-error.rs:19:35
|
LL | let _ = format!("/n/n/n{/n/n/n");
| ^ expected `'}'` in format string
| - ^ expected `'}'` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
@ -64,14 +74,19 @@ error: invalid format string: expected `'}'` but string was terminated
--> $DIR/format-string-error.rs:25:3
|
LL | {"###);
| ^ expected `'}'` in format string
| -^ expected `'}'` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
error: invalid format string: expected `'}'` but string was terminated
--> $DIR/format-string-error.rs:32:1
--> $DIR/format-string-error.rs:33:1
|
LL | {
| - because of this opening brace
LL |
LL | "###);
| ^ expected `'}'` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`

View File

@ -168,7 +168,9 @@ error: invalid format string: expected `'}'` but string was terminated
--> $DIR/ifmt-bad-arg.rs:51:15
|
LL | format!("{"); //~ ERROR: expected `'}'` but string was terminated
| ^ expected `'}'` in format string
| -^ expected `'}'` in format string
| |
| because of this opening brace
|
= note: if you intended to print `{`, you can escape it using `{{`
@ -207,8 +209,12 @@ LL | {foo}
error: invalid format string: expected `'}'`, found `'t'`
--> $DIR/ifmt-bad-arg.rs:75:1
|
LL | ninth number: {
| - because of this opening brace
LL | tenth number: {}",
| ^ expected `}` in format string
|
= note: if you intended to print `{`, you can escape it using `{{`
error: aborting due to 28 previous errors

View File

@ -2,7 +2,9 @@ error: invalid format string: expected `'}'` but string was terminated
--> $DIR/issue-51848.rs:6:20
|
LL | println!("{"); //~ ERROR invalid
| ^ expected `'}'` in format string
| -^ expected `'}'` in format string
| |
| because of this opening brace
...
LL | macro_with_error!();
| -------------------- in this macro invocation