1use ariadne::{Color, Label, Report, ReportKind, Source};
2use chumsky::error::Rich;
3use std::ops::Range;
4
5fn is_noise_pattern(s: &str) -> bool {
7 s.contains("whitespace")
8 || s.contains("end of input")
9 || s == "something else"
10 || s.contains("'/'")
11}
12
13fn find_unmatched_opener(source: &str, open: char, close: char) -> Option<usize> {
15 let mut stack = Vec::new();
16 let mut in_string = false;
17
18 for (i, ch) in source.char_indices() {
19 if ch == '"' {
20 in_string = !in_string;
21 } else if !in_string {
22 if ch == open {
23 stack.push(i);
24 } else if ch == close {
25 stack.pop();
26 }
27 }
28 }
29
30 stack.last().copied()
31}
32
33fn find_unclosed_string(source: &str) -> Option<usize> {
35 let mut in_string = false;
36 let mut string_start = 0;
37
38 for (i, ch) in source.char_indices() {
39 if ch == '"' {
40 if !in_string {
41 in_string = true;
42 string_start = i;
43 } else {
44 in_string = false;
45 }
46 }
47 }
48
49 if in_string { Some(string_start) } else { None }
50}
51
52struct DetectedError {
54 message: String,
55 hint: Option<String>,
56 span: Option<Range<usize>>,
57}
58
59fn find_pattern(source: &str, pattern: &str) -> Option<usize> {
61 source.find(pattern)
62}
63
64fn detect_error(source: &str) -> Option<DetectedError> {
66 if let Some(pos) = find_unclosed_string(source) {
68 return Some(DetectedError {
69 message: "unclosed string literal".into(),
70 hint: Some("add a closing '\"' to complete the string".into()),
71 span: Some(pos..pos + 1),
72 });
73 }
74
75 if let Some(pos) = find_unmatched_opener(source, '(', ')') {
77 return Some(DetectedError {
78 message: "unclosed parentheses".into(),
79 hint: Some("add a closing ')' to complete the argument list".into()),
80 span: Some(pos..pos + 1),
81 });
82 }
83
84 if let Some(pos) = find_unmatched_opener(source, '{', '}') {
86 return Some(DetectedError {
87 message: "unclosed block".into(),
88 hint: Some("add a closing '}' to complete the block".into()),
89 span: Some(pos..pos + 1),
90 });
91 }
92
93 if let Some(pos) = find_unmatched_opener(source, '[', ']') {
95 return Some(DetectedError {
96 message: "unclosed list".into(),
97 hint: Some("add a closing ']' to complete the list".into()),
98 span: Some(pos..pos + 1),
99 });
100 }
101
102 if let Some(pos) = find_pattern(source, ": ,") {
104 return Some(DetectedError {
105 message: "missing value after ':'".into(),
106 hint: Some("add a value after the colon".into()),
107 span: Some(pos..pos + 3),
108 });
109 }
110
111 if let Some(pos) = find_pattern(source, ", ,") {
113 return Some(DetectedError {
114 message: "unexpected ','".into(),
115 hint: Some("remove the extra comma or add a value".into()),
116 span: Some(pos + 2..pos + 3),
117 });
118 }
119
120 if let Some(pos) = find_pattern(source, "(,") {
122 return Some(DetectedError {
123 message: "unexpected ',' at start of arguments".into(),
124 hint: Some("remove the leading comma".into()),
125 span: Some(pos + 1..pos + 2),
126 });
127 }
128
129 None
130}
131
132fn format_from_expected(found: Option<&char>, expected: &[String]) -> String {
134 let useful: Vec<_> = expected
135 .iter()
136 .filter(|s| !is_noise_pattern(s))
137 .take(3)
138 .collect();
139
140 match found {
141 Some(ch) if useful.is_empty() => format!("unexpected '{}'", ch),
142 Some(ch) => format!("unexpected '{}', expected {}", ch, useful.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" or ")),
143 None if useful.is_empty() => "unexpected end of input".into(),
144 None => format!("unexpected end of input, expected {}", useful.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" or ")),
145 }
146}
147
148pub fn print_parse_errors(filename: &str, source: &str, errors: &[Rich<char>]) {
150 for error in errors {
151 let default_span = error.span().into_range();
152
153 let (msg, hint, span) = if let Some(detected) = detect_error(source) {
155 (detected.message, detected.hint, detected.span.unwrap_or(default_span))
156 } else {
157 let expected: Vec<String> = error.expected().map(|e| format!("{}", e)).collect();
159 (format_from_expected(error.found(), &expected), None, default_span)
160 };
161
162 let mut report = Report::build(ReportKind::Error, filename, span.start)
163 .with_message("Parse error")
164 .with_label(
165 Label::new((filename, span))
166 .with_message(&msg)
167 .with_color(Color::Red),
168 );
169
170 if let Some(hint_msg) = hint {
171 report = report.with_help(hint_msg);
172 }
173
174 report
175 .finish()
176 .print((filename, Source::from(source)))
177 .unwrap();
178 }
179}
180
181pub fn format_parse_errors(filename: &str, source: &str, errors: &[Rich<char>]) -> String {
183 let mut output = Vec::new();
184
185 for error in errors {
186 let default_span = error.span().into_range();
187
188 let (msg, hint, span) = if let Some(detected) = detect_error(source) {
189 (detected.message, detected.hint, detected.span.unwrap_or(default_span))
190 } else {
191 let expected: Vec<String> = error.expected().map(|e| format!("{}", e)).collect();
192 (format_from_expected(error.found(), &expected), None, default_span)
193 };
194
195 let mut report = Report::build(ReportKind::Error, filename, span.start)
196 .with_message("Parse error")
197 .with_label(
198 Label::new((filename, span))
199 .with_message(&msg)
200 .with_color(Color::Red),
201 );
202
203 if let Some(hint_msg) = hint {
204 report = report.with_help(hint_msg);
205 }
206
207 report
208 .finish()
209 .write((filename, Source::from(source)), &mut output)
210 .unwrap();
211 }
212
213 String::from_utf8(output).unwrap()
214}
215
216pub fn format_error_simple(error: &Rich<char>) -> String {
218 let expected: Vec<String> = error
219 .expected()
220 .map(|e| format!("{}", e))
221 .filter(|s| !is_noise_pattern(s))
222 .take(5)
223 .collect();
224
225 match error.found() {
226 Some(ch) if expected.is_empty() => {
227 format!("unexpected '{}' at position {}", ch, error.span().start)
228 }
229 Some(ch) => {
230 format!("unexpected '{}' at position {}, expected {}", ch, error.span().start, expected.join(" or "))
231 }
232 None if expected.is_empty() => {
233 format!("unexpected end of input at position {}", error.span().start)
234 }
235 None => {
236 format!("unexpected end of input at position {}, expected {}", error.span().start, expected.join(" or "))
237 }
238 }
239}