okane_core/parse/
error.rs

1use std::{
2    fmt::Display,
3    ops::{Deref, Range},
4};
5
6use annotate_snippets::{Level, Renderer, Snippet};
7use winnow::{
8    error::{ContextError, StrContext},
9    stream::{Location, Offset, Stream},
10    LocatingSlice,
11};
12
13#[derive(Debug, thiserror::Error)]
14#[error(transparent)]
15pub struct ParseError(Box<ParseErrorImpl>);
16
17#[derive(Debug)]
18pub struct ParseErrorImpl {
19    renderer: Renderer,
20    error_span: Range<usize>,
21    input: String,
22    line_start: usize,
23    winnow_error: ContextError,
24}
25
26impl ParseError {
27    /// Create a new instance of ParseError.
28    pub(super) fn new<'i>(
29        renderer: Renderer,
30        initial: &'i str,
31        mut input: LocatingSlice<&'i str>,
32        start: <LocatingSlice<&'i str> as Stream>::Checkpoint,
33        error: ContextError<StrContext>,
34    ) -> Self {
35        let offset = input.offset_from(&start);
36        input.reset(&start);
37        let line_start = compute_line_number(initial, input.current_token_start());
38        // Assume the error span is only for the first `char`.
39        // When we'll implement
40        let end = (offset + 1..)
41            .find(|e| input.is_char_boundary(*e))
42            .unwrap_or(offset);
43        Self(Box::new(ParseErrorImpl {
44            renderer,
45            error_span: offset..end,
46            input: input.deref().to_string(),
47            line_start,
48            winnow_error: error,
49        }))
50    }
51}
52
53impl Display for ParseErrorImpl {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        let message = self.winnow_error.to_string();
56        let message = Level::Error.title(&message).snippet(
57            Snippet::source(&self.input)
58                .line_start(self.line_start)
59                .fold(true)
60                .annotation(Level::Error.span(self.error_span.clone())),
61        );
62        let rendered = self.renderer.render(message);
63        rendered.fmt(f)
64    }
65}
66
67impl std::error::Error for ParseErrorImpl {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        self.winnow_error
70            .cause()
71            .map(|x| x as &(dyn std::error::Error + 'static))
72    }
73}
74
75/// Computes the line number at the `pos` position of `s`.
76/// If `pos` is outside of `s` or not a UTF-8 boundary, it panics.
77pub(super) fn compute_line_number(s: &str, pos: usize) -> usize {
78    assert!(
79        pos <= s.len(),
80        "cannot compute line_number for out-of-range position"
81    );
82    let (s, _) = s.as_bytes().split_at(pos);
83    1 + s.iter().filter(|x| **x == b'\n').count()
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn compute_line_number_valid_inputs() {
92        assert_eq!(compute_line_number("This\nis\npen", 0), 1);
93        assert_eq!(compute_line_number("This\nis\npen", 1), 1);
94        assert_eq!(compute_line_number("This\nis\npen", 4), 1);
95        assert_eq!(compute_line_number("This\nis\npen", 5), 2);
96        assert_eq!(compute_line_number("This\nis\npen", 7), 2);
97        assert_eq!(compute_line_number("This\nis\npen", 8), 3);
98    }
99
100    #[test]
101    fn compute_line_number_works_on_invalid_utf8_boundary() {
102        assert_eq!(compute_line_number("日本語だよ", 1), 1);
103    }
104
105    #[test]
106    #[should_panic(expected = "cannot compute line_number for")]
107    fn compute_line_number_panics_on_out_of_range_pos() {
108        compute_line_number("hello world", 12);
109    }
110}