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) 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}
209pub fn error_line(err: &ParseError) -> usize {
211 err.span.line
212}
213pub fn error_col(err: &ParseError) -> usize {
215 err.span.column
216}
217pub fn error_start(err: &ParseError) -> usize {
219 err.span.start
220}
221pub fn error_end(err: &ParseError) -> usize {
223 err.span.end
224}
225pub fn error_source_text<'a>(err: &ParseError, source: &'a str) -> &'a str {
227 source.get(err.span.start..err.span.end).unwrap_or("")
228}
229pub fn syntax_error(msg: impl Into<String>, span: Span) -> ParseError {
231 ParseError::new(ParseErrorKind::InvalidSyntax(msg.into()), span)
232}
233pub fn duplicate_error(name: impl Into<String>, span: Span) -> ParseError {
235 ParseError::new(ParseErrorKind::DuplicateDeclaration(name.into()), span)
236}
237pub fn binder_error(msg: impl Into<String>, span: Span) -> ParseError {
239 ParseError::new(ParseErrorKind::InvalidBinder(msg.into()), span)
240}
241pub fn pattern_error(msg: impl Into<String>, span: Span) -> ParseError {
243 ParseError::new(ParseErrorKind::InvalidPattern(msg.into()), span)
244}
245pub fn universe_error(msg: impl Into<String>, span: Span) -> ParseError {
247 ParseError::new(ParseErrorKind::InvalidUniverse(msg.into()), span)
248}
249pub fn other_error(msg: impl Into<String>, span: Span) -> ParseError {
251 ParseError::new(ParseErrorKind::Other(msg.into()), span)
252}
253pub 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}
265pub 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#[allow(dead_code)]
364pub fn is_duplicate_error(err: &ParseError) -> bool {
365 matches!(err.kind, ParseErrorKind::DuplicateDeclaration(_))
366}
367#[allow(dead_code)]
369pub fn is_syntax_error(err: &ParseError) -> bool {
370 matches!(err.kind, ParseErrorKind::InvalidSyntax(_))
371}
372#[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#[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#[allow(dead_code)]
401pub fn span_len(err: &ParseError) -> usize {
402 err.span.end.saturating_sub(err.span.start)
403}
404#[allow(dead_code)]
406pub fn span_is_empty(err: &ParseError) -> bool {
407 span_len(err) == 0
408}
409#[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#[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#[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#[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#[allow(dead_code)]
449pub fn sort_errors(errs: &mut [ParseError]) {
450 errs.sort_by_key(|e| e.span.start);
451}
452#[allow(dead_code)]
454pub fn earliest_error(errs: &[ParseError]) -> Option<&ParseError> {
455 errs.iter().min_by_key(|e| e.span.start)
456}
457#[allow(dead_code)]
459pub fn latest_error(errs: &[ParseError]) -> Option<&ParseError> {
460 errs.iter().max_by_key(|e| e.span.start)
461}
462#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[allow(dead_code)]
1072#[allow(missing_docs)]
1073pub fn error_contains(e: &LocatedError, s: &str) -> bool {
1074 e.message.contains(s)
1075}
1076#[allow(dead_code)]
1078#[allow(missing_docs)]
1079pub fn error_line_ext2(e: &LocatedError) -> usize {
1080 e.line
1081}
1082#[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}