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