Skip to main content

oxilean_parse/diagnostic/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::tokens::{Token, TokenKind};
6
7use super::types::{DiagnosticCode, SyncToken};
8
9/// Find the next synchronization token starting from position `from`.
10///
11/// Scans the token slice for a token that matches a common synchronization
12/// point (semicolon, end keyword, declaration keyword, closing brackets, or EOF).
13/// Returns the index of the found sync token, or `tokens.len()` if none found.
14#[allow(dead_code)]
15pub fn find_sync_token(tokens: &[Token], from: usize) -> usize {
16    for (i, token) in tokens.iter().enumerate().skip(from) {
17        if is_sync_kind(&token.kind) {
18            return i;
19        }
20    }
21    tokens.len()
22}
23/// Skip tokens until a specific sync token type is found.
24///
25/// Returns the index of the found sync token, or `tokens.len()` if not found.
26#[allow(dead_code)]
27pub fn skip_to_sync(tokens: &[Token], from: usize, sync: SyncToken) -> usize {
28    for (i, token) in tokens.iter().enumerate().skip(from) {
29        if matches_sync(&token.kind, sync) {
30            return i;
31        }
32    }
33    tokens.len()
34}
35/// Check if a token kind is any synchronization point.
36pub(super) fn is_sync_kind(kind: &TokenKind) -> bool {
37    matches!(
38        kind,
39        TokenKind::Semicolon
40            | TokenKind::End
41            | TokenKind::Eof
42            | TokenKind::RBrace
43            | TokenKind::RParen
44            | TokenKind::Definition
45            | TokenKind::Theorem
46            | TokenKind::Lemma
47            | TokenKind::Axiom
48            | TokenKind::Inductive
49            | TokenKind::Structure
50            | TokenKind::Class
51            | TokenKind::Instance
52    )
53}
54/// Check if a token kind matches a specific sync token.
55pub(super) fn matches_sync(kind: &TokenKind, sync: SyncToken) -> bool {
56    match sync {
57        SyncToken::Semicolon => matches!(kind, TokenKind::Semicolon),
58        SyncToken::End => matches!(kind, TokenKind::End),
59        SyncToken::Declaration => {
60            matches!(
61                kind,
62                TokenKind::Definition
63                    | TokenKind::Theorem
64                    | TokenKind::Lemma
65                    | TokenKind::Axiom
66                    | TokenKind::Inductive
67                    | TokenKind::Structure
68                    | TokenKind::Class
69                    | TokenKind::Instance
70            )
71        }
72        SyncToken::RightBrace => matches!(kind, TokenKind::RBrace),
73        SyncToken::RightParen => matches!(kind, TokenKind::RParen),
74        SyncToken::Eof => matches!(kind, TokenKind::Eof),
75    }
76}
77/// Suggest a correction when the wrong token is found.
78///
79/// Returns a human-readable suggestion string if a common mistake is detected.
80#[allow(dead_code)]
81pub fn suggest_token(expected: &TokenKind, found: &TokenKind) -> Option<String> {
82    match (expected, found) {
83        (TokenKind::RParen, TokenKind::RBracket) => {
84            Some("did you mean `)` instead of `]`?".to_string())
85        }
86        (TokenKind::RParen, TokenKind::RBrace) => {
87            Some("did you mean `)` instead of `}`?".to_string())
88        }
89        (TokenKind::RBrace, TokenKind::RParen) => {
90            Some("did you mean `}` instead of `)`?".to_string())
91        }
92        (TokenKind::RBrace, TokenKind::RBracket) => {
93            Some("did you mean `}` instead of `]`?".to_string())
94        }
95        (TokenKind::RBracket, TokenKind::RParen) => {
96            Some("did you mean `]` instead of `)`?".to_string())
97        }
98        (TokenKind::RBracket, TokenKind::RBrace) => {
99            Some("did you mean `]` instead of `}`?".to_string())
100        }
101        (TokenKind::Colon, TokenKind::Assign) => {
102            Some("did you mean `:` instead of `:=`?".to_string())
103        }
104        (TokenKind::Assign, TokenKind::Colon) => {
105            Some("did you mean `:=` instead of `:`?".to_string())
106        }
107        (TokenKind::Assign, TokenKind::Eq) => Some("did you mean `:=` instead of `=`?".to_string()),
108        (TokenKind::Arrow, TokenKind::Eq) => Some("did you mean `->` instead of `=`?".to_string()),
109        (TokenKind::Semicolon, TokenKind::Comma) => {
110            Some("did you mean `;` instead of `,`?".to_string())
111        }
112        (TokenKind::Comma, TokenKind::Semicolon) => {
113            Some("did you mean `,` instead of `;`?".to_string())
114        }
115        (TokenKind::Semicolon, _) => Some("missing `;`".to_string()),
116        _ => None,
117    }
118}
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::diagnostic::*;
123    use crate::tokens::Span;
124    fn dummy_span() -> Span {
125        Span::new(0, 1, 1, 1)
126    }
127    fn span_at(line: usize, col: usize, start: usize, end: usize) -> Span {
128        Span::new(start, end, line, col)
129    }
130    #[test]
131    fn test_error_diagnostic() {
132        let diag = Diagnostic::error("test error".to_string(), dummy_span());
133        assert!(diag.is_error());
134        assert!(!diag.is_warning());
135    }
136    #[test]
137    fn test_warning_diagnostic() {
138        let diag = Diagnostic::warning("test warning".to_string(), dummy_span());
139        assert!(diag.is_warning());
140        assert!(!diag.is_error());
141    }
142    #[test]
143    fn test_with_label() {
144        let diag = Diagnostic::error("test".to_string(), dummy_span())
145            .with_label("label".to_string(), dummy_span());
146        assert_eq!(diag.labels.len(), 1);
147    }
148    #[test]
149    fn test_with_help() {
150        let diag =
151            Diagnostic::error("test".to_string(), dummy_span()).with_help("help text".to_string());
152        assert!(diag.help.is_some());
153    }
154    #[test]
155    fn test_collector_create() {
156        let collector = DiagnosticCollector::new();
157        assert_eq!(collector.error_count(), 0);
158        assert!(!collector.has_errors());
159    }
160    #[test]
161    fn test_collector_add_error() {
162        let mut collector = DiagnosticCollector::new();
163        collector.add(Diagnostic::error("test".to_string(), dummy_span()));
164        assert_eq!(collector.error_count(), 1);
165        assert!(collector.has_errors());
166    }
167    #[test]
168    fn test_collector_add_warning() {
169        let mut collector = DiagnosticCollector::new();
170        collector.add(Diagnostic::warning("test".to_string(), dummy_span()));
171        assert_eq!(collector.warning_count(), 1);
172        assert!(!collector.has_errors());
173    }
174    #[test]
175    fn test_collector_clear() {
176        let mut collector = DiagnosticCollector::new();
177        collector.add(Diagnostic::error("test".to_string(), dummy_span()));
178        collector.clear();
179        assert_eq!(collector.error_count(), 0);
180    }
181    #[test]
182    fn test_with_code() {
183        let diag =
184            Diagnostic::error("test".to_string(), dummy_span()).with_code(DiagnosticCode::E0001);
185        assert_eq!(diag.code, Some(DiagnosticCode::E0001));
186    }
187    #[test]
188    fn test_with_fix() {
189        let fix = CodeFix {
190            message: "add semicolon".to_string(),
191            span: dummy_span(),
192            replacement: ";".to_string(),
193        };
194        let diag = Diagnostic::error("test".to_string(), dummy_span()).with_fix(fix);
195        assert_eq!(diag.fixes.len(), 1);
196        assert_eq!(diag.fixes[0].replacement, ";");
197    }
198    #[test]
199    fn test_note_diagnostic() {
200        let diag = Diagnostic::note("a note".to_string(), dummy_span());
201        assert_eq!(diag.severity, Severity::Info);
202        assert_eq!(diag.message, "a note");
203    }
204    #[test]
205    fn test_diagnostic_code_display() {
206        assert_eq!(format!("{}", DiagnosticCode::E0001), "E0001");
207        assert_eq!(format!("{}", DiagnosticCode::E0100), "E0100");
208        assert_eq!(format!("{}", DiagnosticCode::E0901), "E0901");
209    }
210    #[test]
211    fn test_diagnostic_display_with_code() {
212        let diag = Diagnostic::error("bad token".to_string(), dummy_span())
213            .with_code(DiagnosticCode::E0001);
214        let s = format!("{}", diag);
215        assert!(s.contains("E0001"));
216        assert!(s.contains("bad token"));
217    }
218    #[test]
219    fn test_diagnostic_display_without_code() {
220        let diag = Diagnostic::warning("unused var".to_string(), dummy_span());
221        let s = format!("{}", diag);
222        assert!(s.contains("warning"));
223        assert!(s.contains("unused var"));
224        assert!(!s.contains("["));
225    }
226    #[test]
227    fn test_format_line_highlight() {
228        let source = "let x := 42";
229        let span = span_at(1, 5, 4, 5);
230        let output = Diagnostic::format_line_highlight(source, &span);
231        assert!(output.contains("let x := 42"));
232        assert!(output.contains("^"));
233    }
234    #[test]
235    fn test_format_line_highlight_out_of_range() {
236        let source = "hello";
237        let span = span_at(5, 1, 0, 1);
238        let output = Diagnostic::format_line_highlight(source, &span);
239        assert!(output.is_empty());
240    }
241    #[test]
242    fn test_format_rich() {
243        let source = "let x := 42\nlet y := true";
244        let span = span_at(1, 5, 4, 5);
245        let diag = Diagnostic::error("type mismatch".to_string(), span)
246            .with_code(DiagnosticCode::E0100)
247            .with_help("expected Nat".to_string());
248        let output = diag.format_rich(source);
249        assert!(output.contains("error[E0100]"));
250        assert!(output.contains("type mismatch"));
251        assert!(output.contains("let x := 42"));
252        assert!(output.contains("expected Nat"));
253    }
254    #[test]
255    fn test_format_rich_with_fix() {
256        let source = "let x = 42";
257        let span = span_at(1, 7, 6, 7);
258        let fix = CodeFix {
259            message: "use `:=` for assignment".to_string(),
260            span: span_at(1, 7, 6, 7),
261            replacement: ":=".to_string(),
262        };
263        let diag = Diagnostic::error("unexpected `=`".to_string(), span).with_fix(fix);
264        let output = diag.format_rich(source);
265        assert!(output.contains("fix:"));
266        assert!(output.contains(":="));
267    }
268    #[test]
269    fn test_diagnostics_at() {
270        let mut collector = DiagnosticCollector::new();
271        collector.add(Diagnostic::error("err1".to_string(), span_at(1, 1, 0, 1)));
272        collector.add(Diagnostic::error("err2".to_string(), span_at(2, 1, 10, 11)));
273        collector.add(Diagnostic::warning(
274            "warn1".to_string(),
275            span_at(1, 5, 4, 5),
276        ));
277        let line1 = collector.diagnostics_at(1);
278        assert_eq!(line1.len(), 2);
279        let line2 = collector.diagnostics_at(2);
280        assert_eq!(line2.len(), 1);
281        let line3 = collector.diagnostics_at(3);
282        assert!(line3.is_empty());
283    }
284    #[test]
285    fn test_info_count() {
286        let mut collector = DiagnosticCollector::new();
287        collector.add(Diagnostic::error("e".to_string(), dummy_span()));
288        collector.add(Diagnostic::info("i1".to_string(), dummy_span()));
289        collector.add(Diagnostic::info("i2".to_string(), dummy_span()));
290        assert_eq!(collector.info_count(), 2);
291    }
292    #[test]
293    fn test_sort_by_severity() {
294        let mut collector = DiagnosticCollector::new();
295        collector.add(Diagnostic::warning("w".to_string(), dummy_span()));
296        collector.add(Diagnostic::error("e".to_string(), dummy_span()));
297        collector.add(Diagnostic::info("i".to_string(), dummy_span()));
298        collector.sort_by_severity();
299        let diags = collector.diagnostics();
300        assert_eq!(diags[0].severity, Severity::Error);
301        assert_eq!(diags[1].severity, Severity::Warning);
302        assert_eq!(diags[2].severity, Severity::Info);
303    }
304    #[test]
305    fn test_sort_by_position() {
306        let mut collector = DiagnosticCollector::new();
307        collector.add(Diagnostic::error(
308            "second".to_string(),
309            span_at(2, 1, 10, 11),
310        ));
311        collector.add(Diagnostic::error("first".to_string(), span_at(1, 1, 0, 1)));
312        collector.sort_by_position();
313        let diags = collector.diagnostics();
314        assert_eq!(diags[0].message, "first");
315        assert_eq!(diags[1].message, "second");
316    }
317    #[test]
318    fn test_filter_severity() {
319        let mut collector = DiagnosticCollector::new();
320        collector.add(Diagnostic::error("e1".to_string(), dummy_span()));
321        collector.add(Diagnostic::warning("w1".to_string(), dummy_span()));
322        collector.add(Diagnostic::error("e2".to_string(), dummy_span()));
323        let errors = collector.filter_severity(Severity::Error);
324        assert_eq!(errors.len(), 2);
325        let warnings = collector.filter_severity(Severity::Warning);
326        assert_eq!(warnings.len(), 1);
327    }
328    #[test]
329    fn test_merge() {
330        let mut collector1 = DiagnosticCollector::new();
331        collector1.add(Diagnostic::error("e1".to_string(), dummy_span()));
332        let mut collector2 = DiagnosticCollector::new();
333        collector2.add(Diagnostic::warning("w1".to_string(), dummy_span()));
334        collector2.add(Diagnostic::error("e2".to_string(), dummy_span()));
335        collector1.merge(&collector2);
336        assert_eq!(collector1.error_count(), 2);
337        assert_eq!(collector1.warning_count(), 1);
338        assert_eq!(collector1.diagnostics().len(), 3);
339    }
340    #[test]
341    fn test_summary_empty() {
342        let collector = DiagnosticCollector::new();
343        assert_eq!(collector.summary(), "no diagnostics");
344    }
345    #[test]
346    fn test_summary_errors_only() {
347        let mut collector = DiagnosticCollector::new();
348        collector.add(Diagnostic::error("e1".to_string(), dummy_span()));
349        assert_eq!(collector.summary(), "1 error");
350    }
351    #[test]
352    fn test_summary_mixed() {
353        let mut collector = DiagnosticCollector::new();
354        collector.add(Diagnostic::error("e1".to_string(), dummy_span()));
355        collector.add(Diagnostic::error("e2".to_string(), dummy_span()));
356        collector.add(Diagnostic::error("e3".to_string(), dummy_span()));
357        collector.add(Diagnostic::warning("w1".to_string(), dummy_span()));
358        collector.add(Diagnostic::warning("w2".to_string(), dummy_span()));
359        assert_eq!(collector.summary(), "3 errors, 2 warnings");
360    }
361    #[test]
362    fn test_summary_with_info() {
363        let mut collector = DiagnosticCollector::new();
364        collector.add(Diagnostic::info("i1".to_string(), dummy_span()));
365        assert_eq!(collector.summary(), "1 info");
366    }
367    #[test]
368    fn test_find_sync_token_semicolon() {
369        let tokens = vec![
370            Token::new(TokenKind::Ident("x".to_string()), dummy_span()),
371            Token::new(TokenKind::Eq, dummy_span()),
372            Token::new(TokenKind::Nat(42), dummy_span()),
373            Token::new(TokenKind::Semicolon, dummy_span()),
374        ];
375        assert_eq!(find_sync_token(&tokens, 0), 3);
376    }
377    #[test]
378    fn test_find_sync_token_not_found() {
379        let tokens = vec![
380            Token::new(TokenKind::Ident("x".to_string()), dummy_span()),
381            Token::new(TokenKind::Eq, dummy_span()),
382        ];
383        assert_eq!(find_sync_token(&tokens, 0), tokens.len());
384    }
385    #[test]
386    fn test_find_sync_token_eof() {
387        let tokens = vec![
388            Token::new(TokenKind::Ident("x".to_string()), dummy_span()),
389            Token::new(TokenKind::Eof, dummy_span()),
390        ];
391        assert_eq!(find_sync_token(&tokens, 0), 1);
392    }
393    #[test]
394    fn test_skip_to_sync_semicolon() {
395        let tokens = vec![
396            Token::new(TokenKind::Ident("a".to_string()), dummy_span()),
397            Token::new(TokenKind::Ident("b".to_string()), dummy_span()),
398            Token::new(TokenKind::Semicolon, dummy_span()),
399            Token::new(TokenKind::Ident("c".to_string()), dummy_span()),
400        ];
401        assert_eq!(skip_to_sync(&tokens, 0, SyncToken::Semicolon), 2);
402    }
403    #[test]
404    fn test_skip_to_sync_declaration() {
405        let tokens = vec![
406            Token::new(TokenKind::Ident("garbage".to_string()), dummy_span()),
407            Token::new(TokenKind::Definition, dummy_span()),
408        ];
409        assert_eq!(skip_to_sync(&tokens, 0, SyncToken::Declaration), 1);
410    }
411    #[test]
412    fn test_skip_to_sync_not_found() {
413        let tokens = vec![Token::new(TokenKind::Ident("a".to_string()), dummy_span())];
414        assert_eq!(skip_to_sync(&tokens, 0, SyncToken::Semicolon), tokens.len());
415    }
416    #[test]
417    fn test_suggest_token_paren_bracket() {
418        let suggestion = suggest_token(&TokenKind::RParen, &TokenKind::RBracket);
419        assert!(suggestion.is_some());
420        assert!(suggestion
421            .expect("test operation should succeed")
422            .contains(")"));
423    }
424    #[test]
425    fn test_suggest_token_assign_eq() {
426        let suggestion = suggest_token(&TokenKind::Assign, &TokenKind::Eq);
427        assert!(suggestion.is_some());
428        assert!(suggestion
429            .expect("test operation should succeed")
430            .contains(":="));
431    }
432    #[test]
433    fn test_suggest_token_no_suggestion() {
434        let suggestion = suggest_token(&TokenKind::Ident("x".to_string()), &TokenKind::Nat(42));
435        assert!(suggestion.is_none());
436    }
437    #[test]
438    fn test_suggest_token_missing_semicolon() {
439        let suggestion = suggest_token(&TokenKind::Semicolon, &TokenKind::Ident("x".to_string()));
440        assert!(suggestion.is_some());
441        assert!(suggestion
442            .expect("test operation should succeed")
443            .contains(";"));
444    }
445    #[test]
446    fn test_severity_ordering() {
447        assert!(Severity::Error < Severity::Warning);
448        assert!(Severity::Warning < Severity::Info);
449        assert!(Severity::Info < Severity::Hint);
450    }
451    #[test]
452    fn test_multiple_fixes() {
453        let fix1 = CodeFix {
454            message: "fix1".to_string(),
455            span: dummy_span(),
456            replacement: "a".to_string(),
457        };
458        let fix2 = CodeFix {
459            message: "fix2".to_string(),
460            span: dummy_span(),
461            replacement: "b".to_string(),
462        };
463        let diag = Diagnostic::error("test".to_string(), dummy_span())
464            .with_fix(fix1)
465            .with_fix(fix2);
466        assert_eq!(diag.fixes.len(), 2);
467    }
468    #[test]
469    fn test_sort_by_position_same_line() {
470        let mut collector = DiagnosticCollector::new();
471        collector.add(Diagnostic::error(
472            "later".to_string(),
473            span_at(1, 10, 9, 10),
474        ));
475        collector.add(Diagnostic::error(
476            "earlier".to_string(),
477            span_at(1, 1, 0, 1),
478        ));
479        collector.sort_by_position();
480        let diags = collector.diagnostics();
481        assert_eq!(diags[0].message, "earlier");
482        assert_eq!(diags[1].message, "later");
483    }
484}
485/// Return a short human-readable description for each `DiagnosticCode`.
486#[allow(dead_code)]
487pub fn code_description(code: DiagnosticCode) -> &'static str {
488    match code {
489        DiagnosticCode::E0001 => "unexpected token",
490        DiagnosticCode::E0002 => "unterminated string literal",
491        DiagnosticCode::E0003 => "unmatched bracket",
492        DiagnosticCode::E0004 => "missing semicolon",
493        DiagnosticCode::E0005 => "invalid number literal",
494        DiagnosticCode::E0100 => "type mismatch",
495        DiagnosticCode::E0101 => "undeclared variable",
496        DiagnosticCode::E0102 => "cannot infer type",
497        DiagnosticCode::E0103 => "too many arguments",
498        DiagnosticCode::E0104 => "too few arguments",
499        DiagnosticCode::E0200 => "no goals to solve",
500        DiagnosticCode::E0201 => "tactic failed",
501        DiagnosticCode::E0202 => "unsolved goals",
502        DiagnosticCode::E0900 => "internal error",
503        DiagnosticCode::E0901 => "not implemented",
504    }
505}
506/// Return a suggested fix hint for each `DiagnosticCode`.
507#[allow(dead_code)]
508pub fn code_hint(code: DiagnosticCode) -> Option<&'static str> {
509    match code {
510        DiagnosticCode::E0001 => Some("check that the token is valid in this position"),
511        DiagnosticCode::E0002 => Some("add a closing `\"` to terminate the string"),
512        DiagnosticCode::E0003 => Some("ensure brackets are properly matched and closed"),
513        DiagnosticCode::E0004 => Some("add a `;` after the statement"),
514        DiagnosticCode::E0005 => Some("only decimal digits are allowed in number literals"),
515        DiagnosticCode::E0100 => Some("check the expected type of this expression"),
516        DiagnosticCode::E0101 => Some("declare the variable or check spelling"),
517        DiagnosticCode::E0102 => Some("add a type annotation, e.g. `(x : Nat)`"),
518        DiagnosticCode::E0103 => Some("remove extra arguments"),
519        DiagnosticCode::E0104 => Some("provide missing arguments"),
520        DiagnosticCode::E0200 => Some("remove the extra tactic step"),
521        DiagnosticCode::E0201 => Some("check tactic preconditions and goal state"),
522        DiagnosticCode::E0202 => Some("close all goals before finishing the proof"),
523        DiagnosticCode::E0900 => None,
524        DiagnosticCode::E0901 => Some("this feature is not yet implemented"),
525    }
526}
527/// Return all defined diagnostic codes.
528#[allow(dead_code)]
529pub fn all_codes() -> &'static [DiagnosticCode] {
530    &[
531        DiagnosticCode::E0001,
532        DiagnosticCode::E0002,
533        DiagnosticCode::E0003,
534        DiagnosticCode::E0004,
535        DiagnosticCode::E0005,
536        DiagnosticCode::E0100,
537        DiagnosticCode::E0101,
538        DiagnosticCode::E0102,
539        DiagnosticCode::E0103,
540        DiagnosticCode::E0104,
541        DiagnosticCode::E0200,
542        DiagnosticCode::E0201,
543        DiagnosticCode::E0202,
544        DiagnosticCode::E0900,
545        DiagnosticCode::E0901,
546    ]
547}
548#[cfg(test)]
549mod extra_tests {
550    use super::*;
551    use crate::diagnostic::*;
552    use crate::tokens::Span;
553    fn dummy_span() -> Span {
554        Span::new(0, 1, 1, 1)
555    }
556    fn span_at(line: usize, col: usize, start: usize, end: usize) -> Span {
557        Span::new(start, end, line, col)
558    }
559    #[test]
560    fn test_renderer_renders_error() {
561        let source = "let x := bad";
562        let renderer = DiagnosticRenderer::new(source);
563        let diag = Diagnostic::error("test error".to_string(), span_at(1, 1, 0, 3));
564        let output = renderer.render(&diag);
565        assert!(output.contains("error"));
566        assert!(output.contains("test error"));
567    }
568    #[test]
569    fn test_renderer_renders_all() {
570        let renderer = DiagnosticRenderer::new("code here");
571        let diags = vec![
572            Diagnostic::error("e1".to_string(), dummy_span()),
573            Diagnostic::warning("w1".to_string(), dummy_span()),
574        ];
575        let output = renderer.render_all(&diags);
576        assert!(output.contains("error"));
577        assert!(output.contains("warning"));
578    }
579    #[test]
580    fn test_renderer_errors_only() {
581        let renderer = DiagnosticRenderer::new("code");
582        let mut c = DiagnosticCollector::new();
583        c.add(Diagnostic::error("err".to_string(), dummy_span()));
584        c.add(Diagnostic::warning("warn".to_string(), dummy_span()));
585        let output = renderer.render_errors(&c);
586        assert!(output.contains("error"));
587        assert!(!output.contains("warning"));
588    }
589    #[test]
590    fn test_filter_with_code() {
591        let mut c = DiagnosticCollector::new();
592        c.add(Diagnostic::error("e".to_string(), dummy_span()).with_code(DiagnosticCode::E0001));
593        c.add(Diagnostic::warning("w".to_string(), dummy_span()));
594        let f = DiagnosticFilter::new(&c);
595        assert_eq!(f.with_code(DiagnosticCode::E0001).len(), 1);
596        assert_eq!(f.with_code(DiagnosticCode::E0002).len(), 0);
597    }
598    #[test]
599    fn test_filter_message_contains() {
600        let mut c = DiagnosticCollector::new();
601        c.add(Diagnostic::error(
602            "type mismatch here".to_string(),
603            dummy_span(),
604        ));
605        c.add(Diagnostic::error("bad token".to_string(), dummy_span()));
606        let f = DiagnosticFilter::new(&c);
607        assert_eq!(f.message_contains("mismatch").len(), 1);
608    }
609    #[test]
610    fn test_filter_in_line_range() {
611        let mut c = DiagnosticCollector::new();
612        c.add(Diagnostic::error("e1".to_string(), span_at(1, 1, 0, 1)));
613        c.add(Diagnostic::error("e2".to_string(), span_at(5, 1, 10, 11)));
614        let f = DiagnosticFilter::new(&c);
615        assert_eq!(f.in_line_range(1, 3).len(), 1);
616        assert_eq!(f.in_line_range(1, 10).len(), 2);
617    }
618    #[test]
619    fn test_filter_with_fixes() {
620        let mut c = DiagnosticCollector::new();
621        let fix = CodeFix {
622            message: "fix".to_string(),
623            span: dummy_span(),
624            replacement: ";".to_string(),
625        };
626        c.add(Diagnostic::error("e".to_string(), dummy_span()).with_fix(fix));
627        c.add(Diagnostic::warning("w".to_string(), dummy_span()));
628        let f = DiagnosticFilter::new(&c);
629        assert_eq!(f.with_fixes().len(), 1);
630    }
631    #[test]
632    fn test_aggregator_totals() {
633        let mut agg = DiagnosticAggregator::new("test");
634        let mut c1 = DiagnosticCollector::new();
635        c1.add(Diagnostic::error("e1".to_string(), dummy_span()));
636        let mut c2 = DiagnosticCollector::new();
637        c2.add(Diagnostic::warning("w1".to_string(), dummy_span()));
638        c2.add(Diagnostic::warning("w2".to_string(), dummy_span()));
639        agg.add_collector(c1);
640        agg.add_collector(c2);
641        assert_eq!(agg.total_errors(), 1);
642        assert_eq!(agg.total_warnings(), 2);
643        assert_eq!(agg.total_count(), 3);
644        assert!(agg.has_errors());
645    }
646    #[test]
647    fn test_aggregator_flat_sorted() {
648        let mut agg = DiagnosticAggregator::new("sort_test");
649        let mut c = DiagnosticCollector::new();
650        c.add(Diagnostic::error("e2".to_string(), span_at(3, 1, 10, 11)));
651        c.add(Diagnostic::error("e1".to_string(), span_at(1, 1, 0, 1)));
652        agg.add_collector(c);
653        let sorted = agg.flat_sorted();
654        assert_eq!(sorted[0].message, "e1");
655        assert_eq!(sorted[1].message, "e2");
656    }
657    #[test]
658    fn test_builder_error() {
659        let d = DiagnosticBuilder::error("err msg", dummy_span())
660            .code(DiagnosticCode::E0001)
661            .help("try this")
662            .build();
663        assert!(d.is_error());
664        assert_eq!(d.code, Some(DiagnosticCode::E0001));
665        assert!(d.help.is_some());
666    }
667    #[test]
668    fn test_builder_warning_with_fix() {
669        let d = DiagnosticBuilder::warning("warn", dummy_span())
670            .fix("add semicolon", dummy_span(), ";")
671            .build();
672        assert!(d.is_warning());
673        assert_eq!(d.fixes.len(), 1);
674    }
675    #[test]
676    fn test_builder_label() {
677        let d = DiagnosticBuilder::info("info", dummy_span())
678            .label("here", dummy_span())
679            .build();
680        assert_eq!(d.labels.len(), 1);
681    }
682    #[test]
683    fn test_group_counts() {
684        let mut g = DiagnosticGroup::new("file.ox");
685        g.add(Diagnostic::error("e".to_string(), dummy_span()));
686        g.add(Diagnostic::warning("w".to_string(), dummy_span()));
687        assert_eq!(g.error_count(), 1);
688        assert_eq!(g.warning_count(), 1);
689        assert_eq!(g.len(), 2);
690        assert!(g.has_errors());
691    }
692    #[test]
693    fn test_group_sort() {
694        let mut g = DiagnosticGroup::new("g");
695        g.add(Diagnostic::error("b".to_string(), span_at(3, 1, 10, 11)));
696        g.add(Diagnostic::error("a".to_string(), span_at(1, 1, 0, 1)));
697        g.sort_by_position();
698        assert_eq!(g.diagnostics[0].message, "a");
699    }
700    #[test]
701    fn test_span_contains() {
702        let outer = span_at(1, 1, 0, 10);
703        let inner = span_at(1, 3, 2, 5);
704        assert!(SpanUtils::contains(&outer, &inner));
705        assert!(!SpanUtils::contains(&inner, &outer));
706    }
707    #[test]
708    fn test_span_overlaps() {
709        let a = span_at(1, 1, 0, 5);
710        let b = span_at(1, 4, 3, 8);
711        assert!(SpanUtils::overlaps(&a, &b));
712        let c = span_at(1, 9, 8, 10);
713        assert!(!SpanUtils::overlaps(&a, &c));
714    }
715    #[test]
716    fn test_span_merge() {
717        let a = span_at(1, 1, 0, 5);
718        let b = span_at(1, 4, 3, 10);
719        let merged = SpanUtils::merge(&a, &b);
720        assert_eq!(merged.start, 0);
721        assert_eq!(merged.end, 10);
722    }
723    #[test]
724    fn test_span_byte_len() {
725        let s = span_at(1, 1, 5, 10);
726        assert_eq!(SpanUtils::byte_len(&s), 5);
727    }
728    #[test]
729    fn test_span_is_empty() {
730        assert!(SpanUtils::is_empty(&span_at(1, 1, 5, 5)));
731        assert!(!SpanUtils::is_empty(&span_at(1, 1, 5, 6)));
732    }
733    #[test]
734    fn test_span_extract() {
735        let src = "hello world";
736        let s = span_at(1, 7, 6, 11);
737        assert_eq!(SpanUtils::extract(&s, src), "world");
738    }
739    #[test]
740    fn test_stats_from_collector() {
741        let mut c = DiagnosticCollector::new();
742        c.add(Diagnostic::error("e".to_string(), dummy_span()));
743        c.add(Diagnostic::warning("w".to_string(), dummy_span()).with_help("h".to_string()));
744        let stats = DiagnosticStats::from_collector(&c);
745        assert_eq!(stats.errors, 1);
746        assert_eq!(stats.warnings, 1);
747        assert_eq!(stats.with_help, 1);
748    }
749    #[test]
750    fn test_stats_total_and_has_errors() {
751        let mut c = DiagnosticCollector::new();
752        c.add(Diagnostic::error("e".to_string(), dummy_span()));
753        let s = DiagnosticStats::from_collector(&c);
754        assert_eq!(s.total(), 1);
755        assert!(s.has_errors());
756    }
757    #[test]
758    fn test_code_description() {
759        assert!(!code_description(DiagnosticCode::E0001).is_empty());
760        assert!(!code_description(DiagnosticCode::E0100).is_empty());
761    }
762    #[test]
763    fn test_code_hint_some() {
764        assert!(code_hint(DiagnosticCode::E0001).is_some());
765    }
766    #[test]
767    fn test_code_hint_none() {
768        assert!(code_hint(DiagnosticCode::E0900).is_none());
769    }
770    #[test]
771    fn test_all_codes_non_empty() {
772        assert!(!all_codes().is_empty());
773    }
774    #[test]
775    fn test_exporter_to_json() {
776        let d = Diagnostic::error("bad token".to_string(), dummy_span())
777            .with_code(DiagnosticCode::E0001);
778        let json = DiagnosticExporter::to_json(&d);
779        assert!(json.contains("error"));
780        assert!(json.contains("E0001"));
781    }
782    #[test]
783    fn test_exporter_collector_to_json() {
784        let mut c = DiagnosticCollector::new();
785        c.add(Diagnostic::error("e".to_string(), dummy_span()));
786        let json = DiagnosticExporter::collector_to_json(&c);
787        assert!(json.starts_with('['));
788        assert!(json.ends_with(']'));
789    }
790    #[test]
791    fn test_exporter_to_oneliner() {
792        let d = Diagnostic::warning("unused".to_string(), span_at(3, 7, 0, 1));
793        let s = DiagnosticExporter::to_oneliner(&d);
794        assert!(s.contains("3:7"));
795        assert!(s.contains("warning"));
796    }
797    #[test]
798    fn test_exporter_to_csv() {
799        let d = Diagnostic::error("bad".to_string(), span_at(2, 5, 0, 1));
800        let csv = DiagnosticExporter::to_csv(&d);
801        assert!(csv.contains("2,5,error"));
802    }
803    #[test]
804    fn test_exporter_collector_to_csv() {
805        let mut c = DiagnosticCollector::new();
806        c.add(Diagnostic::error("e".to_string(), dummy_span()));
807        let csv = DiagnosticExporter::collector_to_csv(&c);
808        assert!(csv.starts_with("line,col,severity,message"));
809    }
810    #[test]
811    fn test_suppressor_code() {
812        let sup = DiagnosticSuppressor::new().suppress_code(DiagnosticCode::E0001);
813        let d = Diagnostic::error("e".to_string(), dummy_span()).with_code(DiagnosticCode::E0001);
814        assert!(sup.should_suppress(&d));
815        let d2 = Diagnostic::error("e".to_string(), dummy_span()).with_code(DiagnosticCode::E0002);
816        assert!(!sup.should_suppress(&d2));
817    }
818    #[test]
819    fn test_suppressor_warnings() {
820        let sup = DiagnosticSuppressor::new().suppress_all_warnings();
821        let d = Diagnostic::warning("w".to_string(), dummy_span());
822        assert!(sup.should_suppress(&d));
823        let d2 = Diagnostic::error("e".to_string(), dummy_span());
824        assert!(!sup.should_suppress(&d2));
825    }
826    #[test]
827    fn test_suppressor_filter_collector() {
828        let mut c = DiagnosticCollector::new();
829        c.add(Diagnostic::error("e".to_string(), dummy_span()).with_code(DiagnosticCode::E0001));
830        c.add(Diagnostic::warning("w".to_string(), dummy_span()));
831        let sup = DiagnosticSuppressor::new().suppress_code(DiagnosticCode::E0001);
832        let filtered = sup.filter_collector(&c);
833        assert_eq!(filtered.diagnostics().len(), 1);
834        assert!(filtered.diagnostics()[0].is_warning());
835    }
836    #[test]
837    fn test_policy_fail_fast() {
838        let mut c = DiagnosticCollector::new();
839        c.add(Diagnostic::error("e".to_string(), dummy_span()));
840        assert!(DiagnosticPolicy::FailFast.should_fail(&c));
841    }
842    #[test]
843    fn test_policy_permissive() {
844        let mut c = DiagnosticCollector::new();
845        c.add(Diagnostic::error("e".to_string(), dummy_span()));
846        assert!(!DiagnosticPolicy::Permissive.should_fail(&c));
847    }
848    #[test]
849    fn test_policy_warnings_as_errors() {
850        let mut c = DiagnosticCollector::new();
851        c.add(Diagnostic::warning("w".to_string(), dummy_span()));
852        assert!(DiagnosticPolicy::WarningsAsErrors.should_fail(&c));
853    }
854    #[test]
855    fn test_policy_name() {
856        assert_eq!(DiagnosticPolicy::FailFast.name(), "fail-fast");
857        assert_eq!(DiagnosticPolicy::Permissive.name(), "permissive");
858    }
859    #[test]
860    fn test_printer_output_non_empty() {
861        let mut c = DiagnosticCollector::new();
862        c.add(Diagnostic::error("e".to_string(), dummy_span()));
863        let printer = DiagnosticPrinter::new(DiagnosticPolicy::CollectAll);
864        let out = printer.print(&c);
865        assert!(!out.is_empty());
866    }
867    #[test]
868    fn test_printer_should_fail() {
869        let mut c = DiagnosticCollector::new();
870        c.add(Diagnostic::error("e".to_string(), dummy_span()));
871        let printer = DiagnosticPrinter::new(DiagnosticPolicy::FailFast);
872        assert!(printer.should_fail(&c));
873    }
874    #[test]
875    fn test_sync_token_info_all_non_empty() {
876        assert!(!SyncTokenInfo::all().is_empty());
877    }
878    #[test]
879    fn test_sync_token_info_semicolon_is_statement_end() {
880        let all = SyncTokenInfo::all();
881        let semi = all
882            .iter()
883            .find(|i| i.kind == SyncToken::Semicolon)
884            .expect("lookup should succeed");
885        assert!(semi.is_statement_end);
886    }
887    #[test]
888    fn test_sync_token_info_right_paren_not_statement_end() {
889        let all = SyncTokenInfo::all();
890        let rp = all
891            .iter()
892            .find(|i| i.kind == SyncToken::RightParen)
893            .expect("test operation should succeed");
894        assert!(!rp.is_statement_end);
895    }
896}
897#[cfg(test)]
898mod diagnostic_filter_tests {
899    use super::*;
900    use crate::diagnostic::*;
901    use crate::tokens::Span;
902    #[test]
903    fn test_severity_filter() {
904        let f = SeverityFilter::all();
905        assert_eq!(f.min_severity, 0);
906        let e = SeverityFilter::errors_only();
907        assert_eq!(e.min_severity, 2);
908    }
909}
910/// Returns whether a diagnostic message matches a given pattern.
911#[allow(dead_code)]
912#[allow(missing_docs)]
913pub fn diagnostic_matches_pattern(message: &str, pattern: &str) -> bool {
914    message.contains(pattern)
915}
916#[cfg(test)]
917mod diagnostic_event_tests {
918    use super::*;
919    use crate::diagnostic::*;
920    #[test]
921    fn test_diagnostic_event() {
922        let e = DiagnosticEvent::new(1, "test message");
923        assert_eq!(e.id, 1);
924        assert_eq!(e.message, "test message");
925    }
926    #[test]
927    fn test_matches_pattern() {
928        assert!(diagnostic_matches_pattern(
929            "unexpected token 'foo'",
930            "token"
931        ));
932        assert!(!diagnostic_matches_pattern("unexpected token", "missing"));
933    }
934}