Skip to main content

oxilean_parse/error_impl/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::tokens::Span;
6
7use super::types::{
8    AnnotatedSpan, DiagnosticSeverity, FullDiagnostic, LocatedError, ParseError, ParseErrorKind,
9    ParseErrors, RecoveryHint,
10};
11
12#[cfg(test)]
13mod tests {
14    use super::*;
15    use crate::error_impl::*;
16    use crate::tokens::TokenKind;
17    #[test]
18    fn test_parse_error_display() {
19        let err = ParseError::unexpected(
20            vec!["identifier".to_string()],
21            TokenKind::LParen,
22            Span::new(10, 11, 2, 5),
23        );
24        let msg = format!("{}", err);
25        assert!(msg.contains("line 2"));
26        assert!(msg.contains("column 5"));
27        assert!(msg.contains("expected identifier"));
28    }
29    #[test]
30    fn test_unexpected_eof() {
31        let err =
32            ParseError::unexpected_eof(vec!["expression".to_string()], Span::new(100, 100, 10, 1));
33        assert!(matches!(err.kind, ParseErrorKind::UnexpectedEof { .. }));
34    }
35}
36/// Format a list of expected token descriptions as a human-readable string.
37pub fn format_expected(expected: &[String]) -> String {
38    match expected.len() {
39        0 => "something".to_string(),
40        1 => expected[0].clone(),
41        2 => format!("{} or {}", expected[0], expected[1]),
42        _ => {
43            let (last, rest) = expected
44                .split_last()
45                .expect("slice has len >= 3 per match arm");
46            format!("{}, or {}", rest.join(", "), last)
47        }
48    }
49}
50/// Suggest a correction for a common typo in keywords.
51pub fn suggest_correction(got: &str) -> Option<&'static str> {
52    match got {
53        "Def" | "def" | "defn" => Some("definition"),
54        "thm" | "Thm" | "Theorem" => Some("theorem"),
55        "Axiom" => Some("axiom"),
56        "fun " | "fn" => Some("fun"),
57        "match " | "Match" => Some("match"),
58        _ => None,
59    }
60}
61/// Truncate source text to at most `max_len` chars, appending `…` if needed.
62pub fn truncate_source(src: &str, max_len: usize) -> String {
63    if src.chars().count() <= max_len {
64        src.to_string()
65    } else {
66        let truncated: String = src.chars().take(max_len).collect();
67        format!("{}…", truncated)
68    }
69}
70/// Return a string that underlines the span within its line.
71pub fn underline_span(line: &str, col: usize, len: usize) -> String {
72    let col = col.saturating_sub(1);
73    let spaces = " ".repeat(col);
74    let len = len.max(1).min(line.len().saturating_sub(col));
75    let carets = "^".repeat(len);
76    format!("{}{}", spaces, carets)
77}
78/// Generate recovery hints for common parse errors.
79pub fn recovery_hints(err: &ParseError) -> Vec<RecoveryHint> {
80    let mut hints = Vec::new();
81    match &err.kind {
82        ParseErrorKind::UnexpectedEof { .. } => {
83            hints.push(RecoveryHint::new(
84                "Did you forget a closing `)`, `}`, or `]`?",
85            ));
86        }
87        ParseErrorKind::InvalidBinder(msg) => {
88            if msg.contains("type") {
89                hints.push(RecoveryHint::new(
90                    "Binders require a type annotation, e.g. `(x : Nat)`",
91                ));
92            }
93        }
94        ParseErrorKind::InvalidSyntax(msg) => {
95            if msg.contains("=>") {
96                hints.push(
97                    RecoveryHint::new("OxiLean uses `->` for arrows, not `=>`")
98                        .with_replacement("->"),
99                );
100            }
101        }
102        _ => {}
103    }
104    hints
105}
106#[cfg(test)]
107mod error_extra_tests {
108    use super::*;
109    use crate::error_impl::*;
110    use crate::tokens::TokenKind;
111    fn make_err(kind: ParseErrorKind) -> ParseError {
112        ParseError::new(kind, Span::new(0, 5, 1, 1))
113    }
114    #[test]
115    fn test_severity_ordering() {
116        assert!(Severity::Note < Severity::Warning);
117        assert!(Severity::Warning < Severity::Error);
118    }
119    #[test]
120    fn test_severity_display() {
121        assert_eq!(Severity::Error.to_string(), "error");
122        assert_eq!(Severity::Warning.to_string(), "warning");
123        assert_eq!(Severity::Note.to_string(), "note");
124    }
125    #[test]
126    fn test_diagnostic_report() {
127        let err = ParseError::unexpected(
128            vec!["identifier".to_string()],
129            TokenKind::LParen,
130            Span::new(0, 5, 1, 1),
131        );
132        let diag = Diagnostic::error(err).with_hint("try writing an identifier here");
133        let report = diag.report("(hello)");
134        assert!(report.contains("error"));
135        assert!(report.contains("hint"));
136    }
137    #[test]
138    fn test_parse_errors_collection() {
139        let mut errs = ParseErrors::new();
140        errs.add_error(make_err(ParseErrorKind::Other("oops".to_string())));
141        errs.add_warning(make_err(ParseErrorKind::Other("meh".to_string())));
142        assert!(errs.has_errors());
143        assert!(errs.has_warnings());
144        assert_eq!(errs.len(), 2);
145        assert_eq!(errs.errors().count(), 1);
146        assert_eq!(errs.warnings().count(), 1);
147    }
148    #[test]
149    fn test_parse_errors_empty() {
150        let errs = ParseErrors::new();
151        assert!(errs.is_empty());
152        assert!(!errs.has_errors());
153        assert!(errs.first_error().is_none());
154    }
155    #[test]
156    fn test_format_expected() {
157        assert_eq!(format_expected(&[]), "something");
158        assert_eq!(format_expected(&["expr".to_string()]), "expr");
159        assert_eq!(
160            format_expected(&["a".to_string(), "b".to_string()]),
161            "a or b"
162        );
163        let three = format_expected(&["a".to_string(), "b".to_string(), "c".to_string()]);
164        assert!(three.contains("a"));
165        assert!(three.contains("or c"));
166    }
167    #[test]
168    fn test_suggest_correction() {
169        assert_eq!(suggest_correction("thm"), Some("theorem"));
170        assert_eq!(suggest_correction("def"), Some("definition"));
171        assert!(suggest_correction("totally_unknown").is_none());
172    }
173    #[test]
174    fn test_truncate_source() {
175        let s = "hello world this is a long string";
176        let t = truncate_source(s, 10);
177        assert!(t.len() <= 15);
178        assert!(t.contains('…'));
179        assert_eq!(truncate_source("short", 20), "short");
180    }
181    #[test]
182    fn test_underline_span() {
183        let u = underline_span("hello world", 1, 5);
184        assert!(u.starts_with("^"));
185    }
186    #[test]
187    fn test_recovery_hints_eof() {
188        let err = ParseError::unexpected_eof(vec![], Span::new(0, 0, 1, 1));
189        let hints = recovery_hints(&err);
190        assert!(!hints.is_empty());
191    }
192    #[test]
193    fn test_recovery_hints_arrow() {
194        let err = make_err(ParseErrorKind::InvalidSyntax("use => not ->".to_string()));
195        let hints = recovery_hints(&err);
196        assert!(!hints.is_empty());
197    }
198    #[test]
199    fn test_parse_errors_summary() {
200        let mut errs = ParseErrors::new();
201        errs.add_error(make_err(ParseErrorKind::Other("e".to_string())));
202        let s = errs.summary();
203        assert!(s.contains("1 error(s)"));
204        assert!(s.contains("0 warning(s)"));
205    }
206    #[test]
207    fn test_diagnostic_display() {
208        let err = make_err(ParseErrorKind::Other("bad".to_string()));
209        let diag = Diagnostic::error(err);
210        let s = format!("{}", diag);
211        assert!(s.contains("error"));
212    }
213}
214/// Return the 1-indexed line number from a `ParseError`.
215pub fn error_line(err: &ParseError) -> usize {
216    err.span.line
217}
218/// Return the 1-indexed column from a `ParseError`.
219pub fn error_col(err: &ParseError) -> usize {
220    err.span.column
221}
222/// Return the byte start offset from a `ParseError`.
223pub fn error_start(err: &ParseError) -> usize {
224    err.span.start
225}
226/// Return the byte end offset from a `ParseError`.
227pub fn error_end(err: &ParseError) -> usize {
228    err.span.end
229}
230/// Extract the source text covered by the error span.
231pub fn error_source_text<'a>(err: &ParseError, source: &'a str) -> &'a str {
232    source.get(err.span.start..err.span.end).unwrap_or("")
233}
234/// Convenience: create an `InvalidSyntax` error at the given location.
235pub fn syntax_error(msg: impl Into<String>, span: Span) -> ParseError {
236    ParseError::new(ParseErrorKind::InvalidSyntax(msg.into()), span)
237}
238/// Convenience: create a `DuplicateDeclaration` error.
239pub fn duplicate_error(name: impl Into<String>, span: Span) -> ParseError {
240    ParseError::new(ParseErrorKind::DuplicateDeclaration(name.into()), span)
241}
242/// Convenience: create an `InvalidBinder` error.
243pub fn binder_error(msg: impl Into<String>, span: Span) -> ParseError {
244    ParseError::new(ParseErrorKind::InvalidBinder(msg.into()), span)
245}
246/// Convenience: create an `InvalidPattern` error.
247pub fn pattern_error(msg: impl Into<String>, span: Span) -> ParseError {
248    ParseError::new(ParseErrorKind::InvalidPattern(msg.into()), span)
249}
250/// Convenience: create an `InvalidUniverse` error.
251pub fn universe_error(msg: impl Into<String>, span: Span) -> ParseError {
252    ParseError::new(ParseErrorKind::InvalidUniverse(msg.into()), span)
253}
254/// Convenience: wrap any string into an `Other` parse error.
255pub fn other_error(msg: impl Into<String>, span: Span) -> ParseError {
256    ParseError::new(ParseErrorKind::Other(msg.into()), span)
257}
258/// Produce a compact JSON-compatible representation of a `ParseError`.
259pub fn error_to_json(err: &ParseError) -> String {
260    format!(
261        r#"{{"kind":"{}","line":{},"col":{},"start":{},"end":{},"message":"{}"}}"#,
262        error_kind_name(&err.kind),
263        err.span.line,
264        err.span.column,
265        err.span.start,
266        err.span.end,
267        err.message().replace('"', "\\\"")
268    )
269}
270/// Return a short category name for a `ParseErrorKind`.
271pub fn error_kind_name(kind: &ParseErrorKind) -> &'static str {
272    match kind {
273        ParseErrorKind::UnexpectedToken { .. } => "unexpected_token",
274        ParseErrorKind::UnexpectedEof { .. } => "unexpected_eof",
275        ParseErrorKind::InvalidSyntax(_) => "invalid_syntax",
276        ParseErrorKind::DuplicateDeclaration(_) => "duplicate_declaration",
277        ParseErrorKind::InvalidBinder(_) => "invalid_binder",
278        ParseErrorKind::InvalidPattern(_) => "invalid_pattern",
279        ParseErrorKind::InvalidUniverse(_) => "invalid_universe",
280        ParseErrorKind::Other(_) => "other",
281    }
282}
283#[cfg(test)]
284mod error_extra_tests2 {
285    use super::*;
286    use crate::error_impl::*;
287    use crate::tokens::TokenKind;
288    #[test]
289    fn test_error_location_helpers() {
290        let err = ParseError::new(
291            ParseErrorKind::Other("test".to_string()),
292            Span::new(10, 20, 5, 3),
293        );
294        assert_eq!(error_line(&err), 5);
295        assert_eq!(error_col(&err), 3);
296        assert_eq!(error_start(&err), 10);
297        assert_eq!(error_end(&err), 20);
298    }
299    #[test]
300    fn test_error_source_text() {
301        let source = "hello world";
302        let err = ParseError::new(
303            ParseErrorKind::Other("t".to_string()),
304            Span::new(6, 11, 1, 7),
305        );
306        assert_eq!(error_source_text(&err, source), "world");
307    }
308    #[test]
309    fn test_parse_error_builder() {
310        let err = ParseErrorBuilder::new()
311            .kind(ParseErrorKind::Other("oops".to_string()))
312            .at(Span::new(5, 10, 2, 3))
313            .build();
314        assert_eq!(err.span.line, 2);
315        assert_eq!(err.span.column, 3);
316        assert!(matches!(err.kind, ParseErrorKind::Other(_)));
317    }
318    #[test]
319    fn test_convenience_constructors() {
320        let span = Span::new(0, 5, 1, 1);
321        let se = syntax_error("bad syntax", span.clone());
322        assert!(matches!(se.kind, ParseErrorKind::InvalidSyntax(_)));
323        let de = duplicate_error("foo", span.clone());
324        assert!(matches!(de.kind, ParseErrorKind::DuplicateDeclaration(_)));
325        let be = binder_error("bad binder", span.clone());
326        assert!(matches!(be.kind, ParseErrorKind::InvalidBinder(_)));
327        let pe = pattern_error("bad pattern", span.clone());
328        assert!(matches!(pe.kind, ParseErrorKind::InvalidPattern(_)));
329        let ue = universe_error("bad universe", span.clone());
330        assert!(matches!(ue.kind, ParseErrorKind::InvalidUniverse(_)));
331        let oe = other_error("other", span);
332        assert!(matches!(oe.kind, ParseErrorKind::Other(_)));
333    }
334    #[test]
335    fn test_error_kind_name() {
336        assert_eq!(
337            error_kind_name(&ParseErrorKind::Other("x".to_string())),
338            "other"
339        );
340        assert_eq!(
341            error_kind_name(&ParseErrorKind::UnexpectedEof { expected: vec![] }),
342            "unexpected_eof"
343        );
344    }
345    #[test]
346    fn test_error_to_json() {
347        let err = syntax_error("oops", Span::new(0, 5, 1, 1));
348        let json = error_to_json(&err);
349        assert!(json.contains("\"kind\""));
350        assert!(json.contains("\"line\""));
351        assert!(json.contains("invalid_syntax"));
352    }
353    #[test]
354    fn test_parse_errors_first_error() {
355        let mut errs = ParseErrors::new();
356        let err = syntax_error("first", Span::new(0, 1, 1, 1));
357        errs.add_error(err.clone());
358        assert!(errs.first_error().is_some());
359        assert_eq!(
360            errs.first_error()
361                .expect("test operation should succeed")
362                .message(),
363            err.message()
364        );
365    }
366}
367/// Return `true` if the error kind is a duplicate declaration.
368#[allow(dead_code)]
369pub fn is_duplicate_error(err: &ParseError) -> bool {
370    matches!(err.kind, ParseErrorKind::DuplicateDeclaration(_))
371}
372/// Return `true` if the error kind is an invalid syntax error.
373#[allow(dead_code)]
374pub fn is_syntax_error(err: &ParseError) -> bool {
375    matches!(err.kind, ParseErrorKind::InvalidSyntax(_))
376}
377/// Classify a `ParseError` into a human-readable category string.
378#[allow(dead_code)]
379pub fn classify_error(err: &ParseError) -> &'static str {
380    match &err.kind {
381        ParseErrorKind::UnexpectedToken { .. } => "syntax",
382        ParseErrorKind::UnexpectedEof { .. } => "eof",
383        ParseErrorKind::InvalidSyntax(_) => "syntax",
384        ParseErrorKind::DuplicateDeclaration(_) => "semantic",
385        ParseErrorKind::InvalidBinder(_) => "binder",
386        ParseErrorKind::InvalidPattern(_) => "pattern",
387        ParseErrorKind::InvalidUniverse(_) => "universe",
388        ParseErrorKind::Other(_) => "other",
389    }
390}
391/// Return `true` if the error is likely recoverable (parser can continue).
392///
393/// EOF errors and certain syntax errors are generally unrecoverable,
394/// while duplicate declaration errors are recoverable.
395#[allow(dead_code)]
396pub fn is_recoverable(err: &ParseError) -> bool {
397    match &err.kind {
398        ParseErrorKind::UnexpectedEof { .. } => false,
399        ParseErrorKind::DuplicateDeclaration(_) => true,
400        ParseErrorKind::InvalidBinder(_) => false,
401        _ => true,
402    }
403}
404/// Return the length (in bytes) of the span.
405#[allow(dead_code)]
406pub fn span_len(err: &ParseError) -> usize {
407    err.span.end.saturating_sub(err.span.start)
408}
409/// Check if an error's span is empty (zero-length).
410#[allow(dead_code)]
411pub fn span_is_empty(err: &ParseError) -> bool {
412    span_len(err) == 0
413}
414/// Expand the span of an error by `n` bytes on both sides (clamped to source bounds).
415#[allow(dead_code)]
416pub fn expand_span(err: &ParseError, source_len: usize, n: usize) -> (usize, usize) {
417    let start = err.span.start.saturating_sub(n);
418    let end = (err.span.end + n).min(source_len);
419    (start, end)
420}
421/// Render a collection of diagnostics as a multi-line string.
422#[allow(dead_code)]
423pub fn render_diagnostics(diags: &ParseErrors, source: &str) -> String {
424    let mut out = String::new();
425    for diag in diags.iter() {
426        out.push_str(&diag.report(source));
427        out.push('\n');
428    }
429    out
430}
431/// Return the count of each kind in a `ParseErrors` collection.
432#[allow(dead_code)]
433pub fn count_by_kind(errs: &ParseErrors) -> std::collections::HashMap<&'static str, usize> {
434    let mut counts = std::collections::HashMap::new();
435    for diag in errs.iter() {
436        let category = classify_error(&diag.error);
437        *counts.entry(category).or_insert(0) += 1;
438    }
439    counts
440}
441/// Deduplicate errors by span start position.
442///
443/// If two errors start at the same position, only the first is kept.
444#[allow(dead_code)]
445pub fn dedup_errors(errs: &[ParseError]) -> Vec<ParseError> {
446    let mut seen = std::collections::HashSet::new();
447    errs.iter()
448        .filter(|e| seen.insert(e.span.start))
449        .cloned()
450        .collect()
451}
452/// Sort errors by their span start position.
453#[allow(dead_code)]
454pub fn sort_errors(errs: &mut [ParseError]) {
455    errs.sort_by_key(|e| e.span.start);
456}
457/// Return the error with the earliest position.
458#[allow(dead_code)]
459pub fn earliest_error(errs: &[ParseError]) -> Option<&ParseError> {
460    errs.iter().min_by_key(|e| e.span.start)
461}
462/// Return the error with the latest position.
463#[allow(dead_code)]
464pub fn latest_error(errs: &[ParseError]) -> Option<&ParseError> {
465    errs.iter().max_by_key(|e| e.span.start)
466}
467/// Export all errors in a `ParseErrors` as a JSON array string.
468#[allow(dead_code)]
469pub fn errors_to_json(errs: &ParseErrors) -> String {
470    let entries: Vec<String> = errs.errors().map(|d| error_to_json(&d.error)).collect();
471    format!("[{}]", entries.join(","))
472}
473#[cfg(test)]
474mod error_final_tests {
475    use super::*;
476    use crate::error_impl::*;
477    use crate::tokens::TokenKind;
478    fn make_err_at(start: usize, end: usize) -> ParseError {
479        ParseError::new(
480            ParseErrorKind::Other("test".to_string()),
481            Span::new(start, end, 1, 1),
482        )
483    }
484    #[test]
485    fn test_classify_error() {
486        assert_eq!(
487            classify_error(&syntax_error("bad", Span::new(0, 1, 1, 1))),
488            "syntax"
489        );
490        assert_eq!(
491            classify_error(&duplicate_error("foo", Span::new(0, 1, 1, 1))),
492            "semantic"
493        );
494        assert_eq!(
495            classify_error(&binder_error("bad binder", Span::new(0, 1, 1, 1))),
496            "binder"
497        );
498        assert_eq!(
499            classify_error(&pattern_error("bad pat", Span::new(0, 1, 1, 1))),
500            "pattern"
501        );
502        assert_eq!(
503            classify_error(&universe_error("bad univ", Span::new(0, 1, 1, 1))),
504            "universe"
505        );
506        assert_eq!(
507            classify_error(&other_error("other", Span::new(0, 1, 1, 1))),
508            "other"
509        );
510    }
511    #[test]
512    fn test_is_recoverable() {
513        let eof = ParseError::unexpected_eof(vec![], Span::new(0, 0, 1, 1));
514        assert!(!is_recoverable(&eof));
515        let dup = duplicate_error("foo", Span::new(0, 1, 1, 1));
516        assert!(is_recoverable(&dup));
517        let syn = syntax_error("bad", Span::new(0, 1, 1, 1));
518        assert!(is_recoverable(&syn));
519    }
520    #[test]
521    fn test_span_len() {
522        let err = make_err_at(5, 10);
523        assert_eq!(span_len(&err), 5);
524    }
525    #[test]
526    fn test_span_is_empty() {
527        let err = make_err_at(5, 5);
528        assert!(span_is_empty(&err));
529        let err2 = make_err_at(5, 6);
530        assert!(!span_is_empty(&err2));
531    }
532    #[test]
533    fn test_expand_span() {
534        let err = make_err_at(5, 10);
535        let (start, end) = expand_span(&err, 100, 2);
536        assert_eq!(start, 3);
537        assert_eq!(end, 12);
538    }
539    #[test]
540    fn test_expand_span_clamp() {
541        let err = make_err_at(1, 2);
542        let (start, end) = expand_span(&err, 5, 10);
543        assert_eq!(start, 0);
544        assert_eq!(end, 5);
545    }
546    #[test]
547    fn test_dedup_errors() {
548        let e1 = make_err_at(5, 10);
549        let e2 = make_err_at(5, 12);
550        let e3 = make_err_at(15, 20);
551        let errs = vec![e1, e2, e3];
552        let deduped = dedup_errors(&errs);
553        assert_eq!(deduped.len(), 2);
554    }
555    #[test]
556    fn test_sort_errors() {
557        let mut errs = vec![make_err_at(10, 15), make_err_at(3, 5), make_err_at(7, 9)];
558        sort_errors(&mut errs);
559        assert_eq!(errs[0].span.start, 3);
560        assert_eq!(errs[1].span.start, 7);
561        assert_eq!(errs[2].span.start, 10);
562    }
563    #[test]
564    fn test_earliest_and_latest_error() {
565        let errs = vec![make_err_at(10, 15), make_err_at(3, 5), make_err_at(7, 9)];
566        assert_eq!(
567            earliest_error(&errs)
568                .expect("span should be present")
569                .span
570                .start,
571            3
572        );
573        assert_eq!(
574            latest_error(&errs)
575                .expect("span should be present")
576                .span
577                .start,
578            10
579        );
580    }
581    #[test]
582    fn test_count_by_kind() {
583        let mut errs = ParseErrors::new();
584        errs.add_error(syntax_error("x", Span::new(0, 1, 1, 1)));
585        errs.add_error(syntax_error("y", Span::new(1, 2, 1, 2)));
586        errs.add_error(binder_error("z", Span::new(2, 3, 1, 3)));
587        let counts = count_by_kind(&errs);
588        assert_eq!(counts.get("syntax"), Some(&2));
589        assert_eq!(counts.get("binder"), Some(&1));
590    }
591    #[test]
592    fn test_errors_to_json() {
593        let mut errs = ParseErrors::new();
594        errs.add_error(syntax_error("bad", Span::new(0, 3, 1, 1)));
595        let json = errors_to_json(&errs);
596        assert!(json.starts_with('['));
597        assert!(json.ends_with(']'));
598        assert!(json.contains("invalid_syntax"));
599    }
600    #[test]
601    fn test_render_diagnostics() {
602        let mut errs = ParseErrors::new();
603        errs.add_error(syntax_error("bad", Span::new(0, 3, 1, 1)));
604        let rendered = render_diagnostics(&errs, "bad code here");
605        assert!(!rendered.is_empty());
606    }
607    #[test]
608    fn test_is_duplicate_error_fn() {
609        let err = duplicate_error("foo", Span::new(0, 1, 1, 1));
610        assert!(is_duplicate_error(&err));
611        let err2 = syntax_error("oops", Span::new(0, 1, 1, 1));
612        assert!(!is_duplicate_error(&err2));
613    }
614    #[test]
615    fn test_is_syntax_error_fn() {
616        let err = syntax_error("oops", Span::new(0, 1, 1, 1));
617        assert!(is_syntax_error(&err));
618        let err2 = other_error("other", Span::new(0, 1, 1, 1));
619        assert!(!is_syntax_error(&err2));
620    }
621}
622/// Parse a source location from a "line:col" string.
623#[allow(dead_code)]
624#[allow(missing_docs)]
625pub fn parse_location(s: &str) -> Option<(usize, usize)> {
626    let mut parts = s.splitn(2, ':');
627    let line = parts.next()?.parse::<usize>().ok()?;
628    let col = parts.next()?.parse::<usize>().ok()?;
629    Some((line, col))
630}
631/// Formats a sequence of errors in GNU style (file:line:col: message).
632#[allow(dead_code)]
633#[allow(missing_docs)]
634pub fn format_gnu_errors(filename: &str, errors: &[LocatedError]) -> String {
635    errors
636        .iter()
637        .map(|e| format!("{}:{}:{}: error: {}", filename, e.line, e.col, e.message))
638        .collect::<Vec<_>>()
639        .join("\n")
640}
641/// Computes a fingerprint for an error message (for deduplication).
642#[allow(dead_code)]
643#[allow(missing_docs)]
644pub fn error_fingerprint(msg: &str) -> u64 {
645    let mut hash = 14695981039346656037u64;
646    for b in msg.bytes() {
647        hash ^= b as u64;
648        hash = hash.wrapping_mul(1099511628211u64);
649    }
650    hash
651}
652/// Deduplicates a list of errors by message fingerprint.
653#[allow(dead_code)]
654#[allow(missing_docs)]
655pub fn dedup_by_message(errors: Vec<LocatedError>) -> Vec<LocatedError> {
656    let mut seen = std::collections::HashSet::new();
657    errors
658        .into_iter()
659        .filter(|e| seen.insert(error_fingerprint(&e.message)))
660        .collect()
661}
662/// Format source text with annotated spans underlined.
663#[allow(dead_code)]
664#[allow(missing_docs)]
665pub fn format_annotated_source(src: &str, spans: &[AnnotatedSpan]) -> String {
666    let mut out = String::from(src);
667    out.push('\n');
668    for span in spans {
669        let start = span.start.min(src.len());
670        let end = span.end.min(src.len());
671        let len = end.saturating_sub(start);
672        out.push_str(&" ".repeat(start));
673        out.push_str(&"^".repeat(len.max(1)));
674        out.push(' ');
675        out.push_str(&span.label);
676        out.push('\n');
677    }
678    out
679}
680/// Extract context lines around a given line number from source.
681#[allow(dead_code)]
682#[allow(missing_docs)]
683pub fn extract_context(src: &str, line_no: usize, radius: usize) -> (Vec<String>, usize) {
684    let lines: Vec<&str> = src.lines().collect();
685    let idx = line_no.saturating_sub(1);
686    let start = idx.saturating_sub(radius);
687    let end = (idx + radius + 1).min(lines.len());
688    let context: Vec<String> = lines[start..end].iter().map(|s| s.to_string()).collect();
689    let error_idx = idx - start;
690    (context, error_idx)
691}
692/// Counts the number of errors at each severity level.
693#[allow(dead_code)]
694#[allow(missing_docs)]
695pub fn count_by_severity(
696    diagnostics: &[FullDiagnostic],
697) -> std::collections::HashMap<String, usize> {
698    let mut counts = std::collections::HashMap::new();
699    for d in diagnostics {
700        *counts.entry(d.severity.to_string()).or_insert(0) += 1;
701    }
702    counts
703}
704/// Formats a compact error summary line.
705#[allow(dead_code)]
706#[allow(missing_docs)]
707pub fn compact_error_summary(diagnostics: &[FullDiagnostic]) -> String {
708    let errors = diagnostics
709        .iter()
710        .filter(|d| d.severity >= DiagnosticSeverity::Error)
711        .count();
712    let warnings = diagnostics
713        .iter()
714        .filter(|d| d.severity == DiagnosticSeverity::Warning)
715        .count();
716    format!("{} error(s), {} warning(s)", errors, warnings)
717}
718/// Returns the line and column for a byte offset in source.
719#[allow(dead_code)]
720#[allow(missing_docs)]
721pub fn byte_offset_to_line_col(src: &str, offset: usize) -> (usize, usize) {
722    let mut line = 1usize;
723    let mut col = 1usize;
724    for (i, c) in src.char_indices() {
725        if i >= offset {
726            break;
727        }
728        if c == '\n' {
729            line += 1;
730            col = 1;
731        } else {
732            col += 1;
733        }
734    }
735    (line, col)
736}
737#[cfg(test)]
738mod error_impl_ext_tests {
739    use super::*;
740    use crate::error_impl::*;
741    use crate::tokens::TokenKind;
742    #[test]
743    fn test_located_error_format() {
744        let err = LocatedError::new("unexpected token", 0, 5, 1, 3);
745        assert_eq!(err.format(), "1:3: unexpected token");
746    }
747    #[test]
748    fn test_error_sink() {
749        let mut sink = ErrorSink::new();
750        sink.push(LocatedError::new("err", 0, 1, 1, 1));
751        assert!(sink.has_errors());
752        assert_eq!(sink.len(), 1);
753        sink.clear();
754        assert!(sink.is_empty());
755    }
756    #[test]
757    fn test_error_code_format() {
758        let code = ErrorCode::new("E", 42);
759        assert_eq!(code.format(), "E0042");
760    }
761    #[test]
762    fn test_full_diagnostic_display() {
763        let diag = FullDiagnostic::error("something went wrong").with_note("check the input");
764        let s = diag.display();
765        assert!(s.contains("error"));
766        assert!(s.contains("something went wrong"));
767        assert!(s.contains("check the input"));
768    }
769    #[test]
770    fn test_diagnostic_bag() {
771        let mut bag = DiagnosticBag::new();
772        bag.add(FullDiagnostic::error("oops"));
773        bag.add(FullDiagnostic::warning("careful"));
774        assert!(bag.has_errors());
775        assert_eq!(bag.errors().len(), 1);
776        assert_eq!(bag.warnings().len(), 1);
777    }
778    #[test]
779    fn test_parse_location() {
780        assert_eq!(parse_location("10:5"), Some((10, 5)));
781        assert_eq!(parse_location("bad"), None);
782    }
783    #[test]
784    fn test_format_gnu_errors() {
785        let errs = vec![LocatedError::new("unexpected", 0, 1, 2, 3)];
786        let s = format_gnu_errors("main.lean", &errs);
787        assert!(s.contains("main.lean:2:3: error:"));
788    }
789    #[test]
790    fn test_dedup_by_message() {
791        let errs = vec![
792            LocatedError::new("duplicate", 0, 1, 1, 1),
793            LocatedError::new("duplicate", 0, 1, 2, 1),
794            LocatedError::new("unique", 0, 1, 3, 1),
795        ];
796        let deduped = dedup_by_message(errs);
797        assert_eq!(deduped.len(), 2);
798    }
799    #[test]
800    fn test_byte_offset_to_line_col() {
801        let src = "hello\nworld\n";
802        let (line, _col) = byte_offset_to_line_col(src, 6);
803        assert_eq!(line, 2);
804    }
805    #[test]
806    fn test_error_rate_limiter() {
807        let mut limiter = ErrorRateLimiter::new(3);
808        assert!(limiter.accept());
809        assert!(limiter.accept());
810        assert!(limiter.accept());
811        assert!(!limiter.accept());
812        assert!(limiter.exceeded);
813    }
814    #[test]
815    fn test_format_annotated_source() {
816        let src = "fun x -> y";
817        let spans = vec![AnnotatedSpan::new(4, 5, "here")];
818        let out = format_annotated_source(src, &spans);
819        assert!(out.contains("fun x -> y"));
820        assert!(out.contains("here"));
821    }
822    #[test]
823    fn test_extract_context() {
824        let src = "line1\nline2\nline3\nline4\nline5";
825        let (ctx, idx) = extract_context(src, 3, 1);
826        assert!(ctx.len() <= 3);
827        assert!(idx < ctx.len());
828    }
829    #[test]
830    fn test_error_message_filter() {
831        let filter = ErrorMessageFilter::new().suppress("internal");
832        let errs = vec![
833            LocatedError::new("internal error", 0, 1, 1, 1),
834            LocatedError::new("parse error", 0, 1, 2, 1),
835        ];
836        let shown = filter.filter(&errs);
837        assert_eq!(shown.len(), 1);
838        assert!(shown[0].message.contains("parse"));
839    }
840    #[test]
841    fn test_compact_error_summary() {
842        let diags = vec![
843            FullDiagnostic::error("e1"),
844            FullDiagnostic::error("e2"),
845            FullDiagnostic::warning("w1"),
846        ];
847        let s = compact_error_summary(&diags);
848        assert!(s.contains("2 error"));
849        assert!(s.contains("1 warning"));
850    }
851}
852/// Formats an error range as a caret string (e.g., "   ^^^").
853#[allow(dead_code)]
854#[allow(missing_docs)]
855pub fn format_caret_range(col: usize, len: usize) -> String {
856    format!(
857        "{}{}",
858        " ".repeat(col.saturating_sub(1)),
859        "^".repeat(len.max(1))
860    )
861}
862#[cfg(test)]
863mod error_impl_ext2_tests {
864    use super::*;
865    use crate::error_impl::*;
866    use crate::tokens::TokenKind;
867    #[test]
868    fn test_lint_warning() {
869        let w = LintWarning::new("unused-variable", "variable x is unused")
870            .with_suggestion("prefix with _")
871            .at_range(5, 6);
872        assert_eq!(w.code, "unused-variable");
873        assert!(w.suggestion.is_some());
874        assert_eq!(w.start, 5);
875    }
876    #[test]
877    fn test_lint_report() {
878        let mut report = LintReport::new();
879        report.add(LintWarning::new("code1", "msg1"));
880        report.add(LintWarning::new("code2", "msg2"));
881        assert_eq!(report.len(), 2);
882        assert_eq!(report.by_code("code1").len(), 1);
883        let out = report.format_all();
884        assert!(out.contains("[code1]"));
885    }
886    #[test]
887    fn test_error_with_fix_apply() {
888        let e = ErrorWithFix::new("replace x", 4, 5, "y");
889        let result = e.apply("fun x -> x");
890        assert!(result.contains('y'));
891    }
892    #[test]
893    fn test_multi_file_errors() {
894        let mut mfe = MultiFileErrors::new();
895        mfe.add("a.lean", LocatedError::new("err1", 0, 1, 1, 1));
896        mfe.add("b.lean", LocatedError::new("err2", 0, 1, 2, 1));
897        assert_eq!(mfe.total(), 2);
898        assert_eq!(mfe.get("a.lean").len(), 1);
899    }
900    #[test]
901    fn test_error_range() {
902        let r1 = ErrorRange::new(0, 5);
903        let r2 = ErrorRange::new(3, 8);
904        assert!(r1.overlaps(&r2));
905        let r3 = ErrorRange::new(5, 10);
906        assert!(!r1.overlaps(&r3));
907        assert_eq!(r1.len(), 5);
908    }
909    #[test]
910    fn test_format_caret_range() {
911        let s = format_caret_range(3, 4);
912        assert_eq!(s, "  ^^^^");
913    }
914}
915/// Checks if two errors have the same message after normalisation.
916#[allow(dead_code)]
917#[allow(missing_docs)]
918pub fn errors_have_same_message(a: &LocatedError, b: &LocatedError) -> bool {
919    let norm_a: String = a.message.split_whitespace().collect::<Vec<_>>().join(" ");
920    let norm_b: String = b.message.split_whitespace().collect::<Vec<_>>().join(" ");
921    norm_a == norm_b
922}
923#[cfg(test)]
924mod error_impl_ext3_tests {
925    use super::*;
926    use crate::error_impl::*;
927    use crate::tokens::TokenKind;
928    #[test]
929    fn test_error_template() {
930        let t = ErrorTemplate::new("expected {0} but found {1}");
931        let msg = t.format(&["Ident", "Nat"]);
932        assert_eq!(msg, "expected Ident but found Nat");
933    }
934    #[test]
935    fn test_spanned_error_overlaps() {
936        let err = SpannedError::new(100, "msg", 5, 10);
937        assert!(err.overlaps(7, 15));
938        assert!(!err.overlaps(0, 5));
939        assert!(!err.overlaps(10, 20));
940    }
941    #[test]
942    fn test_error_batch() {
943        let mut batch = ErrorBatch::new();
944        batch.add(SpannedError::new(100, "msg1", 0, 5));
945        batch.add(SpannedError::new(100, "msg2", 6, 10));
946        batch.add(SpannedError::new(200, "msg3", 0, 3));
947        assert_eq!(batch.total(), 3);
948        assert_eq!(batch.get(100).len(), 2);
949        assert_eq!(batch.get(200).len(), 1);
950    }
951    #[test]
952    fn test_recoverable_error() {
953        let err = RecoverableError::new("unexpected token")
954            .suggest("try adding a semicolon")
955            .mark_recovered();
956        assert!(err.recovered);
957        assert_eq!(err.suggestions.len(), 1);
958    }
959    #[test]
960    fn test_errors_have_same_message() {
961        let e1 = LocatedError::new("foo   bar", 0, 1, 1, 1);
962        let e2 = LocatedError::new("foo bar", 0, 1, 2, 1);
963        assert!(errors_have_same_message(&e1, &e2));
964    }
965}
966/// Partitions errors by line number.
967#[allow(dead_code)]
968#[allow(missing_docs)]
969pub fn partition_by_line(
970    errors: &[LocatedError],
971) -> std::collections::HashMap<usize, Vec<&LocatedError>> {
972    let mut map: std::collections::HashMap<usize, Vec<&LocatedError>> =
973        std::collections::HashMap::new();
974    for err in errors {
975        map.entry(err.line).or_default().push(err);
976    }
977    map
978}
979#[cfg(test)]
980mod error_window_tests {
981    use super::*;
982    use crate::error_impl::*;
983    use crate::tokens::TokenKind;
984    #[test]
985    fn test_error_window() {
986        let mut w = ErrorWindow::new(2);
987        w.push(LocatedError::new("e1", 0, 1, 1, 1));
988        w.push(LocatedError::new("e2", 0, 1, 2, 1));
989        w.push(LocatedError::new("e3", 0, 1, 3, 1));
990        assert!(w.truncated);
991        assert_eq!(w.shown.len(), 2);
992        assert!(w.summary().contains("more omitted"));
993    }
994    #[test]
995    fn test_partition_by_line() {
996        let errs = vec![
997            LocatedError::new("e1", 0, 1, 1, 1),
998            LocatedError::new("e2", 0, 1, 1, 2),
999            LocatedError::new("e3", 0, 1, 2, 1),
1000        ];
1001        let partitioned = partition_by_line(&errs);
1002        assert_eq!(partitioned[&1].len(), 2);
1003        assert_eq!(partitioned[&2].len(), 1);
1004    }
1005}
1006/// Format a list of errors as a numbered list.
1007#[allow(dead_code)]
1008#[allow(missing_docs)]
1009pub fn numbered_error_list(errors: &[LocatedError]) -> String {
1010    errors
1011        .iter()
1012        .enumerate()
1013        .map(|(i, e)| format!("{}. {}", i + 1, e.format()))
1014        .collect::<Vec<_>>()
1015        .join("\n")
1016}
1017#[cfg(test)]
1018mod error_chain_tests {
1019    use super::*;
1020    use crate::error_impl::*;
1021    use crate::tokens::TokenKind;
1022    #[test]
1023    fn test_error_chain_ext() {
1024        let e = ErrorChainExt::new("while parsing def", "unexpected token");
1025        assert_eq!(e.format(), "while parsing def: unexpected token");
1026    }
1027    #[test]
1028    fn test_numbered_error_list() {
1029        let errs = vec![
1030            LocatedError::new("e1", 0, 1, 1, 1),
1031            LocatedError::new("e2", 0, 1, 2, 1),
1032        ];
1033        let out = numbered_error_list(&errs);
1034        assert!(out.contains("1."));
1035        assert!(out.contains("2."));
1036    }
1037}
1038/// Returns the total byte span covered by a list of errors.
1039#[allow(dead_code)]
1040#[allow(missing_docs)]
1041pub fn total_span(errors: &[LocatedError]) -> usize {
1042    errors.iter().map(|e| e.end.saturating_sub(e.start)).sum()
1043}
1044/// Returns the error with the widest span.
1045#[allow(dead_code)]
1046#[allow(missing_docs)]
1047pub fn widest_error(errors: &[LocatedError]) -> Option<&LocatedError> {
1048    errors.iter().max_by_key(|e| e.end.saturating_sub(e.start))
1049}
1050#[cfg(test)]
1051mod error_impl_pad {
1052    use super::*;
1053    use crate::error_impl::*;
1054    use crate::tokens::TokenKind;
1055    #[test]
1056    fn test_total_span() {
1057        let e = vec![
1058            LocatedError::new("a", 0, 5, 1, 1),
1059            LocatedError::new("b", 5, 10, 2, 1),
1060        ];
1061        assert_eq!(total_span(&e), 10);
1062    }
1063    #[test]
1064    fn test_widest_error() {
1065        let e = vec![
1066            LocatedError::new("a", 0, 3, 1, 1),
1067            LocatedError::new("b", 0, 10, 2, 1),
1068        ];
1069        assert_eq!(
1070            widest_error(&e).expect("test operation should succeed").end,
1071            10
1072        );
1073    }
1074}
1075/// Checks if an error message matches a substring.
1076#[allow(dead_code)]
1077#[allow(missing_docs)]
1078pub fn error_contains(e: &LocatedError, s: &str) -> bool {
1079    e.message.contains(s)
1080}
1081/// Returns the line number of a located error.
1082#[allow(dead_code)]
1083#[allow(missing_docs)]
1084pub fn error_line_ext2(e: &LocatedError) -> usize {
1085    e.line
1086}
1087/// Returns the column of a located error.
1088#[allow(dead_code)]
1089#[allow(missing_docs)]
1090pub fn error_col_ext2(e: &LocatedError) -> usize {
1091    e.col
1092}
1093#[cfg(test)]
1094mod error_impl_pad2 {
1095    use super::*;
1096    use crate::error_impl::*;
1097    use crate::tokens::TokenKind;
1098    #[test]
1099    fn test_error_contains() {
1100        let e = LocatedError::new("unexpected token", 0, 5, 1, 1);
1101        assert!(error_contains(&e, "unexpected"));
1102        assert!(!error_contains(&e, "missing"));
1103    }
1104}