Skip to content

Commit 8299a16

Browse files
wip: Closer to pretty display error info
1 parent 1d0e670 commit 8299a16

File tree

4 files changed

+436
-48
lines changed

4 files changed

+436
-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: 282 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,317 @@ 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+
pub fn index_span(&self) -> Span<usize> {
58+
self.index_span.clone()
59+
}
60+
61+
pub fn line_col_span(&self) -> Span<LineCol> {
62+
self.display_span.clone()
63+
}
64+
65+
pub fn lines(&self) -> &str {
66+
&self.lines
67+
}
68+
69+
pub fn error_snippet(&self) -> &str {
70+
let (mut start, end) = self.index_span.clone().into_inner();
71+
start -= self.lines_start;
72+
match end {
73+
Some(mut end) => {
74+
end -= self.lines_start;
75+
&self.lines[start..=end]
76+
}
77+
None => &self.lines[start..],
78+
}
79+
}
4380
}
4481

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

51134
impl std::error::Error for ParseError {}
52135

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

66-
impl fmt::Display for ParseErrorKind {
257+
impl fmt::Display for ParseErrorInner {
67258
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68259
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"),
260+
Self::LingeringBytes => f.write_str("Found bytes after the top-level pair"),
261+
Self::ExpectedWhitespace => f.write_str("Expected whitespace"),
262+
Self::ExpectedNewlineAfterMacro => f.write_str("Expected newline after macro"),
72263
Self::EoiParsingString => {
73-
f.write_str("Encountered the end-of-input while pasing a string")
264+
f.write_str("Encountered the end of input while parsing a string")
74265
}
75266
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"),
267+
Self::InvalidEscapedCharacter { invalid } => {
268+
write!(f, "Invalid escaped string character \\{invalid}")
269+
}
270+
Self::EoiParsingMap => f.write_str("Encountered the end of input while pasing a map"),
78271
Self::InvalidComment => f.write_str("Invalid character in comment"),
79272
}
80273
}
81274
}
82275

83-
#[derive(Clone, Debug)]
84-
pub enum Span {
85-
Single(usize),
86-
Run { index: usize, len: usize },
276+
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
277+
pub struct LineCol {
278+
pub line: usize,
279+
pub col: usize,
87280
}
88281

89-
impl Span {
90-
pub(crate) fn run_with_len(index: usize, len: usize) -> Self {
91-
Span::Run { index, len }
282+
impl Default for LineCol {
283+
fn default() -> Self {
284+
Self { line: 1, col: 1 }
92285
}
93286
}
287+
impl fmt::Display for LineCol {
288+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289+
let Self { line, col } = self;
290+
write!(f, "{line}:{col}")
291+
}
292+
}
293+
294+
#[derive(Clone, Debug, PartialEq, Eq)]
295+
pub enum Span<T> {
296+
Inclusive(RangeInclusive<T>),
297+
ToEoi(RangeFrom<T>),
298+
}
299+
300+
impl<T> Span<T> {
301+
pub fn new(start: T, maybe_end: Option<T>) -> Self {
302+
match maybe_end {
303+
Some(end) => Self::Inclusive(start..=end),
304+
None => Self::ToEoi(start..),
305+
}
306+
}
94307

95-
impl From<usize> for Span {
96-
fn from(index: usize) -> Self {
97-
Self::Single(index)
308+
pub fn into_inner(self) -> (T, Option<T>) {
309+
match self {
310+
Self::Inclusive(r) => {
311+
let (start, end) = r.into_inner();
312+
(start, Some(end))
313+
}
314+
Self::ToEoi(r) => (r.start, None),
315+
}
316+
}
317+
}
318+
319+
impl Span<usize> {
320+
pub fn slice<'span, 'text>(&'span self, s: &'text str) -> &'text str {
321+
match self.to_owned() {
322+
Self::Inclusive(r) => &s[r],
323+
Self::ToEoi(r) => &s[r],
324+
}
325+
}
326+
}
327+
328+
impl<T> From<RangeInclusive<T>> for Span<T> {
329+
fn from(r: RangeInclusive<T>) -> Self {
330+
Self::Inclusive(r)
331+
}
332+
}
333+
334+
impl<T> From<RangeFrom<T>> for Span<T> {
335+
fn from(r: RangeFrom<T>) -> Self {
336+
Self::ToEoi(r)
337+
}
338+
}
339+
340+
impl<T: fmt::Display + PartialEq> fmt::Display for Span<T> {
341+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342+
match self {
343+
Self::Inclusive(r) => {
344+
if r.start() == r.end() {
345+
write!(f, "{}", r.start())
346+
} else {
347+
write!(f, "{} to {}", r.start(), r.end())
348+
}
349+
}
350+
Self::ToEoi(r) => write!(f, "{} to the end of input", r.start),
351+
}
98352
}
99353
}

0 commit comments

Comments
 (0)