use super::*;
use nom::error::VerboseError;
use std::collections::BTreeMap;
use std::fmt;
impl<'a> Error<'a> {
pub(super) fn new(src: &'a str, verr: VerboseError<&'a str>) -> Self {
Self {
errs: verr
.errors
.into_iter()
.map(|(i, e)| (offset(src, i), e))
.collect(),
lines: lines_to_map(src),
empty_input: src.is_empty(),
}
}
pub fn get(&self, index: usize) -> Option<Trace<'a>> {
let (offset, ekind) = self.errs.get(index)?;
let trace = if self.empty_input {
make_empty_trace(ekind)
} else {
let (line_offset, line, linestr) =
get_line(&self.lines, *offset).expect("not empty input");
let col = offset.saturating_sub(line_offset);
let msg = make_trace_msg(linestr, col, ekind);
let col = col + 1; Trace {
line,
col,
linestr,
msg,
}
};
Some(trace)
}
pub fn iter<'i>(&'i self) -> impl Iterator<Item = Trace<'a>> + 'i {
(0..self.len()).filter_map(move |i| self.get(i))
}
pub fn len(&self) -> usize {
self.errs.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn backtrace(&self) -> String {
use std::io::Write;
let mut buf = Vec::<u8>::new();
let last = self.len().saturating_sub(1);
self.iter().enumerate().for_each(|(idx, trace)| {
let e = "won't fail in memory";
let mut w = || write!(&mut buf, "#{}: {}", idx, trace).expect(e);
if idx == last {
w();
} else {
w();
writeln!(&mut buf, "\n").expect(e);
}
});
String::from_utf8(buf).expect("all str so should not be invalid")
}
}
impl<'a> fmt::Debug for Error<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.backtrace())
}
}
impl<'a> fmt::Display for Trace<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "at {}:{} :: {}", self.line, self.col, self.msg)?;
writeln!(f, "{}", self.linestr)?;
if self.col > 0 {
for _ in 0..self.col.saturating_sub(1) {
write!(f, " ")?;
}
write!(f, "^")?;
}
Ok(())
}
}
fn offset(src: &str, sub: &str) -> usize {
(sub.as_ptr() as usize) - (src.as_ptr() as usize)
}
fn get_line<'a>(
lines: &BTreeMap<usize, (usize, &'a str)>,
offset: usize,
) -> Option<(usize, usize, &'a str)> {
lines
.range(..=offset)
.last()
.map(|x| (*x.0, (x.1).0, (x.1).1))
}
fn lines_to_map(src: &str) -> BTreeMap<usize, (usize, &str)> {
src.lines()
.enumerate()
.map(|(idx, line)| (offset(src, line), (idx + 1, line)))
.collect()
}
fn make_empty_trace(ekind: &VerboseErrorKind) -> Trace<'static> {
use VerboseErrorKind::*;
let msg = match ekind {
Char(c) => format!("expected '{}', got empty input", c),
Context(s) => format!("in {}, got empty input", s),
Nom(e) => format!("in {:?}, got empty input", e),
};
Trace {
line: 0,
col: 0,
linestr: "",
msg,
}
}
fn make_trace_msg(linestr: &str, col: usize, ekind: &VerboseErrorKind) -> String {
use VerboseErrorKind::*;
match ekind {
Char(c) => {
if col >= linestr.len() {
format!("expected '{}', reached end of line or end of file first", c)
} else {
format!(
"expected '{}', found '{}'",
c,
linestr[col..]
.chars()
.next()
.expect("character should exist at col")
)
}
}
Context(s) => format!("in {}", s),
Nom(e) => format!("in {:?}", e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_offset() {
let s = "Hello, world!";
let slice = &s[5..];
assert_eq!(offset(s, slice), 5);
let slice = &s[..];
assert_eq!(offset(s, slice), 0);
let slice = &s[..5];
assert_eq!(offset(s, slice), 0);
}
#[test]
fn test_lines_to_map() {
let s = "Hello\nA\r\nBrave\r\nWorld";
let map = lines_to_map(s);
let mut iter = map.iter();
assert_eq!(iter.next(), Some((&0, &(1, "Hello"))));
assert_eq!(iter.next(), Some((&6, &(2, "A"))));
assert_eq!(iter.next(), Some((&9, &(3, "Brave"))));
assert_eq!(iter.next(), Some((&16, &(4, "World"))));
assert_eq!(iter.next(), None);
}
#[test]
#[allow(clippy::all)]
fn test_get_line() {
let s = "";
let map = lines_to_map(s);
assert_eq!(get_line(&map, 0), None);
assert_eq!(get_line(&map, 1), None);
let s = "Hello\nA\r\nBrave\r\nWorld";
let map = lines_to_map(s);
assert_eq!(get_line(&map, 0), Some((0, 1, "Hello")));
assert_eq!(get_line(&map, 1), Some((0, 1, "Hello")));
assert_eq!(get_line(&map, 2), Some((0, 1, "Hello")));
assert_eq!(get_line(&map, 3), Some((0, 1, "Hello")));
assert_eq!(get_line(&map, 4), Some((0, 1, "Hello")));
assert_eq!(get_line(&map, 5), Some((0, 1, "Hello")));
assert_eq!(get_line(&map, 6), Some((6, 2, "A")));
assert_eq!(get_line(&map, 7), Some((6, 2, "A")));
assert_eq!(get_line(&map, 8), Some((6, 2, "A")));
assert_eq!(get_line(&map, 9), Some((9, 3, "Brave")));
assert_eq!(get_line(&map, 10), Some((9, 3, "Brave")));
assert_eq!(get_line(&map, 11), Some((9, 3, "Brave")));
assert_eq!(get_line(&map, 12), Some((9, 3, "Brave")));
assert_eq!(get_line(&map, 13), Some((9, 3, "Brave")));
assert_eq!(get_line(&map, 14), Some((9, 3, "Brave")));
assert_eq!(get_line(&map, 15), Some((9, 3, "Brave")));
assert_eq!(get_line(&map, 16), Some((16, 4, "World")));
assert_eq!(get_line(&map, 17), Some((16, 4, "World")));
assert_eq!(get_line(&map, 18), Some((16, 4, "World")));
assert_eq!(get_line(&map, 19), Some((16, 4, "World")));
assert_eq!(get_line(&map, 20), Some((16, 4, "World")));
assert_eq!(get_line(&map, 21), Some((16, 4, "World")));
assert_eq!(get_line(&map, 22), Some((16, 4, "World")));
assert_eq!(get_line(&map, 23), Some((16, 4, "World")));
assert_eq!(get_line(&map, 24), Some((16, 4, "World")));
}
#[test]
fn str_parse() {
let s = r#"<str> "Hello, world!"#;
let err = parse(s).unwrap_err();
println!("{}", err.backtrace());
assert_eq!(err.len(), 4);
assert_eq!(
err.get(0),
Some(Trace {
line: 1,
col: 21,
linestr: r#"<str> "Hello, world!"#,
msg: r#"expected '"', reached end of line or end of file first"#.to_string(),
})
);
assert_eq!(
err.get(1),
Some(Trace {
line: 1,
col: 7,
linestr: r#"<str> "Hello, world!"#,
msg: r#"in string"#.to_string(),
})
);
assert_eq!(
err.get(2),
Some(Trace {
line: 1,
col: 7,
linestr: r#"<str> "Hello, world!"#,
msg: r#"in primitive value"#.to_string(),
})
);
assert_eq!(
err.get(3),
Some(Trace {
line: 1,
col: 1,
linestr: r#"<str> "Hello, world!"#,
msg: r#"in primitive"#.to_string(),
})
);
assert_eq!(err.get(4), None);
}
#[test]
fn test_parse_err_get() {
let s = r#"
[[a-map]]
("key, 1):
[1,2,3,,4]
"#;
let err = parse(s).unwrap_err();
println!("{}", err.backtrace());
assert_eq!(
err.get(0),
Some(Trace {
line: 4,
col: 16,
linestr: " [1,2,3,,4]",
msg: r#"expected '"', reached end of line or end of file first"#.to_string(),
})
);
}
#[test]
fn test_empty_trace() {
let err = parse("").unwrap_err();
println!("{}", err.backtrace());
for trace in err.iter() {
assert_eq!(trace.msg.contains("got empty input"), true);
}
}
#[test]
fn debug_err_fmt() {
let err = parse("(1,2,5]").unwrap_err();
assert_eq!(format!("{:?}", err), err.backtrace());
}
#[test]
fn make_empty_trace_test() {
assert_eq!(
make_empty_trace(&VerboseErrorKind::Char('x')),
Trace {
line: 0,
col: 0,
linestr: "",
msg: r#"expected 'x', got empty input"#.into()
}
);
assert_eq!(
make_empty_trace(&VerboseErrorKind::Context("some-context")),
Trace {
line: 0,
col: 0,
linestr: "",
msg: r#"in some-context, got empty input"#.into()
}
);
assert_eq!(
make_empty_trace(&VerboseErrorKind::Nom(nom::error::ErrorKind::Alt)),
Trace {
line: 0,
col: 0,
linestr: "",
msg: r#"in Alt, got empty input"#.into()
}
);
}
#[test]
fn make_trace_msg_test() {
assert_eq!(
&make_trace_msg("a line string", 5, &VerboseErrorKind::Char('x')),
"expected 'x', found 'e'"
);
assert_eq!(
&make_trace_msg(
"a line string",
5,
&VerboseErrorKind::Context("some-context")
),
"in some-context"
);
assert_eq!(
&make_trace_msg(
"a line string",
5,
&VerboseErrorKind::Nom(nom::error::ErrorKind::Alt)
),
"in Alt"
);
}
#[test]
fn backtrace_msg() {
let string = "
list = [
(0,1)
(2,3]
]";
let fail = parse::parse(string).unwrap_err();
println!("{}", fail.backtrace());
assert_eq!(
&fail.backtrace(),
r##"#0: at 4:9 :: expected ')', found ']'
(2,3]
^
#1: at 4:6 :: in inline tuple
(2,3]
^
#2: at 2:9 :: in multi-line (concise) sequence
list = [
^
#3: at 2:1 :: in name-kserd key value pair
list = [
^"##
);
}
}