1use ariadne::{Color, Label, Report, ReportKind, Source};
4use chumsky::error::Simple;
5
6use crate::span::Span;
7use crate::token::Token;
8
9pub struct ParseResult<T> {
12 pub ast: Option<T>,
13 pub errors: Vec<ParseError>,
14}
15
16#[derive(Debug, Clone)]
18pub struct ParseError {
19 pub span: Span,
20 pub message: String,
21 pub expected: Vec<String>,
22 pub found: Option<String>,
23 pub label: Option<&'static str>,
24}
25
26impl ParseError {
27 pub fn from_chumsky(err: Simple<Token>) -> Self {
29 let span = err.span();
30 let message = format!("{err}");
31 let expected: Vec<String> = err
32 .expected()
33 .map(|e| match e {
34 Some(tok) => format!("{tok}"),
35 None => "end of input".to_string(),
36 })
37 .collect();
38 let found = err.found().map(|t| format!("{t}"));
39 let label = err.label();
40
41 Self {
42 span,
43 message,
44 expected,
45 found,
46 label,
47 }
48 }
49
50 pub fn render(&self, source_name: &str, source: &str) -> String {
52 let mut buf = Vec::new();
53
54 let msg = if !self.expected.is_empty() {
55 let expected_str = self.expected.join(", ");
56 match &self.found {
57 Some(found) => format!("expected {expected_str}, found {found}"),
58 None => format!("expected {expected_str}"),
59 }
60 } else {
61 self.message.clone()
62 };
63
64 let label_msg = match self.label {
65 Some(label) => format!("in {label}"),
66 None => msg.clone(),
67 };
68
69 Report::build(ReportKind::Error, source_name, self.span.start)
70 .with_message(&msg)
71 .with_label(
72 Label::new((source_name, self.span.clone()))
73 .with_message(label_msg)
74 .with_color(Color::Red),
75 )
76 .finish()
77 .write((source_name, Source::from(source)), &mut buf)
78 .unwrap();
79
80 String::from_utf8(buf).unwrap()
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn render_error() {
90 let err = ParseError {
91 span: 6..7,
92 message: "unexpected token".to_string(),
93 expected: vec!["(".to_string()],
94 found: Some(")".to_string()),
95 label: None,
96 };
97 let rendered = err.render("test.cyp", "MATCH ) foo");
98 assert!(rendered.contains("test.cyp"));
99 assert!(rendered.contains("expected"));
100 }
101
102 #[test]
103 fn render_with_label() {
104 let err = ParseError {
105 span: 0..5,
106 message: "unexpected".to_string(),
107 expected: vec![],
108 found: None,
109 label: Some("match pattern"),
110 };
111 let rendered = err.render("query.cyp", "XXXXX (n)");
112 assert!(rendered.contains("match pattern"));
113 }
114}