okane_core/parse/
error.rs1use 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 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 = &[
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
77pub(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}