1use 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}
36pub 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}
50pub 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}
61pub 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}
70pub 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}
78pub 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}
214pub fn error_line(err: &ParseError) -> usize {
216 err.span.line
217}
218pub fn error_col(err: &ParseError) -> usize {
220 err.span.column
221}
222pub fn error_start(err: &ParseError) -> usize {
224 err.span.start
225}
226pub fn error_end(err: &ParseError) -> usize {
228 err.span.end
229}
230pub fn error_source_text<'a>(err: &ParseError, source: &'a str) -> &'a str {
232 source.get(err.span.start..err.span.end).unwrap_or("")
233}
234pub fn syntax_error(msg: impl Into<String>, span: Span) -> ParseError {
236 ParseError::new(ParseErrorKind::InvalidSyntax(msg.into()), span)
237}
238pub fn duplicate_error(name: impl Into<String>, span: Span) -> ParseError {
240 ParseError::new(ParseErrorKind::DuplicateDeclaration(name.into()), span)
241}
242pub fn binder_error(msg: impl Into<String>, span: Span) -> ParseError {
244 ParseError::new(ParseErrorKind::InvalidBinder(msg.into()), span)
245}
246pub fn pattern_error(msg: impl Into<String>, span: Span) -> ParseError {
248 ParseError::new(ParseErrorKind::InvalidPattern(msg.into()), span)
249}
250pub fn universe_error(msg: impl Into<String>, span: Span) -> ParseError {
252 ParseError::new(ParseErrorKind::InvalidUniverse(msg.into()), span)
253}
254pub fn other_error(msg: impl Into<String>, span: Span) -> ParseError {
256 ParseError::new(ParseErrorKind::Other(msg.into()), span)
257}
258pub 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}
270pub 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#[allow(dead_code)]
369pub fn is_duplicate_error(err: &ParseError) -> bool {
370 matches!(err.kind, ParseErrorKind::DuplicateDeclaration(_))
371}
372#[allow(dead_code)]
374pub fn is_syntax_error(err: &ParseError) -> bool {
375 matches!(err.kind, ParseErrorKind::InvalidSyntax(_))
376}
377#[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#[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#[allow(dead_code)]
406pub fn span_len(err: &ParseError) -> usize {
407 err.span.end.saturating_sub(err.span.start)
408}
409#[allow(dead_code)]
411pub fn span_is_empty(err: &ParseError) -> bool {
412 span_len(err) == 0
413}
414#[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#[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#[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#[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#[allow(dead_code)]
454pub fn sort_errors(errs: &mut [ParseError]) {
455 errs.sort_by_key(|e| e.span.start);
456}
457#[allow(dead_code)]
459pub fn earliest_error(errs: &[ParseError]) -> Option<&ParseError> {
460 errs.iter().min_by_key(|e| e.span.start)
461}
462#[allow(dead_code)]
464pub fn latest_error(errs: &[ParseError]) -> Option<&ParseError> {
465 errs.iter().max_by_key(|e| e.span.start)
466}
467#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[allow(dead_code)]
1077#[allow(missing_docs)]
1078pub fn error_contains(e: &LocatedError, s: &str) -> bool {
1079 e.message.contains(s)
1080}
1081#[allow(dead_code)]
1083#[allow(missing_docs)]
1084pub fn error_line_ext2(e: &LocatedError) -> usize {
1085 e.line
1086}
1087#[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}