Skip to content

Commit 668d2df

Browse files
wip: Closer to pretty display error info
1 parent 1d0e670 commit 668d2df

File tree

4 files changed

+447
-48
lines changed

4 files changed

+447
-48
lines changed

keyvalues-parser/fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,10 @@ name = "parse"
2424
path = "fuzz_targets/parse.rs"
2525
test = false
2626
doc = false
27+
28+
[[bin]]
29+
name = "error_invariants"
30+
path = "fuzz_targets/error_invariants.rs"
31+
test = false
32+
doc = false
33+
bench = false
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#![no_main]
2+
3+
use keyvalues_parser::Vdf;
4+
use libfuzzer_sys::fuzz_target;
5+
6+
fuzz_target!(|text: &str| {
7+
if let Err(err) = Vdf::parse(text) {
8+
// Lots of fiddly logic in displaying that can panic
9+
err.to_string();
10+
11+
// The error snippet should match the original text sliced using the error span
12+
let from_orig = err.index_span().slice(text);
13+
let from_snippet = err.error_snippet();
14+
assert_eq!(from_orig, from_snippet);
15+
}
16+
});

keyvalues-parser/src/error.rs

Lines changed: 293 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
//! All error information for parsing and rendering
22
3-
use std::fmt;
3+
// TODO: move most of this into a `parse` module?
4+
5+
use std::{
6+
fmt::{self, Write},
7+
ops::{RangeFrom, RangeInclusive},
8+
};
49

510
/// An alias for `Result` with an [`RenderError`]
611
pub type RenderResult<T> = std::result::Result<T, RenderError>;
@@ -32,68 +37,328 @@ impl fmt::Display for RenderError {
3237

3338
impl std::error::Error for RenderError {}
3439

35-
/// An alias for `Result` with an [`Error`]
40+
/// An alias for `Result` with a [`ParseError`]
3641
pub type ParseResult<T> = std::result::Result<T, ParseError>;
3742

3843
#[derive(Clone, Debug)]
3944
pub struct ParseError {
40-
pub kind: ParseErrorKind,
41-
pub span: Span,
42-
line: String,
45+
pub(crate) inner: ParseErrorInner,
46+
pub(crate) index_span: Span<usize>,
47+
pub(crate) display_span: Span<LineCol>,
48+
pub(crate) lines: String,
49+
pub(crate) lines_start: usize,
50+
}
51+
52+
impl ParseError {
53+
pub fn inner(&self) -> ParseErrorInner {
54+
self.inner
55+
}
56+
57+
/// The span indicating where the error is in the original text
58+
///
59+
/// # Example
60+
///
61+
/// ```
62+
/// # use keyvalues_parser::Vdf;
63+
/// let vdf_text = "key value >:V extra bytes";
64+
/// let err = Vdf::parse(vdf_text).unwrap_err();
65+
/// let error_snippet = err.index_span().slice(vdf_text);
66+
/// assert_eq!(error_snippet, ">:V extra bytes");
67+
/// ```
68+
pub fn index_span(&self) -> Span<usize> {
69+
self.index_span.clone()
70+
}
71+
72+
pub fn line_col_span(&self) -> Span<LineCol> {
73+
self.display_span.clone()
74+
}
75+
76+
pub fn lines(&self) -> &str {
77+
&self.lines
78+
}
79+
80+
pub fn error_snippet(&self) -> &str {
81+
let (mut start, end) = self.index_span.clone().into_inner();
82+
start -= self.lines_start;
83+
match end {
84+
Some(mut end) => {
85+
end -= self.lines_start;
86+
&self.lines[start..=end]
87+
}
88+
None => &self.lines[start..],
89+
}
90+
}
4391
}
4492

93+
// TODO: we could avoid virtually all of the allocations done in here
94+
// TODO: could use loooots of wrappers to clean up the display code
4595
impl fmt::Display for ParseError {
4696
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47-
todo!();
97+
let Self {
98+
inner,
99+
display_span,
100+
lines,
101+
..
102+
} = self;
103+
let (display_start, display_end) = display_span.clone().into_inner();
104+
105+
writeln!(f, "error: {inner}")?;
106+
writeln!(f, "at: {display_span}")?;
107+
108+
let mut lines_iter = lines.lines().zip(display_start.line..).peekable();
109+
while let Some((line, line_idx)) = lines_iter.next() {
110+
let line = line.replace('\n', " ").replace('\r', " ");
111+
let line_idx_str = line_idx.to_string();
112+
writeln!(f, "{line_idx_str} | {line}")?;
113+
114+
let on_start_line = line_idx == display_start.line;
115+
let num_before = if on_start_line {
116+
display_start.col.saturating_sub(1)
117+
} else {
118+
0
119+
};
120+
let padding_before = on_start_line
121+
.then(|| " ".repeat(num_before))
122+
.unwrap_or_default();
123+
124+
let (num_after, append_extra_arrow) = if let Some(display_end) = display_end {
125+
let num_after = line.len().checked_sub(display_end.col).unwrap();
126+
(num_after, false)
127+
} else {
128+
let is_last_line = lines_iter.peek().is_none();
129+
(0, is_last_line)
130+
};
131+
132+
let num_arrows = line.len().checked_sub(num_before + num_after).unwrap()
133+
+ append_extra_arrow as usize;
134+
let arrows = "^".repeat(num_arrows);
135+
136+
let blank_idx = " ".repeat(line_idx_str.len());
137+
138+
writeln!(f, "{blank_idx} | {padding_before}{arrows}")?;
139+
}
140+
141+
Ok(())
48142
}
49143
}
50144

51145
impl std::error::Error for ParseError {}
52146

53147
/// Errors encountered while parsing VDF text
54-
#[derive(Clone, Debug, PartialEq, Eq)]
55-
pub enum ParseErrorKind {
148+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149+
pub enum ParseErrorInner {
150+
/// Indicates that there were significant bytes found after the top-level pair
151+
///
152+
/// # Example
153+
///
154+
/// ```
155+
/// # use keyvalues_parser::{Vdf, error::ParseErrorInner};
156+
/// let err = Vdf::parse("key value >:V extra bytes").unwrap_err();
157+
/// assert_eq!(err.inner(), ParseErrorInner::LingeringBytes);
158+
/// # print!("{err}");
159+
/// let expected = r#"
160+
/// error: Found bytes after the top-level pair
161+
/// at: 1:11 to the end of input
162+
/// 1 | key value >:V extra bytes
163+
/// | ^^^^^^^^^^^^^^^^
164+
/// "#.trim_start();
165+
/// assert_eq!(err.to_string(), expected);
166+
/// ```
56167
LingeringBytes,
57-
InvalidMacro,
58-
MissingTopLevelPair,
168+
/// There was required whitespace that wasn't present
169+
///
170+
/// There are very few places where whitespace is strictly _required_
171+
///
172+
/// # Example
173+
///
174+
/// ```
175+
/// # use keyvalues_parser::{Vdf, error::ParseErrorInner};
176+
/// let err = Vdf::parse("#baseBAD").unwrap_err();
177+
/// assert_eq!(err.inner(), ParseErrorInner::ExpectedWhitespace);
178+
/// # print!("{err}");
179+
/// let expected = r#"
180+
/// error: Expected whitespace
181+
/// at: 1:6
182+
/// 1 | #baseBAD
183+
/// | ^
184+
/// "#.trim_start();
185+
/// assert_eq!(err.to_string(), expected);
186+
/// ```
187+
ExpectedWhitespace,
188+
/// The required top-level key-value pair was missing
189+
///
190+
/// # Example
191+
///
192+
/// ```
193+
/// # use keyvalues_parser::{Vdf, error::ParseErrorInner};
194+
/// let err = Vdf::parse("#base robot_standard.pop").unwrap_err();
195+
/// assert_eq!(err.inner(), ParseErrorInner::ExpectedNewlineAfterMacro);
196+
/// # print!("{err}");
197+
/// let expected = r#"
198+
/// error: Expected newline after macro
199+
/// at: 1:25 to the end of input
200+
/// 1 | #base robot_standard.pop
201+
/// | ^
202+
/// "#.trim_start();
203+
/// assert_eq!(err.to_string(), expected);
204+
/// ```
205+
ExpectedNewlineAfterMacro,
206+
/// Encountered the end of input while parsing a string
207+
///
208+
/// # Example
209+
///
210+
/// ```
211+
/// # use keyvalues_parser::{Vdf, error::ParseErrorInner};
212+
/// let err = Vdf::parse("key \"incomplete ...").unwrap_err();
213+
/// assert_eq!(err.inner(), ParseErrorInner::EoiParsingString);
214+
/// # print!("{err}");
215+
/// let expected = r#"
216+
/// error: Encountered the end of input while parsing a string
217+
/// at: 1:5 to the end of input
218+
/// 1 | key "incomplete ...
219+
/// | ^^^^^^^^^^^^^^^^
220+
/// "#.trim_start();
221+
/// assert_eq!(err.to_string(), expected);
222+
/// ```
59223
EoiParsingString,
60224
ExpectedUnquotedString,
61-
InvalidEscapedCharacter,
225+
/// Encountered an invalid escape character in a quoted string
226+
///
227+
/// # Example
228+
///
229+
/// ```
230+
/// # use keyvalues_parser::{Vdf, error::ParseErrorInner};
231+
/// let err = Vdf::parse(r#"key "invalid -> \u""#).unwrap_err();
232+
/// # print!("{err}");
233+
/// let expected = r#"
234+
/// error: Invalid escaped string character \u
235+
/// at: 1:17 to 1:18
236+
/// 1 | key "invalid -> \u"
237+
/// | ^^
238+
/// "#.trim_start();
239+
/// assert_eq!(err.to_string(), expected);
240+
/// ```
241+
InvalidEscapedCharacter {
242+
invalid: char,
243+
},
244+
/// Encountered the end of input while parsing a map
245+
///
246+
/// # Example
247+
///
248+
/// ```
249+
/// # use keyvalues_parser::{Vdf, error::ParseErrorInner};
250+
/// let err = Vdf::parse("key {\n foo {}").unwrap_err();
251+
/// assert_eq!(err.inner(), ParseErrorInner::EoiParsingMap);
252+
/// # print!("{err}");
253+
/// let expected = r#"
254+
/// error: Encountered the end of input while parsing a map
255+
/// at: 1:5 to the end of input
256+
/// 1 | key {
257+
/// | ^
258+
/// 2 | foo {}
259+
/// | ^^^^^^^^^
260+
/// "#.trim_start();
261+
/// assert_eq!(err.to_string(), expected);
262+
/// ```
62263
EoiParsingMap,
264+
// TODO: store the invalid character
63265
InvalidComment,
64266
}
65267

66-
impl fmt::Display for ParseErrorKind {
268+
impl fmt::Display for ParseErrorInner {
67269
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68270
match self {
69-
Self::LingeringBytes => f.write_str("Bytes remained after parsed pair"),
70-
Self::InvalidMacro => f.write_str("Invalid macro"),
71-
Self::MissingTopLevelPair => f.write_str("Missing top-level pair"),
271+
Self::LingeringBytes => f.write_str("Found bytes after the top-level pair"),
272+
Self::ExpectedWhitespace => f.write_str("Expected whitespace"),
273+
Self::ExpectedNewlineAfterMacro => f.write_str("Expected newline after macro"),
72274
Self::EoiParsingString => {
73-
f.write_str("Encountered the end-of-input while pasing a string")
275+
f.write_str("Encountered the end of input while parsing a string")
74276
}
75277
Self::ExpectedUnquotedString => f.write_str("Expected unquoted string"),
76-
Self::InvalidEscapedCharacter => f.write_str("Invalid escaped string character"),
77-
Self::EoiParsingMap => f.write_str("Encountered the end-of-input while pasing a map"),
278+
Self::InvalidEscapedCharacter { invalid } => {
279+
write!(f, "Invalid escaped string character \\{invalid}")
280+
}
281+
Self::EoiParsingMap => f.write_str("Encountered the end of input while parsing a map"),
78282
Self::InvalidComment => f.write_str("Invalid character in comment"),
79283
}
80284
}
81285
}
82286

83-
#[derive(Clone, Debug)]
84-
pub enum Span {
85-
Single(usize),
86-
Run { index: usize, len: usize },
287+
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
288+
pub struct LineCol {
289+
pub line: usize,
290+
pub col: usize,
87291
}
88292

89-
impl Span {
90-
pub(crate) fn run_with_len(index: usize, len: usize) -> Self {
91-
Span::Run { index, len }
293+
impl Default for LineCol {
294+
fn default() -> Self {
295+
Self { line: 1, col: 1 }
92296
}
93297
}
298+
impl fmt::Display for LineCol {
299+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300+
let Self { line, col } = self;
301+
write!(f, "{line}:{col}")
302+
}
303+
}
304+
305+
#[derive(Clone, Debug, PartialEq, Eq)]
306+
pub enum Span<T> {
307+
Inclusive(RangeInclusive<T>),
308+
ToEoi(RangeFrom<T>),
309+
}
310+
311+
impl<T> Span<T> {
312+
pub fn new(start: T, maybe_end: Option<T>) -> Self {
313+
match maybe_end {
314+
Some(end) => Self::Inclusive(start..=end),
315+
None => Self::ToEoi(start..),
316+
}
317+
}
94318

95-
impl From<usize> for Span {
96-
fn from(index: usize) -> Self {
97-
Self::Single(index)
319+
pub fn into_inner(self) -> (T, Option<T>) {
320+
match self {
321+
Self::Inclusive(r) => {
322+
let (start, end) = r.into_inner();
323+
(start, Some(end))
324+
}
325+
Self::ToEoi(r) => (r.start, None),
326+
}
327+
}
328+
}
329+
330+
impl Span<usize> {
331+
pub fn slice<'span, 'text>(&'span self, s: &'text str) -> &'text str {
332+
match self.to_owned() {
333+
Self::Inclusive(r) => &s[r],
334+
Self::ToEoi(r) => &s[r],
335+
}
336+
}
337+
}
338+
339+
impl<T> From<RangeInclusive<T>> for Span<T> {
340+
fn from(r: RangeInclusive<T>) -> Self {
341+
Self::Inclusive(r)
342+
}
343+
}
344+
345+
impl<T> From<RangeFrom<T>> for Span<T> {
346+
fn from(r: RangeFrom<T>) -> Self {
347+
Self::ToEoi(r)
348+
}
349+
}
350+
351+
impl<T: fmt::Display + PartialEq> fmt::Display for Span<T> {
352+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353+
match self {
354+
Self::Inclusive(r) => {
355+
if r.start() == r.end() {
356+
write!(f, "{}", r.start())
357+
} else {
358+
write!(f, "{} to {}", r.start(), r.end())
359+
}
360+
}
361+
Self::ToEoi(r) => write!(f, "{} to the end of input", r.start),
362+
}
98363
}
99364
}

0 commit comments

Comments
 (0)