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 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 {
50 Some(string_start)
51 } else {
52 None
53 }
54}
55
56struct DetectedError {
58 message: String,
59 hint: Option<String>,
60 span: Option<Range<usize>>,
61}
62
63fn find_pattern(source: &str, pattern: &str) -> Option<usize> {
65 source.find(pattern)
66}
67
68fn detect_error(source: &str) -> Option<DetectedError> {
70 if let Some(pos) = find_unclosed_string(source) {
72 return Some(DetectedError {
73 message: "unclosed string literal".into(),
74 hint: Some("add a closing '\"' to complete the string".into()),
75 span: Some(pos..pos + 1),
76 });
77 }
78
79 if let Some(pos) = find_unmatched_opener(source, '(', ')') {
81 return Some(DetectedError {
82 message: "unclosed parentheses".into(),
83 hint: Some("add a closing ')' to complete the argument list".into()),
84 span: Some(pos..pos + 1),
85 });
86 }
87
88 if let Some(pos) = find_unmatched_opener(source, '{', '}') {
90 return Some(DetectedError {
91 message: "unclosed block".into(),
92 hint: Some("add a closing '}' to complete the block".into()),
93 span: Some(pos..pos + 1),
94 });
95 }
96
97 if let Some(pos) = find_unmatched_opener(source, '[', ']') {
99 return Some(DetectedError {
100 message: "unclosed list".into(),
101 hint: Some("add a closing ']' to complete the list".into()),
102 span: Some(pos..pos + 1),
103 });
104 }
105
106 if let Some(pos) = find_pattern(source, ": ,") {
108 return Some(DetectedError {
109 message: "missing value after ':'".into(),
110 hint: Some("add a value after the colon".into()),
111 span: Some(pos..pos + 3),
112 });
113 }
114
115 if let Some(pos) = find_pattern(source, ", ,") {
117 return Some(DetectedError {
118 message: "unexpected ','".into(),
119 hint: Some("remove the extra comma or add a value".into()),
120 span: Some(pos + 2..pos + 3),
121 });
122 }
123
124 if let Some(pos) = find_pattern(source, "(,") {
126 return Some(DetectedError {
127 message: "unexpected ',' at start of arguments".into(),
128 hint: Some("remove the leading comma".into()),
129 span: Some(pos + 1..pos + 2),
130 });
131 }
132
133 None
134}
135
136fn format_from_expected(found: Option<&char>, expected: &[String]) -> String {
138 let useful: Vec<_> = expected
139 .iter()
140 .filter(|s| !is_noise_pattern(s))
141 .take(3)
142 .collect();
143
144 match found {
145 Some(ch) if useful.is_empty() => format!("unexpected '{}'", ch),
146 Some(ch) => format!(
147 "unexpected '{}', expected {}",
148 ch,
149 useful
150 .iter()
151 .map(|s| s.as_str())
152 .collect::<Vec<_>>()
153 .join(" or ")
154 ),
155 None if useful.is_empty() => "unexpected end of input".into(),
156 None => format!(
157 "unexpected end of input, expected {}",
158 useful
159 .iter()
160 .map(|s| s.as_str())
161 .collect::<Vec<_>>()
162 .join(" or ")
163 ),
164 }
165}
166
167pub fn print_parse_errors(filename: &str, source: &str, errors: &[Rich<char>]) {
169 for error in errors {
170 let default_span = error.span().into_range();
171
172 let (msg, hint, span) = if let Some(detected) = detect_error(source) {
174 (
175 detected.message,
176 detected.hint,
177 detected.span.unwrap_or(default_span),
178 )
179 } else {
180 let expected: Vec<String> = error.expected().map(|e| format!("{}", e)).collect();
182 (
183 format_from_expected(error.found(), &expected),
184 None,
185 default_span,
186 )
187 };
188
189 let mut report = Report::build(ReportKind::Error, filename, span.start)
190 .with_message("Parse error")
191 .with_label(
192 Label::new((filename, span))
193 .with_message(&msg)
194 .with_color(Color::Red),
195 );
196
197 if let Some(hint_msg) = hint {
198 report = report.with_help(hint_msg);
199 }
200
201 report
202 .finish()
203 .print((filename, Source::from(source)))
204 .unwrap();
205 }
206}
207
208pub fn format_parse_errors(filename: &str, source: &str, errors: &[Rich<char>]) -> String {
210 let mut output = Vec::new();
211
212 for error in errors {
213 let default_span = error.span().into_range();
214
215 let (msg, hint, span) = if let Some(detected) = detect_error(source) {
216 (
217 detected.message,
218 detected.hint,
219 detected.span.unwrap_or(default_span),
220 )
221 } else {
222 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 .write((filename, Source::from(source)), &mut output)
245 .unwrap();
246 }
247
248 String::from_utf8(output).unwrap()
249}
250
251pub fn format_error_simple(error: &Rich<char>) -> String {
253 let expected: Vec<String> = error
254 .expected()
255 .map(|e| format!("{}", e))
256 .filter(|s| !is_noise_pattern(s))
257 .take(5)
258 .collect();
259
260 match error.found() {
261 Some(ch) if expected.is_empty() => {
262 format!("unexpected '{}' at position {}", ch, error.span().start)
263 }
264 Some(ch) => {
265 format!(
266 "unexpected '{}' at position {}, expected {}",
267 ch,
268 error.span().start,
269 expected.join(" or ")
270 )
271 }
272 None if expected.is_empty() => {
273 format!("unexpected end of input at position {}", error.span().start)
274 }
275 None => {
276 format!(
277 "unexpected end of input at position {}, expected {}",
278 error.span().start,
279 expected.join(" or ")
280 )
281 }
282 }
283}