okane_core/parse/
error.rs

1use std::{
2    fmt::Display,
3    ops::{Deref, Range},
4};
5
6use annotate_snippets::{AnnotationKind, Group, 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 = &[
57            Group::with_title(Level::ERROR.primary_title(&message)).element(
58                Snippet::source(&self.input)
59                    .line_start(self.line_start)
60                    .fold(true)
61                    .annotation(AnnotationKind::Primary.span(self.error_span.clone())),
62            ),
63        ];
64        let rendered = self.renderer.render(message);
65        rendered.fmt(f)
66    }
67}
68
69impl std::error::Error for ParseErrorImpl {
70    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71        self.winnow_error
72            .cause()
73            .map(|x| x as &(dyn std::error::Error + 'static))
74    }
75}
76
77/// Computes the line number at the `pos` position of `s`.
78/// If `pos` is outside of `s` or not a UTF-8 boundary, it panics.
79pub(super) fn compute_line_number(s: &str, pos: usize) -> usize {
80    assert!(
81        pos <= s.len(),
82        "cannot compute line_number for out-of-range position"
83    );
84    let (s, _) = s.as_bytes().split_at(pos);
85    1 + s.iter().filter(|x| **x == b'\n').count()
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn compute_line_number_valid_inputs() {
94        assert_eq!(compute_line_number("This\nis\npen", 0), 1);
95        assert_eq!(compute_line_number("This\nis\npen", 1), 1);
96        assert_eq!(compute_line_number("This\nis\npen", 4), 1);
97        assert_eq!(compute_line_number("This\nis\npen", 5), 2);
98        assert_eq!(compute_line_number("This\nis\npen", 7), 2);
99        assert_eq!(compute_line_number("This\nis\npen", 8), 3);
100    }
101
102    #[test]
103    fn compute_line_number_works_on_invalid_utf8_boundary() {
104        assert_eq!(compute_line_number("日本語だよ", 1), 1);
105    }
106
107    #[test]
108    #[should_panic(expected = "cannot compute line_number for")]
109    fn compute_line_number_panics_on_out_of_range_pos() {
110        compute_line_number("hello world", 12);
111    }
112}