1use serde::ser::{Serialize, SerializeStruct, Serializer};
4use std::any::Any;
5use std::collections::{BTreeMap, HashMap};
6use std::fmt;
7use std::fmt::Write;
8use std::panic::{catch_unwind, AssertUnwindSafe};
9
10use crate::options::{ColorOptions, ErrorSuffix, ErrorSuffixContext, Options};
11use crate::token::Token;
12use crate::value::Value;
13
14#[derive(Debug, Clone, PartialEq)]
15pub struct ErrorToken {
16 pub name: String,
17 pub src: String,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct RecoveredAt {
22 pub skipped: usize,
23 pub sync: Option<crate::token::Tin>,
24 pub bad: bool,
25}
26
27#[derive(Debug, Clone, PartialEq)]
28pub struct TabnasError {
29 pub code: String,
30 pub detail: String,
31 pub pos: usize,
32 pub row: usize,
33 pub col: usize,
34 pub src: String,
35 pub hint: String,
36 pub tag: String,
37 pub full_source: String,
38 pub len: usize,
39 pub rule: String,
40 pub rule_stack: Vec<String>,
41 pub token: ErrorToken,
42 pub expected: Vec<String>,
43 pub plugins: Vec<String>,
44 pub recovered: Option<RecoveredAt>,
47 details: HashMap<String, Value>,
48 rendered_detail: String,
49 rendered_hint: String,
50 instance_tag: String,
51 rule_state: String,
52 why: String,
53 suffix: ErrorSuffix,
54 link: String,
55 color: ColorOptions,
56}
57
58impl TabnasError {
59 pub fn new(
60 code: impl Into<String>,
61 src: impl Into<String>,
62 full_source: impl Into<String>,
63 pos: usize,
64 row: usize,
65 col: usize,
66 ) -> Self {
67 let code_str = code.into();
68 let src_str = src.into();
69 let len = src_str.chars().count();
70 let full_src = full_source.into();
71 let detail = format_error_message(&code_str, &src_str);
72 let hint = format_error_hint(&code_str, &src_str);
73
74 TabnasError {
75 code: code_str,
76 detail: detail.clone(),
77 pos,
78 row,
79 col,
80 src: src_str.clone(),
81 hint: hint.clone(),
82 tag: "tabnas".to_string(),
83 full_source: full_src,
84 len,
85 rule: String::new(),
86 rule_stack: Vec::new(),
87 token: ErrorToken {
88 name: "#BD".to_string(),
89 src: src_str,
90 },
91 expected: Vec::new(),
92 plugins: Vec::new(),
93 recovered: None,
94 details: HashMap::new(),
95 rendered_detail: detail,
96 rendered_hint: hint,
97 instance_tag: "-".into(),
98 rule_state: String::new(),
99 why: String::new(),
100 suffix: ErrorSuffix::Standard,
101 link: String::new(),
102 color: ColorOptions::default(),
103 }
104 }
105
106 pub(crate) fn apply_options(&mut self, options: &Options) {
107 let render = ErrorRender {
108 code: &self.code,
109 src: &self.src,
110 pos: self.pos,
111 row: self.row,
112 col: self.col,
113 rule: &self.rule,
114 details: &self.details,
115 };
116 let next_detail = format_from_catalog(&options.error, &render);
117 if self.detail == self.rendered_detail {
118 self.detail = next_detail.clone();
119 }
120 self.rendered_detail = next_detail;
121
122 let next_hint = format_hint_from_catalog(&options.hint, &render);
123 if self.hint == self.rendered_hint {
124 self.hint = next_hint.clone();
125 }
126 self.rendered_hint = next_hint;
127
128 self.tag.clone_from(&options.errmsg.name);
129 self.instance_tag.clone_from(&options.tag);
130 self.suffix.clone_from(&options.errmsg.suffix);
131 self.link.clone_from(&options.errmsg.link);
132 self.color.clone_from(&options.color);
133 }
134
135 pub(crate) fn from_panic(
136 payload: Box<dyn Any + Send>,
137 api: &str,
138 full_source: &str,
139 pos: usize,
140 row: usize,
141 col: usize,
142 options: &Options,
143 ) -> Self {
144 let mut error = if let Some(error) = payload.downcast_ref::<Self>() {
145 error.clone()
146 } else if let Some(error) = payload.downcast_ref::<Box<Self>>() {
147 (**error).clone()
148 } else {
149 let detail = payload
150 .downcast_ref::<&str>()
151 .map(|message| (*message).to_string())
152 .or_else(|| payload.downcast_ref::<String>().cloned())
153 .unwrap_or_else(|| "unknown panic".into());
154 Self::new(
155 "internal",
156 format!("{api}: {detail}"),
157 full_source,
158 pos,
159 row,
160 col,
161 )
162 };
163 if error.full_source.is_empty() {
164 error.full_source = full_source.into();
165 }
166 if error.row == 0 {
167 error.row = row;
168 }
169 if error.col == 0 {
170 error.col = col;
171 }
172 error.apply_options(options);
173 error
174 }
175
176 pub fn attach_context(
177 &mut self,
178 rule: &str,
179 rule_state: &str,
180 rule_stack: Vec<String>,
181 token: Option<&Token>,
182 mut expected: Vec<String>,
183 ) {
184 self.rule = rule.to_string();
185 self.rule_state = rule_state.to_string();
186 self.rule_stack = rule_stack;
187 expected.sort();
188 expected.dedup();
189 self.expected = expected;
190 if let Some(token) = token {
191 self.pos = token.site.pos;
192 self.row = token.site.ri;
193 self.col = token.site.ci;
194 self.len = token.src.chars().count();
195 self.token = ErrorToken {
196 name: token.name.to_string(),
197 src: token.src.to_string(),
198 };
199 self.details.clone_from(token.use_data());
200 self.why = if token.why.is_empty() {
201 token.err.to_string()
202 } else {
203 token.why.to_string()
204 };
205 }
206 }
207
208 fn source_line(&self) -> String {
209 self.full_source
210 .lines()
211 .nth(self.row.saturating_sub(1))
212 .unwrap_or("")
213 .to_string()
214 }
215}
216
217impl Serialize for TabnasError {
218 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
219 where
220 S: Serializer,
221 {
222 let mut state = serializer.serialize_struct("TabnasError", 15)?;
223 state.serialize_field("status", "failure")?;
224 state.serialize_field("code", &self.code)?;
225 state.serialize_field("message", &self.detail)?;
226 state.serialize_field("hint", &self.hint)?;
227 state.serialize_field("row", &self.row)?;
228 state.serialize_field("col", &self.col)?;
229 state.serialize_field("pos", &self.pos)?;
230 state.serialize_field("len", &self.len)?;
231 state.serialize_field("rule", &self.rule)?;
232 state.serialize_field("ruleStack", &self.rule_stack)?;
233 state.serialize_field(
234 "token",
235 &serde_json::json!({"name": self.token.name, "src": self.token.src}),
236 )?;
237 state.serialize_field("expected", &self.expected)?;
238 state.serialize_field("src", &self.source_line())?;
239 state.serialize_field("plugins", &self.plugins)?;
240 state.serialize_field("version", crate::VERSION)?;
241 state.end()
242 }
243}
244
245impl fmt::Display for TabnasError {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 let (hi, lo, line, reset) = self.color.codes();
248 write!(
249 f,
250 "{hi}[{}/{}]:{reset} {}\n {line}-->{reset} <no-file>:{}:{}",
251 self.tag, self.code, self.detail, self.row, self.col
252 )?;
253
254 if !self.full_source.is_empty() {
255 let site = source_site(
256 &self.full_source,
257 &self.src,
258 &self.detail,
259 self.row,
260 self.col,
261 &self.color,
262 );
263 if !site.is_empty() {
264 write!(f, "\n{site}")?;
265 }
266 }
267
268 if !self.hint.is_empty() {
269 for (index, hint_line) in self.hint.trim().lines().enumerate() {
270 if index == 0 {
271 write!(f, "\n\n {hint_line}")?;
272 } else {
273 write!(f, "\n {hint_line}")?;
274 }
275 }
276 }
277
278 match &self.suffix {
279 ErrorSuffix::Disabled => {}
280 ErrorSuffix::Text(text) => write!(f, "\n{text}")?,
281 ErrorSuffix::Callback(render) => {
282 let context = ErrorSuffixContext {
283 code: self.code.clone(),
284 source: self.src.clone(),
285 message: self.detail.clone(),
286 hint: self.hint.clone(),
287 pos: self.pos,
288 row: self.row,
289 col: self.col,
290 name: self.tag.clone(),
291 tag: self.instance_tag.clone(),
292 rule: self.rule.clone(),
293 rule_state: self.rule_state.clone(),
294 token: self.token.name.clone(),
295 why: self.why.clone(),
296 plugins: self.plugins.clone(),
297 color: self.color.clone(),
298 };
299 let suffix = catch_unwind(AssertUnwindSafe(|| render(&context)))
300 .unwrap_or_else(|_| "--internal: errmsg.suffix callback panicked--".into());
301 write!(f, "\n{suffix}")?;
302 }
303 ErrorSuffix::Standard => {
304 if !self.link.is_empty() {
305 write!(f, "\n\n {lo}{}{reset}", self.link)?;
306 }
307 write!(
308 f,
309 "\n\n {lo}--internal: tag={}; rule={}~{}; token={}",
310 self.instance_tag, self.rule, self.rule_state, self.token.name
311 )?;
312 if !self.why.is_empty() {
313 write!(f, "~{}", self.why)?;
314 }
315 write!(f, "; plugins={}--{reset}", self.plugins.join(","))?;
316 }
317 }
318
319 Ok(())
320 }
321}
322
323impl std::error::Error for TabnasError {}
324
325pub(crate) fn default_error_messages() -> HashMap<String, String> {
326 [
327 ("unknown", "unknown error: {code}"),
328 ("unexpected", "unexpected character(s): {src}"),
329 ("invalid_unicode", "invalid unicode escape: {src}"),
330 ("invalid_ascii", "invalid ascii escape: {src}"),
331 ("unprintable", "unprintable character: {src}"),
332 ("unterminated_string", "unterminated string: {src}"),
333 ("unterminated_comment", "unterminated comment: {src}"),
334 ("unknown_rule", "unknown rule: {rulename}"),
335 ("end_of_source", "unexpected end of source"),
336 ("cancel", "parse cancelled"),
337 ("internal", "internal error: {src}"),
338 ]
339 .into_iter()
340 .map(|(code, template)| (code.into(), template.into()))
341 .collect()
342}
343
344pub(crate) fn default_error_hints() -> HashMap<String, String> {
345 [
346 (
347 "unknown",
348 "Unknown error code: {code}\nDetails:\n{details}",
349 ),
350 (
351 "unexpected",
352 "The character(s) {src} do not match any rule alternative active at\nthis position.",
353 ),
354 (
355 "invalid_unicode",
356 "The escape sequence {src} does not encode a valid unicode code point.",
357 ),
358 (
359 "invalid_ascii",
360 "The escape sequence {src} does not encode a valid ASCII character.",
361 ),
362 (
363 "unprintable",
364 "The character {src} (code point below 32) is not allowed inside a\nstring literal.",
365 ),
366 ("unterminated_string", "This string has no end quote."),
367 ("unterminated_comment", "This comment is never closed."),
368 ("unknown_rule", "No rule named {rulename} is defined."),
369 ("end_of_source", "Unexpected end of source."),
370 (
371 "cancel",
372 "The parse was cancelled by the caller's parse.budget.onCheck callback\n(or exceeded its configured budget) before completing.",
373 ),
374 (
375 "internal",
376 "The parser failed unexpectedly; this is a bug in tabnas\nor a plugin, not in your input.",
377 ),
378 ]
379 .into_iter()
380 .map(|(code, template)| (code.into(), template.into()))
381 .collect()
382}
383
384pub fn format_error_message(code: &str, src: &str) -> String {
385 format_from_catalog(
386 &default_error_messages(),
387 &ErrorRender {
388 code,
389 src,
390 pos: 0,
391 row: 1,
392 col: 1,
393 rule: if code == "unknown_rule" { src } else { "" },
394 details: &HashMap::new(),
395 },
396 )
397}
398
399pub fn format_error_hint(code: &str, src: &str) -> String {
400 format_hint_from_catalog(
401 &default_error_hints(),
402 &ErrorRender {
403 code,
404 src,
405 pos: 0,
406 row: 1,
407 col: 1,
408 rule: if code == "unknown_rule" { src } else { "" },
409 details: &HashMap::new(),
410 },
411 )
412}
413
414struct ErrorRender<'a> {
415 code: &'a str,
416 src: &'a str,
417 pos: usize,
418 row: usize,
419 col: usize,
420 rule: &'a str,
421 details: &'a HashMap<String, Value>,
422}
423
424fn format_from_catalog(catalog: &HashMap<String, String>, render: &ErrorRender<'_>) -> String {
425 let defaults = default_error_messages();
426 let template = catalog
427 .get(render.code)
428 .filter(|template| !template.is_empty())
429 .or_else(|| {
430 catalog
431 .get("unknown")
432 .filter(|template| !template.is_empty())
433 })
434 .or_else(|| defaults.get("unknown"))
435 .map(String::as_str)
436 .unwrap_or("unknown error: {code}");
437 interpolate(template, &interpolation_vars(render))
438}
439
440fn format_hint_from_catalog(catalog: &HashMap<String, String>, render: &ErrorRender<'_>) -> String {
441 let defaults = default_error_hints();
442 let Some(template) = catalog
443 .get(render.code)
444 .filter(|template| !template.is_empty())
445 .or_else(|| {
446 catalog
447 .get("unknown")
448 .filter(|template| !template.is_empty())
449 })
450 .or_else(|| defaults.get("unknown"))
451 else {
452 return String::new();
453 };
454 interpolate(template, &interpolation_vars(render))
455 .trim()
456 .to_string()
457}
458
459fn interpolation_vars(render: &ErrorRender<'_>) -> HashMap<String, String> {
460 let mut vars = HashMap::new();
461 vars.insert("code".into(), render.code.into());
462 vars.insert("src".into(), render.src.into());
463 vars.insert("pos".into(), render.pos.to_string());
464 vars.insert("row".into(), render.row.to_string());
465 vars.insert("col".into(), render.col.to_string());
466 let unknown_rule_name = render
467 .details
468 .get("rulename")
469 .map(injection_value)
470 .unwrap_or_else(|| {
471 if render.code == "unknown_rule" && render.rule.is_empty() {
472 render.src.into()
473 } else {
474 render.rule.into()
475 }
476 });
477 vars.insert("rulename".into(), unknown_rule_name);
478 for (key, value) in render.details {
479 vars.insert(key.clone(), injection_value(value));
480 }
481 vars.insert("details".into(), format_details(render.details));
482 vars
483}
484
485fn injection_value(value: &Value) -> String {
486 match value {
487 Value::Undefined => String::new(),
488 Value::Null => "null".into(),
489 Value::Bool(value) => value.to_string(),
490 Value::Number(value) => value.to_string(),
491 Value::String(value) => value.clone(),
492 other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()),
493 }
494}
495
496fn format_details(details: &HashMap<String, Value>) -> String {
497 let ordered: BTreeMap<_, _> = details.iter().collect();
498 let json = serde_json::to_string(&ordered).unwrap_or_else(|_| "{}".into());
499 json.replace('"', "")
500}
501
502fn interpolate(template: &str, vars: &HashMap<String, String>) -> String {
503 let mut out = String::with_capacity(template.len());
504 let mut chars = template.chars().peekable();
505 while let Some(c) = chars.next() {
506 if c == '{' {
507 let mut key = String::new();
508 while let Some(&k) = chars.peek() {
509 chars.next();
510 if k == '}' {
511 break;
512 }
513 key.push(k);
514 }
515 if let Some(val) = vars.get(key.as_str()) {
516 out.push_str(val);
517 } else {
518 out.push('{');
519 out.push_str(&key);
520 out.push('}');
521 }
522 } else {
523 out.push(c);
524 }
525 }
526 out
527}
528
529fn source_site(
530 source: &str,
531 span: &str,
532 message: &str,
533 row: usize,
534 col: usize,
535 color: &ColorOptions,
536) -> String {
537 let row = row.max(1);
538 let col = col.max(1);
539 let lines: Vec<_> = source.split('\n').collect();
540 if lines.is_empty() {
541 return String::new();
542 }
543 let line_index = row.saturating_sub(1).min(lines.len() - 1);
544 let pad = (row + 2).to_string().len() + 2;
545 let (_, _, line_color, reset) = color.codes();
546 let mut output = Vec::new();
547 let render_line = |number: usize, text: &str| {
548 format!("{line_color}{number:>width$} | {reset}{text}", width = pad)
549 };
550
551 if line_index >= 2 {
552 output.push(render_line(row - 2, lines[line_index - 2]));
553 }
554 if line_index >= 1 {
555 output.push(render_line(row - 1, lines[line_index - 1]));
556 }
557 output.push(render_line(row, lines[line_index]));
558
559 let caret_count = span.chars().count().max(1);
560 let mut caret = String::new();
561 write!(
562 caret,
563 "{} {}{line_color}{} {message}{reset}",
564 " ".repeat(pad),
565 " ".repeat(col - 1),
566 "^".repeat(caret_count),
567 )
568 .expect("writing to a String cannot fail");
569 output.push(caret);
570
571 if line_index + 1 < lines.len() {
572 output.push(render_line(row + 1, lines[line_index + 1]));
573 }
574 if line_index + 2 < lines.len() {
575 output.push(render_line(row + 2, lines[line_index + 2]));
576 }
577 output.join("\n")
578}