Skip to main content

oxilean_parse/error/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5pub use crate::error_impl::{ParseError, ParseErrorKind};
6pub use crate::tokens::Span;
7
8use super::types::{
9    ErrorLocationResolver, ErrorSeverity, ErrorSeverityLevel, ParseDiagnostic, RichError,
10};
11
12/// Convenience alias for results that may fail with a `ParseError`.
13#[allow(missing_docs)]
14pub type ParseResult<T> = Result<T, ParseError>;
15/// Extension trait providing factory methods on `ParseError`.
16#[allow(clippy::new_ret_no_self)]
17#[allow(missing_docs)]
18pub trait ParseErrorFactory {
19    /// Create a generic parse error with a message.
20    fn new(msg: &str) -> ParseError;
21    /// Create an error at a specific source location.
22    fn at(msg: &str, line: u32, col: u32) -> ParseError;
23    /// Create an "unexpected token" error.
24    fn unexpected_token(found: &str, expected: &str, line: u32, col: u32) -> ParseError;
25    /// Create an "unexpected end of file" error.
26    fn unexpected_eof() -> ParseError;
27    /// Create an "invalid syntax" error.
28    fn invalid_syntax(msg: &str, line: u32, col: u32) -> ParseError;
29    /// Create an "unterminated string literal" error.
30    fn unterminated_string(line: u32, col: u32) -> ParseError;
31    /// Create a "reserved keyword used as identifier" error.
32    fn reserved_keyword(kw: &str, line: u32, col: u32) -> ParseError;
33    /// Create a "duplicate binder name" error.
34    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/// Return a short label for a `ParseErrorKind`.
330#[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/// Filters a list of diagnostics by severity.
428#[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/// Filters diagnostics to only include hard errors.
440#[allow(dead_code)]
441#[allow(missing_docs)]
442pub fn errors_only(diagnostics: &[ParseDiagnostic]) -> Vec<&ParseDiagnostic> {
443    filter_by_severity(diagnostics, ErrorSeverity::Error)
444}
445/// Filters diagnostics to only include warnings.
446#[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/// A "try" wrapper: runs a fallible operation, collecting any errors.
558#[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/// Formats a caret pointer under a line at `col`.
572#[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/// Formats an error at a given source position.
578#[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/// An error formatter that outputs machine-readable JSON-like text.
717#[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/// An error formatter that outputs UNIX-style error messages.
727#[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/// Checks if a collection of errors is below a threshold.
740#[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/// Sorts errors by severity (most severe first), then by position.
750#[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/// Deduplicates errors by (code, span_start) key.
760#[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/// Converts a vector of errors to a short summary string.
773#[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/// Writes a batch of errors to a formatted report string.
899#[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/// Check if source contains common error patterns.
912#[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/// Compute an "error density" metric: errors per 100 lines.
927#[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/// Format all errors as a table.
940#[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}