1use ariadne::{Color, Label, Report, ReportKind, Source};
2pub use 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 chars: Vec<char> = source.chars().collect();
17 let mut i = 0;
18 let mut in_double_string = false;
19 let mut in_single_string = false;
20
21 let byte_offsets: Vec<usize> = source.char_indices().map(|(pos, _)| pos).collect();
23
24 while i < chars.len() {
25 let ch = chars[i];
26 let in_string = in_double_string || in_single_string;
27
28 if in_string && ch == '\\' {
29 i += 2;
31 continue;
32 }
33
34 if ch == '"' && !in_single_string {
35 in_double_string = !in_double_string;
36 } else if ch == '\'' && !in_double_string {
37 in_single_string = !in_single_string;
38 } else if !in_string {
39 if ch == open {
40 stack.push(byte_offsets[i]);
41 } else if ch == close {
42 stack.pop();
43 }
44 }
45
46 i += 1;
47 }
48
49 stack.last().copied()
50}
51
52fn find_unclosed_string(source: &str) -> Option<usize> {
54 let chars: Vec<char> = source.chars().collect();
55 let byte_offsets: Vec<usize> = source.char_indices().map(|(pos, _)| pos).collect();
56 let mut i = 0;
57 let mut in_double_string = false;
58 let mut in_single_string = false;
59 let mut string_start = 0;
60
61 while i < chars.len() {
62 let ch = chars[i];
63 let in_string = in_double_string || in_single_string;
64
65 if in_string && ch == '\\' {
66 i += 2;
68 continue;
69 }
70
71 if ch == '"' && !in_single_string {
72 if !in_double_string {
73 in_double_string = true;
74 string_start = byte_offsets[i];
75 } else {
76 in_double_string = false;
77 }
78 } else if ch == '\'' && !in_double_string {
79 if !in_single_string {
80 in_single_string = true;
81 string_start = byte_offsets[i];
82 } else {
83 in_single_string = false;
84 }
85 }
86
87 i += 1;
88 }
89
90 if in_double_string || in_single_string {
91 Some(string_start)
92 } else {
93 None
94 }
95}
96
97struct DetectedError {
99 message: String,
100 hint: Option<String>,
101 span: Option<Range<usize>>,
102}
103
104fn find_pattern(source: &str, pattern: &str) -> Option<usize> {
106 source.find(pattern)
107}
108
109fn detect_error(source: &str) -> Option<DetectedError> {
111 if let Some(pos) = find_unclosed_string(source) {
113 return Some(DetectedError {
114 message: "unclosed string literal".into(),
115 hint: Some("add a closing '\"' to complete the string".into()),
116 span: Some(pos..pos + 1),
117 });
118 }
119
120 if let Some(pos) = find_unmatched_opener(source, '(', ')') {
122 return Some(DetectedError {
123 message: "unclosed parentheses".into(),
124 hint: Some("add a closing ')' to complete the argument list".into()),
125 span: Some(pos..pos + 1),
126 });
127 }
128
129 if let Some(pos) = find_unmatched_opener(source, '{', '}') {
131 return Some(DetectedError {
132 message: "unclosed block".into(),
133 hint: Some("add a closing '}' to complete the block".into()),
134 span: Some(pos..pos + 1),
135 });
136 }
137
138 if let Some(pos) = find_unmatched_opener(source, '[', ']') {
140 return Some(DetectedError {
141 message: "unclosed list".into(),
142 hint: Some("add a closing ']' to complete the list".into()),
143 span: Some(pos..pos + 1),
144 });
145 }
146
147 if let Some(pos) = find_pattern(source, ": ,") {
149 return Some(DetectedError {
150 message: "missing value after ':'".into(),
151 hint: Some("add a value after the colon".into()),
152 span: Some(pos..pos + 3),
153 });
154 }
155
156 if let Some(pos) = find_pattern(source, ", ,") {
158 return Some(DetectedError {
159 message: "unexpected ','".into(),
160 hint: Some("remove the extra comma or add a value".into()),
161 span: Some(pos + 2..pos + 3),
162 });
163 }
164
165 if let Some(pos) = find_pattern(source, "(,") {
167 return Some(DetectedError {
168 message: "unexpected ',' at start of arguments".into(),
169 hint: Some("remove the leading comma".into()),
170 span: Some(pos + 1..pos + 2),
171 });
172 }
173
174 None
175}
176
177fn format_from_expected(found: Option<&char>, expected: &[String]) -> String {
179 let useful: Vec<_> = expected
180 .iter()
181 .filter(|s| !is_noise_pattern(s))
182 .take(3)
183 .collect();
184
185 match found {
186 Some(ch) if useful.is_empty() => format!("unexpected '{}'", ch),
187 Some(ch) => format!(
188 "unexpected '{}', expected {}",
189 ch,
190 useful
191 .iter()
192 .map(|s| s.as_str())
193 .collect::<Vec<_>>()
194 .join(" or ")
195 ),
196 None if useful.is_empty() => "unexpected end of input".into(),
197 None => format!(
198 "unexpected end of input, expected {}",
199 useful
200 .iter()
201 .map(|s| s.as_str())
202 .collect::<Vec<_>>()
203 .join(" or ")
204 ),
205 }
206}
207
208pub fn print_parse_errors(filename: &str, source: &str, errors: &[Rich<char>]) {
210 for error in errors {
211 let default_span = error.span().into_range();
212
213 let (msg, hint, span) = if let Some(detected) = detect_error(source) {
215 (
216 detected.message,
217 detected.hint,
218 detected.span.unwrap_or(default_span),
219 )
220 } else {
221 let expected: Vec<String> = error.expected().map(|e| format!("{}", e)).collect();
223 (
224 format_from_expected(error.found(), &expected),
225 None,
226 default_span,
227 )
228 };
229
230 let mut report = Report::build(ReportKind::Error, filename, span.start)
231 .with_message("Parse error")
232 .with_label(
233 Label::new((filename, span))
234 .with_message(&msg)
235 .with_color(Color::Red),
236 );
237
238 if let Some(hint_msg) = hint {
239 report = report.with_help(hint_msg);
240 }
241
242 report
243 .finish()
244 .print((filename, Source::from(source)))
245 .unwrap();
246 }
247}
248
249pub fn format_parse_errors(filename: &str, source: &str, errors: &[Rich<char>]) -> String {
251 let mut output = Vec::new();
252
253 for error in errors {
254 let default_span = error.span().into_range();
255
256 let (msg, hint, span) = if let Some(detected) = detect_error(source) {
257 (
258 detected.message,
259 detected.hint,
260 detected.span.unwrap_or(default_span),
261 )
262 } else {
263 let expected: Vec<String> = error.expected().map(|e| format!("{}", e)).collect();
264 (
265 format_from_expected(error.found(), &expected),
266 None,
267 default_span,
268 )
269 };
270
271 let mut report = Report::build(ReportKind::Error, filename, span.start)
272 .with_message("Parse error")
273 .with_label(
274 Label::new((filename, span))
275 .with_message(&msg)
276 .with_color(Color::Red),
277 );
278
279 if let Some(hint_msg) = hint {
280 report = report.with_help(hint_msg);
281 }
282
283 report
284 .finish()
285 .write((filename, Source::from(source)), &mut output)
286 .unwrap();
287 }
288
289 String::from_utf8(output).unwrap()
290}
291
292pub fn format_error_simple(error: &Rich<char>) -> String {
294 let expected: Vec<String> = error
295 .expected()
296 .map(|e| format!("{}", e))
297 .filter(|s| !is_noise_pattern(s))
298 .take(5)
299 .collect();
300
301 match error.found() {
302 Some(ch) if expected.is_empty() => {
303 format!("unexpected '{}' at position {}", ch, error.span().start)
304 }
305 Some(ch) => {
306 format!(
307 "unexpected '{}' at position {}, expected {}",
308 ch,
309 error.span().start,
310 expected.join(" or ")
311 )
312 }
313 None if expected.is_empty() => {
314 format!("unexpected end of input at position {}", error.span().start)
315 }
316 None => {
317 format!(
318 "unexpected end of input at position {}, expected {}",
319 error.span().start,
320 expected.join(" or ")
321 )
322 }
323 }
324}