Skip to content

Commit c8c0313

Browse files
committed
move VerboseError to nom-language
1 parent 4e8f4d0 commit c8c0313

File tree

9 files changed

+271
-285
lines changed

9 files changed

+271
-285
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ default-features = false
4141
[dev-dependencies]
4242
doc-comment = "0.3"
4343
proptest = "=1.0.0"
44-
44+
nom-language = { path = "./nom-language" }
4545

4646
[package.metadata.docs.rs]
4747
features = ["alloc", "std", "docsrs"]

benchmarks/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ harness = false
5656

5757
[dev-dependencies]
5858
codspeed-criterion-compat = "2.4.1"
59+
nom-language = { path = "../nom-language" }

benchmarks/benches/json.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ use nom::{
77
bytes::{tag, take},
88
character::{anychar, char, multispace0, none_of},
99
combinator::{map, map_opt, map_res, value, verify},
10-
error::{Error, ErrorKind, FromExternalError, ParseError, VerboseError},
10+
error::{Error, ErrorKind, FromExternalError, ParseError},
1111
multi::{fold, separated_list0},
1212
number::double,
1313
number::recognize_float,
1414
sequence::{delimited, preceded, separated_pair},
1515
Check, Complete, Emit, IResult, Mode, OutputM, Parser,
1616
};
17+
use nom_language::error::VerboseError;
1718

1819
use std::{collections::HashMap, marker::PhantomData, num::ParseIntError};
1920

examples/json.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ use nom::{
55
bytes::complete::{escaped, tag, take_while},
66
character::complete::{alphanumeric1 as alphanumeric, char, one_of},
77
combinator::{cut, map, opt, value},
8-
error::{context, convert_error, ContextError, ErrorKind, ParseError, VerboseError},
8+
error::{context, ContextError, ErrorKind, ParseError},
99
multi::separated_list0,
1010
number::complete::double,
1111
sequence::{delimited, preceded, separated_pair, terminated},
1212
Err, IResult, Parser,
1313
};
14+
use nom_language::error::{convert_error, VerboseError};
1415
use std::collections::HashMap;
1516
use std::str;
1617

examples/s_expression.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ use nom::{
99
bytes::complete::tag,
1010
character::complete::{alpha1, char, digit1, multispace0, multispace1, one_of},
1111
combinator::{cut, map, map_res, opt},
12-
error::{context, VerboseError},
12+
error::context,
1313
multi::many,
1414
sequence::{delimited, preceded, terminated},
1515
IResult, Parser,
1616
};
17+
use nom_language::error::VerboseError;
1718

1819
/// We start by defining the types that define the shape of data that we want.
1920
/// In this case, we want something tree-like

nom-language/src/error.rs

+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
use std::fmt;
2+
3+
use nom::{
4+
error::{ContextError, ErrorKind, FromExternalError, ParseError},
5+
ErrorConvert,
6+
};
7+
8+
/// This error type accumulates errors and their position when backtracking
9+
/// through a parse tree. With some post processing,
10+
/// it can be used to display user friendly error messages
11+
#[derive(Clone, Debug, Eq, PartialEq)]
12+
pub struct VerboseError<I> {
13+
/// List of errors accumulated by `VerboseError`, containing the affected
14+
/// part of input data, and some context
15+
pub errors: Vec<(I, VerboseErrorKind)>,
16+
}
17+
18+
#[derive(Clone, Debug, Eq, PartialEq)]
19+
/// Error context for `VerboseError`
20+
pub enum VerboseErrorKind {
21+
/// Static string added by the `context` function
22+
Context(&'static str),
23+
/// Indicates which character was expected by the `char` function
24+
Char(char),
25+
/// Error kind given by various nom parsers
26+
Nom(ErrorKind),
27+
}
28+
29+
impl<I> ParseError<I> for VerboseError<I> {
30+
fn from_error_kind(input: I, kind: ErrorKind) -> Self {
31+
VerboseError {
32+
errors: vec![(input, VerboseErrorKind::Nom(kind))],
33+
}
34+
}
35+
36+
fn append(input: I, kind: ErrorKind, mut other: Self) -> Self {
37+
other.errors.push((input, VerboseErrorKind::Nom(kind)));
38+
other
39+
}
40+
41+
fn from_char(input: I, c: char) -> Self {
42+
VerboseError {
43+
errors: vec![(input, VerboseErrorKind::Char(c))],
44+
}
45+
}
46+
}
47+
48+
impl<I> ContextError<I> for VerboseError<I> {
49+
fn add_context(input: I, ctx: &'static str, mut other: Self) -> Self {
50+
other.errors.push((input, VerboseErrorKind::Context(ctx)));
51+
other
52+
}
53+
}
54+
55+
impl<I, E> FromExternalError<I, E> for VerboseError<I> {
56+
/// Create a new error from an input position and an external error
57+
fn from_external_error(input: I, kind: ErrorKind, _e: E) -> Self {
58+
Self::from_error_kind(input, kind)
59+
}
60+
}
61+
62+
impl<I: fmt::Display> fmt::Display for VerboseError<I> {
63+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64+
writeln!(f, "Parse error:")?;
65+
for (input, error) in &self.errors {
66+
match error {
67+
VerboseErrorKind::Nom(e) => writeln!(f, "{:?} at: {}", e, input)?,
68+
VerboseErrorKind::Char(c) => writeln!(f, "expected '{}' at: {}", c, input)?,
69+
VerboseErrorKind::Context(s) => writeln!(f, "in section '{}', at: {}", s, input)?,
70+
}
71+
}
72+
73+
Ok(())
74+
}
75+
}
76+
77+
impl<I: fmt::Debug + fmt::Display> std::error::Error for VerboseError<I> {}
78+
79+
impl From<VerboseError<&[u8]>> for VerboseError<Vec<u8>> {
80+
fn from(value: VerboseError<&[u8]>) -> Self {
81+
VerboseError {
82+
errors: value
83+
.errors
84+
.into_iter()
85+
.map(|(i, e)| (i.to_owned(), e))
86+
.collect(),
87+
}
88+
}
89+
}
90+
91+
impl From<VerboseError<&str>> for VerboseError<String> {
92+
fn from(value: VerboseError<&str>) -> Self {
93+
VerboseError {
94+
errors: value
95+
.errors
96+
.into_iter()
97+
.map(|(i, e)| (i.to_owned(), e))
98+
.collect(),
99+
}
100+
}
101+
}
102+
103+
impl<I> ErrorConvert<VerboseError<I>> for VerboseError<(I, usize)> {
104+
fn convert(self) -> VerboseError<I> {
105+
VerboseError {
106+
errors: self.errors.into_iter().map(|(i, e)| (i.0, e)).collect(),
107+
}
108+
}
109+
}
110+
111+
impl<I> ErrorConvert<VerboseError<(I, usize)>> for VerboseError<I> {
112+
fn convert(self) -> VerboseError<(I, usize)> {
113+
VerboseError {
114+
errors: self.errors.into_iter().map(|(i, e)| ((i, 0), e)).collect(),
115+
}
116+
}
117+
}
118+
119+
/// Transforms a `VerboseError` into a trace with input position information
120+
///
121+
/// The errors contain references to input data that must come from `input`,
122+
/// because nom calculates byte offsets between them
123+
pub fn convert_error<I: core::ops::Deref<Target = str>>(input: I, e: VerboseError<I>) -> String {
124+
use nom::Offset;
125+
use std::fmt::Write;
126+
127+
let mut result = String::new();
128+
129+
for (i, (substring, kind)) in e.errors.iter().enumerate() {
130+
let offset = input.offset(substring);
131+
132+
if input.is_empty() {
133+
match kind {
134+
VerboseErrorKind::Char(c) => {
135+
write!(&mut result, "{}: expected '{}', got empty input\n\n", i, c)
136+
}
137+
VerboseErrorKind::Context(s) => write!(&mut result, "{}: in {}, got empty input\n\n", i, s),
138+
VerboseErrorKind::Nom(e) => write!(&mut result, "{}: in {:?}, got empty input\n\n", i, e),
139+
}
140+
} else {
141+
let prefix = &input.as_bytes()[..offset];
142+
143+
// Count the number of newlines in the first `offset` bytes of input
144+
let line_number = prefix.iter().filter(|&&b| b == b'\n').count() + 1;
145+
146+
// Find the line that includes the subslice:
147+
// Find the *last* newline before the substring starts
148+
let line_begin = prefix
149+
.iter()
150+
.rev()
151+
.position(|&b| b == b'\n')
152+
.map(|pos| offset - pos)
153+
.unwrap_or(0);
154+
155+
// Find the full line after that newline
156+
let line = input[line_begin..]
157+
.lines()
158+
.next()
159+
.unwrap_or(&input[line_begin..])
160+
.trim_end();
161+
162+
// The (1-indexed) column number is the offset of our substring into that line
163+
let column_number = line.offset(substring) + 1;
164+
165+
match kind {
166+
VerboseErrorKind::Char(c) => {
167+
if let Some(actual) = substring.chars().next() {
168+
write!(
169+
&mut result,
170+
"{i}: at line {line_number}:\n\
171+
{line}\n\
172+
{caret:>column$}\n\
173+
expected '{expected}', found {actual}\n\n",
174+
i = i,
175+
line_number = line_number,
176+
line = line,
177+
caret = '^',
178+
column = column_number,
179+
expected = c,
180+
actual = actual,
181+
)
182+
} else {
183+
write!(
184+
&mut result,
185+
"{i}: at line {line_number}:\n\
186+
{line}\n\
187+
{caret:>column$}\n\
188+
expected '{expected}', got end of input\n\n",
189+
i = i,
190+
line_number = line_number,
191+
line = line,
192+
caret = '^',
193+
column = column_number,
194+
expected = c,
195+
)
196+
}
197+
}
198+
VerboseErrorKind::Context(s) => write!(
199+
&mut result,
200+
"{i}: at line {line_number}, in {context}:\n\
201+
{line}\n\
202+
{caret:>column$}\n\n",
203+
i = i,
204+
line_number = line_number,
205+
context = s,
206+
line = line,
207+
caret = '^',
208+
column = column_number,
209+
),
210+
VerboseErrorKind::Nom(e) => write!(
211+
&mut result,
212+
"{i}: at line {line_number}, in {nom_err:?}:\n\
213+
{line}\n\
214+
{caret:>column$}\n\n",
215+
i = i,
216+
line_number = line_number,
217+
nom_err = e,
218+
line = line,
219+
caret = '^',
220+
column = column_number,
221+
),
222+
}
223+
}
224+
// Because `write!` to a `String` is infallible, this `unwrap` is fine.
225+
.unwrap();
226+
}
227+
228+
result
229+
}
230+
231+
#[test]
232+
fn convert_error_panic() {
233+
use nom::character::complete::char;
234+
use nom::IResult;
235+
236+
let input = "";
237+
238+
let _result: IResult<_, _, VerboseError<&str>> = char('x')(input);
239+
}
240+
241+
#[test]
242+
fn issue_1027_convert_error_panic_nonempty() {
243+
use nom::character::complete::char;
244+
use nom::sequence::pair;
245+
use nom::Err;
246+
use nom::IResult;
247+
use nom::Parser;
248+
249+
let input = "a";
250+
251+
let result: IResult<_, _, VerboseError<&str>> = pair(char('a'), char('b')).parse(input);
252+
let err = match result.unwrap_err() {
253+
Err::Error(e) => e,
254+
_ => unreachable!(),
255+
};
256+
257+
let msg = convert_error(input, err);
258+
assert_eq!(
259+
msg,
260+
"0: at line 1:\na\n ^\nexpected \'b\', got end of input\n\n"
261+
);
262+
}

0 commit comments

Comments
 (0)