atoxide_parser/chumsky/
error.rs1use ariadne::{Color, Label, Report, ReportKind, Source};
4use atoxide_lexer::{Span, Token};
5use chumsky::error::{Rich, RichPattern, RichReason};
6use chumsky::span::SimpleSpan;
7use std::fmt;
8use std::ops::Range;
9
10#[derive(Debug, Clone)]
12pub struct ParseError {
13 pub span: Span,
15 pub message: String,
17 pub expected: Vec<String>,
19 pub found: Option<String>,
21 pub help: Option<String>,
23}
24
25impl ParseError {
26 pub fn from_rich(error: &Rich<'_, Token, SimpleSpan>, _source: &str) -> Self {
28 let error_span = error.span();
29 let span = Span::new(error_span.start, error_span.end, 1, 1);
30
31 let expected: Vec<String> = error
32 .expected()
33 .map(|e| match e {
34 RichPattern::Token(t) => format!("{:?}", t.kind),
35 RichPattern::Label(l) => l.to_string(),
36 RichPattern::EndOfInput => "end of input".to_string(),
37 _ => "unknown".to_string(),
38 })
39 .collect();
40
41 let found = error.found().map(|t| format!("{:?}", t.kind));
42
43 let message = match error.reason() {
44 RichReason::ExpectedFound { .. } => {
45 if expected.is_empty() {
46 "unexpected token".to_string()
47 } else if expected.len() == 1 {
48 format!("expected {}", expected[0])
49 } else {
50 format!("expected one of: {}", expected.join(", "))
51 }
52 }
53 RichReason::Custom(msg) => msg.to_string(),
54 };
55
56 ParseError {
57 span,
58 message,
59 expected,
60 found,
61 help: None,
62 }
63 }
64
65 #[allow(dead_code)]
67 pub fn with_help(mut self, help: impl Into<String>) -> Self {
68 self.help = Some(help.into());
69 self
70 }
71}
72
73impl fmt::Display for ParseError {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(f, "{}", self.message)?;
76 if let Some(found) = &self.found {
77 write!(f, ", found {}", found)?;
78 }
79 Ok(())
80 }
81}
82
83impl std::error::Error for ParseError {}
84
85pub fn format_errors(errors: &[ParseError], source: &str, filename: &str) -> String {
87 let mut output = Vec::new();
88
89 for error in errors {
90 let report = Report::<(&str, Range<usize>)>::build(
91 ReportKind::Error,
92 (filename, error.span.start..error.span.end),
93 )
94 .with_message(&error.message)
95 .with_label(
96 Label::new((filename, error.span.start..error.span.end))
97 .with_message(if let Some(found) = &error.found {
98 format!("found {} here", found)
99 } else {
100 error.message.clone()
101 })
102 .with_color(Color::Red),
103 );
104
105 let report = if let Some(help) = &error.help {
106 report.with_help(help)
107 } else if !error.expected.is_empty() {
108 report.with_help(format!("expected: {}", error.expected.join(", ")))
109 } else {
110 report
111 };
112
113 let report = report.finish();
114
115 let mut buf = Vec::new();
116 report
117 .write((filename, Source::from(source)), &mut buf)
118 .unwrap();
119
120 output.extend(buf);
121 }
122
123 String::from_utf8(output).unwrap_or_else(|_| "Error formatting failed".to_string())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_error_display() {
132 let error = ParseError {
133 span: Span::new(0, 5, 1, 1),
134 message: "expected identifier".to_string(),
135 expected: vec!["NAME".to_string()],
136 found: Some("Number".to_string()),
137 help: None,
138 };
139
140 let display = format!("{}", error);
141 assert!(display.contains("expected identifier"));
142 assert!(display.contains("Number"));
143 }
144
145 #[test]
146 fn test_format_errors() {
147 let errors = vec![ParseError {
148 span: Span::new(0, 5, 1, 1),
149 message: "unexpected token".to_string(),
150 expected: vec!["module".to_string()],
151 found: Some("Number".to_string()),
152 help: None,
153 }];
154
155 let output = format_errors(&errors, "12345\n", "test.ato");
156 assert!(output.contains("unexpected token"));
157 }
158}