1use crate::tokens::{Token, TokenKind};
6
7use super::types::{DiagnosticCode, SyncToken};
8
9#[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#[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}
35pub(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}
54pub(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#[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#[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#[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#[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#[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}