okane_core/parse/
error.rs1use 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 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 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
75pub(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}