1pub use crate::error_impl::{ParseError, ParseErrorKind};
6pub use crate::tokens::Span;
7
8use super::types::{
9 ErrorLocationResolver, ErrorSeverity, ErrorSeverityLevel, ParseDiagnostic, RichError,
10};
11
12#[allow(missing_docs)]
14pub type ParseResult<T> = Result<T, ParseError>;
15#[allow(clippy::new_ret_no_self)]
17#[allow(missing_docs)]
18pub trait ParseErrorFactory {
19 fn new(msg: &str) -> ParseError;
21 fn at(msg: &str, line: u32, col: u32) -> ParseError;
23 fn unexpected_token(found: &str, expected: &str, line: u32, col: u32) -> ParseError;
25 fn unexpected_eof() -> ParseError;
27 fn invalid_syntax(msg: &str, line: u32, col: u32) -> ParseError;
29 fn unterminated_string(line: u32, col: u32) -> ParseError;
31 fn reserved_keyword(kw: &str, line: u32, col: u32) -> ParseError;
33 fn duplicate_binder(name: &str, line: u32, col: u32) -> ParseError;
35}
36impl ParseErrorFactory for ParseError {
37 fn new(msg: &str) -> ParseError {
38 ParseError::new(
39 ParseErrorKind::InvalidSyntax(msg.to_string()),
40 Span::new(0, 0, 0, 0),
41 )
42 }
43 fn at(msg: &str, line: u32, col: u32) -> ParseError {
44 ParseError::new(
45 ParseErrorKind::InvalidSyntax(msg.to_string()),
46 Span::new(0, 0, line as usize, col as usize),
47 )
48 }
49 fn unexpected_token(found: &str, expected: &str, _line: u32, _col: u32) -> ParseError {
50 let msg = format!("unexpected token '{}', expected {}", found, expected);
51 ParseError::new(ParseErrorKind::InvalidSyntax(msg), Span::new(0, 0, 0, 0))
52 }
53 fn unexpected_eof() -> ParseError {
54 ParseError::new(
55 ParseErrorKind::UnexpectedEof { expected: vec![] },
56 Span::new(0, 0, 0, 0),
57 )
58 }
59 fn invalid_syntax(msg: &str, _line: u32, _col: u32) -> ParseError {
60 ParseError::new(
61 ParseErrorKind::InvalidSyntax(format!("invalid syntax: {}", msg)),
62 Span::new(0, 0, 0, 0),
63 )
64 }
65 fn unterminated_string(_line: u32, _col: u32) -> ParseError {
66 ParseError::new(
67 ParseErrorKind::InvalidSyntax("unterminated string literal".to_string()),
68 Span::new(0, 0, 0, 0),
69 )
70 }
71 fn reserved_keyword(kw: &str, _line: u32, _col: u32) -> ParseError {
72 let msg = format!(
73 "'{}' is a reserved keyword and cannot be used as an identifier",
74 kw
75 );
76 ParseError::new(ParseErrorKind::Other(msg), Span::new(0, 0, 0, 0))
77 }
78 fn duplicate_binder(name: &str, _line: u32, _col: u32) -> ParseError {
79 let msg = format!("duplicate binder name '{}'", name);
80 ParseError::new(ParseErrorKind::Other(msg), Span::new(0, 0, 0, 0))
81 }
82}
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use crate::error::*;
87 fn mk_err(msg: &str) -> ParseError {
88 ParseError::from_msg(msg, 1, 1)
89 }
90 #[test]
91 fn test_factory_new() {
92 let e = <ParseError as ParseErrorFactory>::new("test");
93 assert!(!e.message().is_empty());
94 }
95 #[test]
96 fn test_factory_at() {
97 let e = <ParseError as ParseErrorFactory>::at("test", 3, 5);
98 assert_eq!(e.line(), 3);
99 assert_eq!(e.col(), 5);
100 }
101 #[test]
102 fn test_factory_unexpected_token() {
103 let e = <ParseError as ParseErrorFactory>::unexpected_token("foo", "bar", 1, 1);
104 assert!(e.message().contains("foo"));
105 assert!(e.message().contains("bar"));
106 }
107 #[test]
108 fn test_factory_unexpected_eof() {
109 let e = <ParseError as ParseErrorFactory>::unexpected_eof();
110 assert!(matches!(e.kind, ParseErrorKind::UnexpectedEof { .. }));
111 }
112 #[test]
113 fn test_factory_invalid_syntax() {
114 let e = <ParseError as ParseErrorFactory>::invalid_syntax("missing `:=`", 2, 3);
115 assert!(e.message().contains("invalid syntax"));
116 }
117 #[test]
118 fn test_factory_unterminated_string() {
119 let e = <ParseError as ParseErrorFactory>::unterminated_string(1, 10);
120 assert!(e.message().contains("unterminated"));
121 }
122 #[test]
123 fn test_factory_reserved_keyword() {
124 let e = <ParseError as ParseErrorFactory>::reserved_keyword("def", 1, 1);
125 assert!(e.message().contains("def"));
126 assert!(e.message().contains("reserved"));
127 }
128 #[test]
129 fn test_factory_duplicate_binder() {
130 let e = <ParseError as ParseErrorFactory>::duplicate_binder("x", 5, 5);
131 assert!(e.message().contains("x"));
132 assert!(e.message().contains("duplicate"));
133 }
134 #[test]
135 fn test_collector_add_and_len() {
136 let mut c = ParseErrorCollector::new();
137 c.add(mk_err("one"));
138 c.add(mk_err("two"));
139 assert_eq!(c.len(), 2);
140 assert!(c.has_errors());
141 }
142 #[test]
143 fn test_collector_limit() {
144 let mut c = ParseErrorCollector::with_limit(2);
145 c.add(mk_err("one"));
146 c.add(mk_err("two"));
147 c.add(mk_err("three"));
148 assert_eq!(c.len(), 2);
149 assert!(c.is_full());
150 }
151 #[test]
152 fn test_collector_clear() {
153 let mut c = ParseErrorCollector::new();
154 c.add(mk_err("a"));
155 c.clear();
156 assert!(c.is_empty());
157 }
158 #[test]
159 fn test_collector_first_error() {
160 let mut c = ParseErrorCollector::new();
161 assert!(c.first_error().is_none());
162 c.add(mk_err("first"));
163 c.add(mk_err("second"));
164 assert!(c
165 .first_error()
166 .expect("test operation should succeed")
167 .message()
168 .contains("first"));
169 }
170 #[test]
171 fn test_collector_merge() {
172 let mut c1 = ParseErrorCollector::new();
173 let mut c2 = ParseErrorCollector::new();
174 c1.add(mk_err("a"));
175 c2.add(mk_err("b"));
176 c1.merge(c2);
177 assert_eq!(c1.len(), 2);
178 }
179 #[test]
180 fn test_collector_display() {
181 let mut c = ParseErrorCollector::new();
182 c.add(mk_err("x"));
183 let s = format!("{}", c);
184 assert!(s.contains("1 errors"));
185 }
186 #[test]
187 fn test_recovery_strategy_continues() {
188 assert!(!RecoveryStrategy::Abort.continues());
189 assert!(RecoveryStrategy::SkipToSync.continues());
190 assert!(RecoveryStrategy::InsertToken.continues());
191 assert!(RecoveryStrategy::Replace.continues());
192 }
193 #[test]
194 fn test_recovery_strategy_display() {
195 assert_eq!(format!("{}", RecoveryStrategy::Abort), "abort");
196 assert_eq!(format!("{}", RecoveryStrategy::SkipToSync), "skip-to-sync");
197 }
198 #[test]
199 fn test_error_severity_ordering() {
200 assert!(ErrorSeverity::Error > ErrorSeverity::Warning);
201 assert!(ErrorSeverity::Warning > ErrorSeverity::Note);
202 }
203 #[test]
204 fn test_error_severity_is_error() {
205 assert!(ErrorSeverity::Error.is_error());
206 assert!(!ErrorSeverity::Warning.is_error());
207 }
208 #[test]
209 fn test_error_severity_is_recoverable() {
210 assert!(ErrorSeverity::Warning.is_recoverable());
211 assert!(ErrorSeverity::Note.is_recoverable());
212 assert!(!ErrorSeverity::Error.is_recoverable());
213 }
214 #[test]
215 fn test_error_severity_display() {
216 assert_eq!(format!("{}", ErrorSeverity::Error), "error");
217 assert_eq!(format!("{}", ErrorSeverity::Warning), "warning");
218 assert_eq!(format!("{}", ErrorSeverity::Note), "note");
219 }
220 #[test]
221 fn test_parse_diagnostic_error() {
222 let d = ParseDiagnostic::error("foo.ox", 3, 5, "something went wrong");
223 assert!(d.is_error());
224 assert_eq!(d.line, 3);
225 }
226 #[test]
227 fn test_parse_diagnostic_warning() {
228 let d = ParseDiagnostic::warning("foo.ox", 1, 1, "unused import");
229 assert!(!d.is_error());
230 }
231 #[test]
232 fn test_parse_diagnostic_with_hint() {
233 let d = ParseDiagnostic::error("foo.ox", 1, 1, "oops").with_hint("try this");
234 assert_eq!(d.hint.as_deref(), Some("try this"));
235 }
236 #[test]
237 fn test_parse_diagnostic_with_code() {
238 let d = ParseDiagnostic::error("foo.ox", 1, 1, "oops").with_code("def foo := 1");
239 assert!(d.code.is_some());
240 }
241 #[test]
242 fn test_parse_diagnostic_display() {
243 let d = ParseDiagnostic::error("foo.ox", 2, 4, "msg");
244 let s = format!("{}", d);
245 assert!(s.contains("foo.ox"));
246 assert!(s.contains("2:4"));
247 assert!(s.contains("msg"));
248 }
249 #[test]
250 fn test_formatter_format() {
251 let src = "line 1\nline 2 with error\nline 3\n";
252 let fmt = ParseErrorFormatter::new(src, "test.ox");
253 let err = ParseError::from_msg("test error", 2, 8);
254 let s = fmt.format(&err);
255 assert!(s.contains("test error"));
256 }
257 #[test]
258 fn test_formatter_format_all() {
259 let src = "def x := 1\n";
260 let fmt = ParseErrorFormatter::new(src, "f.ox");
261 let mut c = ParseErrorCollector::new();
262 c.add(mk_err("e1"));
263 c.add(mk_err("e2"));
264 let s = fmt.format_all(&c);
265 assert!(s.contains("e1"));
266 assert!(s.contains("e2"));
267 }
268 #[test]
269 fn test_parse_error_stats_record() {
270 let mut s = ParseErrorStats::new();
271 s.record(&ParseError::from_msg("eof", 0, 0));
272 s.record(&ParseError::from_msg("loc", 1, 5));
273 assert_eq!(s.total, 2);
274 assert_eq!(s.eof_errors, 1);
275 assert_eq!(s.located_errors, 1);
276 }
277 #[test]
278 fn test_parse_error_stats_display() {
279 let s = ParseErrorStats {
280 total: 5,
281 eof_errors: 1,
282 located_errors: 4,
283 };
284 let txt = format!("{}", s);
285 assert!(txt.contains("total: 5"));
286 }
287 #[test]
288 fn test_parse_error_budget_consume() {
289 let mut b = ParseErrorBudget::new(3);
290 assert!(b.consume());
291 assert!(b.consume());
292 assert!(b.consume());
293 assert!(!b.consume());
294 assert!(b.is_exhausted());
295 }
296 #[test]
297 fn test_parse_error_budget_consumed() {
298 let mut b = ParseErrorBudget::new(5);
299 b.consume();
300 b.consume();
301 assert_eq!(b.consumed(), 2);
302 }
303 #[test]
304 fn test_parse_error_budget_reset() {
305 let mut b = ParseErrorBudget::new(3);
306 b.consume();
307 b.reset();
308 assert_eq!(b.remaining, 3);
309 assert!(!b.is_exhausted());
310 }
311 #[test]
312 fn test_parse_result_ok() {
313 let r: ParseResult<i32> = Ok(42);
314 assert_eq!(r, Ok(42));
315 }
316 #[test]
317 fn test_parse_result_err() {
318 let r: ParseResult<i32> = Err(ParseError::from_msg("oops", 1, 1));
319 assert!(r.is_err());
320 }
321 #[test]
322 fn test_collector_into_errors() {
323 let mut c = ParseErrorCollector::new();
324 c.add(mk_err("a"));
325 let v = c.into_errors();
326 assert_eq!(v.len(), 1);
327 }
328}
329#[allow(missing_docs)]
331pub fn error_kind_label(kind: &ParseErrorKind) -> &'static str {
332 match kind {
333 ParseErrorKind::UnexpectedToken { .. } => "unexpected-token",
334 ParseErrorKind::UnexpectedEof { .. } => "unexpected-eof",
335 ParseErrorKind::InvalidSyntax(_) => "invalid-syntax",
336 ParseErrorKind::DuplicateDeclaration(_) => "duplicate-declaration",
337 ParseErrorKind::InvalidBinder(_) => "invalid-binder",
338 ParseErrorKind::InvalidPattern(_) => "invalid-pattern",
339 ParseErrorKind::InvalidUniverse(_) => "invalid-universe",
340 ParseErrorKind::Other(_) => "other",
341 }
342}
343#[cfg(test)]
344mod extra_tests {
345 use super::*;
346 use crate::error::*;
347 #[test]
348 fn test_error_kind_label_eof() {
349 assert_eq!(
350 error_kind_label(&ParseErrorKind::UnexpectedEof { expected: vec![] }),
351 "unexpected-eof"
352 );
353 }
354 #[test]
355 fn test_error_kind_label_token() {
356 assert_eq!(
357 error_kind_label(&ParseErrorKind::UnexpectedToken {
358 expected: vec![],
359 got: crate::tokens::TokenKind::Eof
360 }),
361 "unexpected-token"
362 );
363 }
364 #[test]
365 fn test_error_kind_label_syntax() {
366 assert_eq!(
367 error_kind_label(&ParseErrorKind::InvalidSyntax("".to_string())),
368 "invalid-syntax"
369 );
370 }
371 #[test]
372 fn test_error_kind_label_other() {
373 assert_eq!(
374 error_kind_label(&ParseErrorKind::Other("".to_string())),
375 "other"
376 );
377 }
378 #[test]
379 fn test_parse_warning_new() {
380 let w = ParseWarning::new("unused import", 5, 3);
381 assert_eq!(w.line, 5);
382 assert_eq!(w.col, 3);
383 }
384 #[test]
385 fn test_parse_warning_display() {
386 let w = ParseWarning::new("test warning", 2, 4);
387 let s = format!("{}", w);
388 assert!(s.contains("warning"));
389 assert!(s.contains("test warning"));
390 }
391 #[test]
392 fn test_parse_error_group_new() {
393 let g = ParseErrorGroup::new("syntax");
394 assert_eq!(g.label, "syntax");
395 assert!(g.is_empty());
396 }
397 #[test]
398 fn test_parse_error_group_add() {
399 let mut g = ParseErrorGroup::new("g");
400 g.add(ParseError::from_msg("err", 1, 1));
401 assert_eq!(g.len(), 1);
402 }
403 #[test]
404 fn test_parse_error_group_display() {
405 let mut g = ParseErrorGroup::new("syntax");
406 g.add(ParseError::from_msg("e", 1, 1));
407 let s = format!("{}", g);
408 assert!(s.contains("syntax"));
409 assert!(s.contains("1 errors"));
410 }
411 #[test]
412 fn test_recovery_strategy_replace() {
413 assert!(RecoveryStrategy::Replace.continues());
414 }
415 #[test]
416 fn test_error_severity_note() {
417 assert!(!ErrorSeverity::Note.is_error());
418 assert!(ErrorSeverity::Note.is_recoverable());
419 }
420 #[test]
421 fn test_parse_error_budget_initial_not_exhausted() {
422 let b = ParseErrorBudget::new(10);
423 assert!(!b.is_exhausted());
424 assert_eq!(b.consumed(), 0);
425 }
426}
427#[allow(dead_code)]
429#[allow(missing_docs)]
430pub fn filter_by_severity(
431 diagnostics: &[ParseDiagnostic],
432 min_severity: ErrorSeverity,
433) -> Vec<&ParseDiagnostic> {
434 diagnostics
435 .iter()
436 .filter(|d| d.severity >= min_severity)
437 .collect()
438}
439#[allow(dead_code)]
441#[allow(missing_docs)]
442pub fn errors_only(diagnostics: &[ParseDiagnostic]) -> Vec<&ParseDiagnostic> {
443 filter_by_severity(diagnostics, ErrorSeverity::Error)
444}
445#[allow(dead_code)]
447#[allow(missing_docs)]
448pub fn warnings_only(diagnostics: &[ParseDiagnostic]) -> Vec<&ParseDiagnostic> {
449 diagnostics
450 .iter()
451 .filter(|d| d.severity == ErrorSeverity::Warning)
452 .collect()
453}
454#[cfg(test)]
455mod error_report_tests {
456 use super::*;
457 use crate::error::*;
458 fn mk_diag(sev: ErrorSeverity) -> ParseDiagnostic {
459 ParseDiagnostic::new(sev, "test.ox", 1, 1, "msg")
460 }
461 #[test]
462 fn test_filter_by_severity_error_only() {
463 let diags = vec![
464 mk_diag(ErrorSeverity::Error),
465 mk_diag(ErrorSeverity::Warning),
466 mk_diag(ErrorSeverity::Note),
467 ];
468 let errs = filter_by_severity(&diags, ErrorSeverity::Error);
469 assert_eq!(errs.len(), 1);
470 }
471 #[test]
472 fn test_filter_by_severity_warning_up() {
473 let diags = vec![
474 mk_diag(ErrorSeverity::Error),
475 mk_diag(ErrorSeverity::Warning),
476 mk_diag(ErrorSeverity::Note),
477 ];
478 let result = filter_by_severity(&diags, ErrorSeverity::Warning);
479 assert_eq!(result.len(), 2);
480 }
481 #[test]
482 fn test_errors_only() {
483 let diags = vec![
484 mk_diag(ErrorSeverity::Error),
485 mk_diag(ErrorSeverity::Warning),
486 ];
487 let errs = errors_only(&diags);
488 assert_eq!(errs.len(), 1);
489 }
490 #[test]
491 fn test_warnings_only() {
492 let diags = vec![
493 mk_diag(ErrorSeverity::Error),
494 mk_diag(ErrorSeverity::Warning),
495 mk_diag(ErrorSeverity::Warning),
496 ];
497 let warns = warnings_only(&diags);
498 assert_eq!(warns.len(), 2);
499 }
500 #[test]
501 fn test_parse_error_context_new() {
502 let err = ParseError::from_msg("oops", 1, 1);
503 let ctx = ParseErrorContext::new(err);
504 assert!(ctx.decl_name.is_none());
505 assert!(ctx.phase.is_none());
506 }
507 #[test]
508 fn test_parse_error_context_with_decl() {
509 let err = ParseError::from_msg("oops", 1, 1);
510 let ctx = ParseErrorContext::new(err).with_decl("foo");
511 assert_eq!(ctx.decl_name.as_deref(), Some("foo"));
512 }
513 #[test]
514 fn test_parse_error_context_with_phase() {
515 let err = ParseError::from_msg("oops", 1, 1);
516 let ctx = ParseErrorContext::new(err).with_phase("binder");
517 assert_eq!(ctx.phase.as_deref(), Some("binder"));
518 }
519 #[test]
520 fn test_parse_error_context_display() {
521 let err = ParseError::from_msg("test", 1, 1);
522 let ctx = ParseErrorContext::new(err)
523 .with_decl("myDef")
524 .with_phase("expr");
525 let s = format!("{}", ctx);
526 assert!(s.contains("myDef"));
527 assert!(s.contains("expr"));
528 }
529 #[test]
530 fn test_parse_error_report_new() {
531 let r = ParseErrorReport::new("foo.ox");
532 assert!(r.is_clean());
533 assert_eq!(r.error_count(), 0);
534 }
535 #[test]
536 fn test_parse_error_report_add_error() {
537 let mut r = ParseErrorReport::new("foo.ox");
538 r.add(mk_diag(ErrorSeverity::Error));
539 assert_eq!(r.error_count(), 1);
540 assert!(!r.is_clean());
541 }
542 #[test]
543 fn test_parse_error_report_warnings() {
544 let mut r = ParseErrorReport::new("foo.ox");
545 r.add(mk_diag(ErrorSeverity::Warning));
546 r.add(mk_diag(ErrorSeverity::Warning));
547 assert_eq!(r.warning_count(), 2);
548 assert!(r.is_clean());
549 }
550 #[test]
551 fn test_parse_error_report_display() {
552 let r = ParseErrorReport::new("test.ox");
553 let s = format!("{}", r);
554 assert!(s.contains("test.ox"));
555 }
556}
557#[allow(dead_code)]
559#[allow(missing_docs)]
560pub fn try_collect<T, E: Clone>(results: Vec<Result<T, E>>) -> (Vec<T>, Vec<E>) {
561 let mut oks = Vec::new();
562 let mut errs = Vec::new();
563 for r in results {
564 match r {
565 Ok(v) => oks.push(v),
566 Err(e) => errs.push(e),
567 }
568 }
569 (oks, errs)
570}
571#[allow(dead_code)]
573#[allow(missing_docs)]
574pub fn format_caret(col: usize, len: usize) -> String {
575 format!("{}{}", " ".repeat(col), "^".repeat(len.max(1)))
576}
577#[allow(dead_code)]
579#[allow(missing_docs)]
580pub fn format_error_at(source: &str, byte_offset: usize, message: &str) -> String {
581 let resolver = ErrorLocationResolver::new(source);
582 let (line, col) = resolver.resolve(byte_offset);
583 let line_text = resolver.line_text(line);
584 let caret = format_caret(col, 1);
585 format!(
586 "{}\n{:4} | {}\n | {}\n {}",
587 message,
588 line + 1,
589 line_text,
590 caret,
591 ""
592 )
593}
594#[cfg(test)]
595mod extended_error_tests {
596 use super::*;
597 use crate::error::*;
598 #[test]
599 fn test_rich_error_format() {
600 let e = RichError::error("unexpected '+'", 10, 11)
601 .with_code("E0001")
602 .with_suggestion("remove the '+'")
603 .with_note("operators must be binary");
604 assert_eq!(e.span_len(), 1);
605 let fmt = e.format();
606 assert!(fmt.contains("[E0001]"));
607 assert!(fmt.contains("suggestion: remove the '+'"));
608 assert!(fmt.contains("note: operators must be binary"));
609 }
610 #[test]
611 fn test_error_severity_ordering() {
612 assert!(ErrorSeverityLevel::Fatal > ErrorSeverityLevel::Error);
613 assert!(ErrorSeverityLevel::Error > ErrorSeverityLevel::Warning);
614 assert!(ErrorSeverityLevel::Warning > ErrorSeverityLevel::Note);
615 }
616 #[test]
617 fn test_error_accumulator2() {
618 let mut acc = ErrorAccumulator2::new(10);
619 acc.add(RichError::error("err1", 0, 1));
620 acc.add(RichError::warning("warn1", 5, 6));
621 assert_eq!(acc.error_count(), 1);
622 assert_eq!(acc.warning_count(), 1);
623 assert!(!acc.has_fatal());
624 assert!(!acc.is_clean());
625 }
626 #[test]
627 fn test_error_deduplicator() {
628 let mut dedup = ErrorDeduplicator::new();
629 assert!(dedup.should_emit("E0001 at 5"));
630 assert!(!dedup.should_emit("E0001 at 5"));
631 assert!(dedup.should_emit("E0002 at 10"));
632 assert_eq!(dedup.suppressed_count(), 1);
633 assert_eq!(dedup.unique_count(), 2);
634 }
635 #[test]
636 fn test_error_filter() {
637 let mut filter = ErrorFilter::new(ErrorSeverityLevel::Warning);
638 filter.suppress_code("E0001");
639 let e1 = RichError::error("err", 0, 1).with_code("E0001");
640 let e2 = RichError::error("err2", 0, 1).with_code("E0002");
641 let e3 = RichError::warning("warn", 0, 1);
642 let errors = vec![e1, e2, e3];
643 let shown = filter.filter(&errors);
644 assert_eq!(shown.len(), 2);
645 }
646 #[test]
647 fn test_error_location_resolver() {
648 let src = "hello\nworld\nfoo";
649 let resolver = ErrorLocationResolver::new(src);
650 let (line, col) = resolver.resolve(7);
651 assert_eq!(line, 1);
652 assert_eq!(col, 1);
653 assert_eq!(resolver.line_text(0), "hello");
654 assert_eq!(resolver.line_text(1), "world");
655 assert_eq!(resolver.line_count(), 3);
656 }
657 #[test]
658 fn test_error_location_snippet() {
659 let src = "line1\nline2\nline3\nline4\nline5";
660 let resolver = ErrorLocationResolver::new(src);
661 let snippet = resolver.snippet(6, 1);
662 assert!(snippet.contains("line2"));
663 }
664 #[test]
665 fn test_batch_error_report() {
666 let errors = vec![RichError::error("e1", 0, 1), RichError::warning("w1", 2, 3)];
667 let report = BatchErrorReport::new("foo.ox", errors, 1000);
668 assert_eq!(report.error_count(), 1);
669 assert_eq!(report.warning_count(), 1);
670 assert!(!report.is_success());
671 let summary = report.summary_line();
672 assert!(summary.contains("foo.ox"));
673 assert!(summary.contains("1 error(s)"));
674 }
675 #[test]
676 fn test_error_code_catalogue() {
677 let cat = ErrorCodeCatalogue::new();
678 assert_eq!(cat.description("E0001"), Some("unexpected token"));
679 assert_eq!(cat.description("E9999"), None);
680 assert!(cat.count() >= 10);
681 }
682 #[test]
683 fn test_try_collect() {
684 let results: Vec<Result<i32, &str>> = vec![Ok(1), Err("e1"), Ok(2), Err("e2")];
685 let (oks, errs) = try_collect(results);
686 assert_eq!(oks, vec![1, 2]);
687 assert_eq!(errs, vec!["e1", "e2"]);
688 }
689 #[test]
690 fn test_format_caret() {
691 assert_eq!(format_caret(3, 2), " ^^");
692 assert_eq!(format_caret(0, 1), "^");
693 }
694 #[test]
695 fn test_format_error_at() {
696 let src = "hello world";
697 let msg = format_error_at(src, 6, "unexpected word");
698 assert!(msg.contains("unexpected word"));
699 assert!(msg.contains("hello world"));
700 }
701 #[test]
702 fn test_rich_error_warning() {
703 let w = RichError::warning("unused var", 0, 5);
704 assert_eq!(w.severity, ErrorSeverityLevel::Warning);
705 assert!(!w.is_fatal());
706 }
707 #[test]
708 fn test_error_accumulator_max() {
709 let mut acc = ErrorAccumulator2::new(2);
710 acc.add(RichError::error("e1", 0, 1));
711 acc.add(RichError::error("e2", 0, 1));
712 let added = acc.add(RichError::error("e3", 0, 1));
713 assert!(!added);
714 }
715}
716#[allow(dead_code)]
718#[allow(missing_docs)]
719pub fn format_error_json(e: &RichError) -> String {
720 let code = e.code.as_deref().unwrap_or("null");
721 format!(
722 r#"{{"severity":"{}", "code":"{}", "message":"{}", "span":[{},{}]}}"#,
723 e.severity, code, e.message, e.span_start, e.span_end
724 )
725}
726#[allow(dead_code)]
728#[allow(missing_docs)]
729pub fn format_error_unix(file: &str, line: usize, col: usize, e: &RichError) -> String {
730 format!(
731 "{}:{}:{}: {}: {}",
732 file,
733 line + 1,
734 col + 1,
735 e.severity,
736 e.message
737 )
738}
739#[allow(dead_code)]
741#[allow(missing_docs)]
742pub fn errors_within_budget(errors: &[RichError], max_errors: usize) -> bool {
743 let error_count = errors
744 .iter()
745 .filter(|e| e.severity >= ErrorSeverityLevel::Error)
746 .count();
747 error_count <= max_errors
748}
749#[allow(dead_code)]
751#[allow(missing_docs)]
752pub fn sort_errors_by_severity(errors: &mut [RichError]) {
753 errors.sort_by(|a, b| {
754 b.severity
755 .cmp(&a.severity)
756 .then(a.span_start.cmp(&b.span_start))
757 });
758}
759#[allow(dead_code)]
761#[allow(missing_docs)]
762pub fn dedup_errors(errors: Vec<RichError>) -> Vec<RichError> {
763 let mut seen = std::collections::HashSet::new();
764 errors
765 .into_iter()
766 .filter(|e| {
767 let key = (e.code.clone().unwrap_or_default(), e.span_start);
768 seen.insert(key)
769 })
770 .collect()
771}
772#[allow(dead_code)]
774#[allow(missing_docs)]
775pub fn error_summary(errors: &[RichError]) -> String {
776 let errs = errors
777 .iter()
778 .filter(|e| e.severity >= ErrorSeverityLevel::Error)
779 .count();
780 let warns = errors
781 .iter()
782 .filter(|e| e.severity == ErrorSeverityLevel::Warning)
783 .count();
784 format!("{} error(s), {} warning(s)", errs, warns)
785}
786#[cfg(test)]
787mod extended_error_tests_2 {
788 use super::*;
789 use crate::error::*;
790 #[test]
791 fn test_error_chain() {
792 let root = RichError::error("root error", 0, 5);
793 let cause = RichError::error("underlying cause", 0, 3);
794 let chain = ErrorChain::new(root).caused_by(cause);
795 assert_eq!(chain.len(), 2);
796 let fmt = chain.format_chain();
797 assert!(fmt.contains("root error"));
798 assert!(fmt.contains("caused by: underlying cause"));
799 }
800 #[test]
801 fn test_string_error_sink() {
802 let mut sink = StringErrorSink::new();
803 sink.emit(&RichError::error("e1", 0, 1));
804 sink.emit(&RichError::warning("w1", 2, 3));
805 assert_eq!(sink.count(), 2);
806 assert!(sink.contents().contains("e1"));
807 sink.clear();
808 assert_eq!(sink.count(), 0);
809 }
810 #[test]
811 fn test_error_budget() {
812 let mut budget = ErrorBudget::new(3);
813 assert!(budget.spend());
814 assert!(budget.spend());
815 assert!(budget.spend());
816 assert!(!budget.spend());
817 assert!(budget.is_exhausted());
818 assert!((budget.fraction_used() - 1.0).abs() < 1e-9);
819 }
820 #[test]
821 fn test_error_grouper() {
822 let mut grouper = ErrorGrouper::new();
823 grouper.add(RichError::error("e1", 0, 1).with_code("E0001"));
824 grouper.add(RichError::error("e2", 0, 1).with_code("E0001"));
825 grouper.add(RichError::error("e3", 0, 1).with_code("E0002"));
826 assert_eq!(grouper.group_count(), 2);
827 assert_eq!(grouper.errors_in_group("E0001").len(), 2);
828 assert_eq!(grouper.most_common_code(), Some("E0001"));
829 assert_eq!(grouper.total_error_count(), 3);
830 }
831 #[test]
832 fn test_format_error_json() {
833 let e = RichError::error("unexpected '+'", 5, 6).with_code("E0001");
834 let json = format_error_json(&e);
835 assert!(json.contains("\"severity\":\"error\""));
836 assert!(json.contains("\"code\":\"E0001\""));
837 assert!(json.contains("\"message\":\"unexpected '+'\""));
838 }
839 #[test]
840 fn test_format_error_unix() {
841 let e = RichError::error("bad token", 0, 1);
842 let s = format_error_unix("foo.ox", 2, 5, &e);
843 assert_eq!(s, "foo.ox:3:6: error: bad token");
844 }
845 #[test]
846 fn test_errors_within_budget() {
847 let errors = vec![RichError::error("e1", 0, 1), RichError::warning("w1", 0, 1)];
848 assert!(errors_within_budget(&errors, 1));
849 assert!(!errors_within_budget(&errors, 0));
850 }
851 #[test]
852 fn test_sort_errors_by_severity() {
853 let mut errors = vec![RichError::warning("w", 10, 11), RichError::error("e", 5, 6)];
854 sort_errors_by_severity(&mut errors);
855 assert_eq!(errors[0].severity, ErrorSeverityLevel::Error);
856 }
857 #[test]
858 fn test_dedup_errors() {
859 let errors = vec![
860 RichError::error("e1", 5, 6).with_code("E0001"),
861 RichError::error("e1 dup", 5, 6).with_code("E0001"),
862 RichError::error("e2", 10, 11).with_code("E0002"),
863 ];
864 let deduped = dedup_errors(errors);
865 assert_eq!(deduped.len(), 2);
866 }
867 #[test]
868 fn test_error_summary() {
869 let errors = vec![
870 RichError::error("e1", 0, 1),
871 RichError::error("e2", 0, 1),
872 RichError::warning("w1", 0, 1),
873 ];
874 let s = error_summary(&errors);
875 assert_eq!(s, "2 error(s), 1 warning(s)");
876 }
877 #[test]
878 fn test_recovery_hint() {
879 let h = RecoveryHint::insert_before(":=");
880 assert!(h.description().contains("insert ':=' before"));
881 let d = RecoveryHint::delete("+");
882 assert!(d.description().contains("delete '+'"));
883 let r = RecoveryHint::replace("->".to_string());
884 assert!(r.description().contains("replace with '->'"));
885 }
886 #[test]
887 fn test_tagged_error() {
888 let e = RichError::error("syntax error", 0, 5);
889 let te = TaggedError::new(e)
890 .with_tag(ErrorTag::Syntax)
891 .with_hint(RecoveryHint::insert_before(";"));
892 assert!(te.has_tag(ErrorTag::Syntax));
893 assert!(!te.has_tag(ErrorTag::Type));
894 let fmt = te.format_full();
895 assert!(fmt.contains("help: insert ';' before"));
896 }
897}
898#[allow(dead_code)]
900#[allow(missing_docs)]
901pub fn write_error_report(errors: &[RichError], source_name: &str) -> String {
902 let mut out = format!("=== Error Report: {} ===\n", source_name);
903 out.push_str(&format!("{}\n", error_summary(errors)));
904 out.push_str(&"─".repeat(50));
905 out.push('\n');
906 for (i, e) in errors.iter().enumerate() {
907 out.push_str(&format!("[{}] {}\n", i + 1, e.format()));
908 }
909 out
910}
911#[allow(dead_code)]
913#[allow(missing_docs)]
914pub fn detect_common_mistakes(source: &str) -> Vec<(&'static str, usize)> {
915 let mut issues = Vec::new();
916 for (i, line) in source.lines().enumerate() {
917 if line.contains("->") && line.contains("=>") {
918 issues.push(("mixed arrow styles", i));
919 }
920 if line.trim_start().starts_with("def") && !line.contains(":=") && !line.contains("where") {
921 issues.push(("def without assignment", i));
922 }
923 }
924 issues
925}
926#[allow(dead_code)]
928#[allow(missing_docs)]
929pub fn error_density(errors: &[RichError], source_line_count: usize) -> f64 {
930 if source_line_count == 0 {
931 return 0.0;
932 }
933 let err_count = errors
934 .iter()
935 .filter(|e| e.severity >= ErrorSeverityLevel::Error)
936 .count();
937 err_count as f64 / source_line_count as f64 * 100.0
938}
939#[allow(dead_code)]
941#[allow(missing_docs)]
942pub fn format_error_table(errors: &[RichError]) -> String {
943 let mut out = format!(
944 "{:>4} {:<10} {:<8} {}\n",
945 "N", "Code", "Severity", "Message"
946 );
947 out.push_str(&"─".repeat(60));
948 out.push('\n');
949 for (i, e) in errors.iter().enumerate() {
950 let code = e.code.as_deref().unwrap_or("-");
951 out.push_str(&format!(
952 "{:>4} {:<10} {:<8} {}\n",
953 i + 1,
954 code,
955 format!("{}", e.severity),
956 e.message
957 ));
958 }
959 out
960}
961#[cfg(test)]
962mod extended_error_tests_3 {
963 use super::*;
964 use crate::error::*;
965 #[test]
966 fn test_error_rate_tracker() {
967 let mut tracker = ErrorRateTracker::new(5);
968 tracker.record(3);
969 tracker.commit_window();
970 tracker.record(1);
971 tracker.commit_window();
972 assert!((tracker.average() - 2.0).abs() < 1e-9);
973 assert!((tracker.trend() - (-2.0)).abs() < 1e-9);
974 }
975 #[test]
976 fn test_quick_fix_registry() {
977 let mut reg = QuickFixRegistry::new();
978 reg.register("E0001", "remove the unexpected token");
979 reg.register("E0001", "wrap in parentheses");
980 reg.register("E0002", "add missing '}'");
981 assert_eq!(reg.fixes_for("E0001").len(), 2);
982 assert!(reg.has_fixes("E0002"));
983 assert!(!reg.has_fixes("E9999"));
984 assert_eq!(reg.total_codes(), 2);
985 }
986 #[test]
987 fn test_contextual_rich_error() {
988 let src = "def foo := 1\ndef bar := bad\n";
989 let e = RichError::error("bad identifier", 19, 22);
990 let ce = ContextualRichError::new(e, src, "test.ox");
991 assert_eq!(ce.file, "test.ox");
992 let fmt = ce.format_full();
993 assert!(fmt.contains("test.ox"));
994 assert!(fmt.contains("bad identifier"));
995 }
996 #[test]
997 fn test_write_error_report() {
998 let errors = vec![RichError::error("e1", 0, 1), RichError::warning("w1", 5, 6)];
999 let report = write_error_report(&errors, "main.ox");
1000 assert!(report.contains("=== Error Report: main.ox ==="));
1001 assert!(report.contains("e1"));
1002 assert!(report.contains("w1"));
1003 }
1004 #[test]
1005 fn test_error_explanation() {
1006 let exp = ErrorExplanation::new(
1007 "E0001",
1008 "Unexpected token",
1009 "You used a token that is not valid here.",
1010 "def x 1",
1011 "def x := 1",
1012 );
1013 let rendered = exp.render();
1014 assert!(rendered.contains("[E0001] Unexpected token"));
1015 assert!(rendered.contains("Bad:"));
1016 assert!(rendered.contains("Good:"));
1017 }
1018 #[test]
1019 fn test_error_explanation_book() {
1020 let mut book = ErrorExplanationBook::new();
1021 book.add(ErrorExplanation::new("E0001", "T", "D", "B", "G"));
1022 book.add(ErrorExplanation::new("E0002", "T2", "D2", "B2", "G2"));
1023 assert_eq!(book.count(), 2);
1024 assert!(book.lookup("E0001").is_some());
1025 assert!(book.lookup("E9999").is_none());
1026 }
1027 #[test]
1028 fn test_detect_common_mistakes() {
1029 let src = "def foo where\ndef bar";
1030 let issues = detect_common_mistakes(src);
1031 assert!(issues
1032 .iter()
1033 .any(|(msg, _)| *msg == "def without assignment"));
1034 }
1035 #[test]
1036 fn test_error_density() {
1037 let errors = vec![
1038 RichError::error("e1", 0, 1),
1039 RichError::error("e2", 0, 1),
1040 RichError::warning("w1", 0, 1),
1041 ];
1042 let density = error_density(&errors, 100);
1043 assert!((density - 2.0).abs() < 1e-9);
1044 }
1045 #[test]
1046 fn test_format_error_table() {
1047 let errors = vec![
1048 RichError::error("bad token", 0, 1).with_code("E0001"),
1049 RichError::warning("unused var", 5, 6),
1050 ];
1051 let table = format_error_table(&errors);
1052 assert!(table.contains("E0001"));
1053 assert!(table.contains("bad token"));
1054 assert!(table.contains("warning"));
1055 }
1056}