1use std::ops::Range;
8
9mod catalog;
10mod render;
11
12pub use catalog::{error_catalog, explain};
13pub use render::{render_diagnostic, report_diagnostics_human};
14
15pub type Span = Range<usize>;
17
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
23#[serde(transparent)]
24pub struct ErrorCode(String);
25
26impl ErrorCode {
27 pub fn as_str(&self) -> &str {
29 &self.0
30 }
31}
32
33impl std::fmt::Display for ErrorCode {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.write_str(&self.0)
36 }
37}
38
39impl AsRef<str> for ErrorCode {
40 fn as_ref(&self) -> &str {
41 &self.0
42 }
43}
44
45impl From<&str> for ErrorCode {
46 fn from(s: &str) -> Self {
47 Self(s.to_owned())
48 }
49}
50
51impl From<String> for ErrorCode {
52 fn from(s: String) -> Self {
53 Self(s)
54 }
55}
56
57impl PartialEq<str> for ErrorCode {
58 fn eq(&self, other: &str) -> bool {
59 self.0 == other
60 }
61}
62
63impl PartialEq<&str> for ErrorCode {
64 fn eq(&self, other: &&str) -> bool {
65 self.0 == *other
66 }
67}
68
69impl PartialEq<String> for ErrorCode {
70 fn eq(&self, other: &String) -> bool {
71 self.0 == *other
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
77#[serde(rename_all = "lowercase")]
78pub enum Severity {
79 Info,
81 Warning,
83 Error,
85}
86
87#[derive(Debug, Clone, PartialEq, serde::Serialize)]
89pub struct SecondaryLabel {
90 pub span: Span,
92 pub message: String,
94}
95
96#[derive(Debug, Clone, PartialEq, serde::Serialize)]
98pub struct Suggestion {
99 pub message: String,
101 pub span: Span,
103 pub replacement: String,
105}
106
107#[derive(Debug, Clone, PartialEq, serde::Serialize)]
113pub struct Diagnostic {
114 pub code: ErrorCode,
116 pub severity: Severity,
118 pub message: String,
120 pub file: String,
122 pub primary: Span,
124 pub secondary: Vec<SecondaryLabel>,
126 pub suggestion: Option<Suggestion>,
128}
129
130impl Diagnostic {
131 pub fn error(code: impl Into<ErrorCode>, message: impl Into<String>, span: Span) -> Self {
133 Self {
134 code: code.into(),
135 severity: Severity::Error,
136 message: message.into(),
137 file: String::new(),
138 primary: span,
139 secondary: Vec::new(),
140 suggestion: None,
141 }
142 }
143
144 pub fn warning(code: impl Into<ErrorCode>, message: impl Into<String>, span: Span) -> Self {
146 Self {
147 code: code.into(),
148 severity: Severity::Warning,
149 message: message.into(),
150 file: String::new(),
151 primary: span,
152 secondary: Vec::new(),
153 suggestion: None,
154 }
155 }
156
157 pub fn with_file(mut self, file: impl Into<String>) -> Self {
159 self.file = file.into();
160 self
161 }
162
163 pub fn with_secondary(mut self, span: Span, label: impl Into<String>) -> Self {
165 self.secondary.push(SecondaryLabel {
166 span,
167 message: label.into(),
168 });
169 self
170 }
171
172 pub fn with_suggestion(
174 mut self,
175 message: impl Into<String>,
176 span: Span,
177 replacement: impl Into<String>,
178 ) -> Self {
179 self.suggestion = Some(Suggestion {
180 message: message.into(),
181 span,
182 replacement: replacement.into(),
183 });
184 self
185 }
186
187 pub fn is_error(&self) -> bool {
189 self.severity == Severity::Error
190 }
191}
192
193impl std::fmt::Display for Diagnostic {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 write!(f, "[{}] {}", self.code, self.message)
196 }
197}
198
199impl std::fmt::Display for Severity {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 match self {
202 Severity::Info => write!(f, "info"),
203 Severity::Warning => write!(f, "warning"),
204 Severity::Error => write!(f, "error"),
205 }
206 }
207}
208
209#[derive(Debug, Clone, PartialEq)]
211pub struct ErrorInfo {
212 pub code: &'static str,
214 pub name: &'static str,
216 pub description: &'static str,
218 pub example: &'static str,
220 pub fix: &'static str,
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn error_diagnostic_creation() {
230 let d = Diagnostic::error("A03001", "type mismatch", 10..20);
231 assert_eq!(d.code, "A03001");
232 assert_eq!(d.severity, Severity::Error);
233 assert_eq!(d.primary, 10..20);
234 assert!(d.is_error());
235 }
236
237 #[test]
238 fn warning_diagnostic_creation() {
239 let d = Diagnostic::warning("A05001", "unused variable", 5..10);
240 assert_eq!(d.severity, Severity::Warning);
241 assert!(!d.is_error());
242 }
243
244 #[test]
245 fn diagnostic_with_secondary() {
246 let d = Diagnostic::error("A03002", "expected Int", 10..20)
247 .with_secondary(30..40, "declared here");
248 assert_eq!(d.secondary.len(), 1);
249 assert_eq!(d.secondary[0].message, "declared here");
250 }
251
252 #[test]
253 fn diagnostic_with_suggestion() {
254 let d = Diagnostic::error("A01001", "unexpected token", 5..8).with_suggestion(
255 "try adding a semicolon",
256 7..8,
257 ";",
258 );
259 let s = d.suggestion.unwrap();
260 assert_eq!(s.replacement, ";");
261 }
262
263 #[test]
264 fn diagnostic_display() {
265 let d = Diagnostic::error("A03001", "type mismatch", 0..1);
266 assert_eq!(format!("{d}"), "[A03001] type mismatch");
267 }
268
269 #[test]
270 fn severity_ordering() {
271 assert!(Severity::Info < Severity::Warning);
272 assert!(Severity::Warning < Severity::Error);
273 }
274
275 #[test]
276 fn test_error_diagnostic_is_error() {
277 let d = Diagnostic::error("A01001", "syntax error", 0..5);
278 assert!(d.is_error());
279 assert_eq!(d.severity, Severity::Error);
280 }
281
282 #[test]
283 fn test_warning_diagnostic_is_not_error() {
284 let d = Diagnostic::warning("A02007", "unused import", 10..20);
285 assert!(!d.is_error());
286 assert_eq!(d.severity, Severity::Warning);
287 }
288
289 #[test]
290 fn test_severity_display() {
291 assert_eq!(format!("{}", Severity::Info), "info");
292 assert_eq!(format!("{}", Severity::Warning), "warning");
293 assert_eq!(format!("{}", Severity::Error), "error");
294 }
295
296 #[test]
297 fn test_diagnostic_with_file() {
298 let d = Diagnostic::error("A03001", "type mismatch", 0..10).with_file("test.assura");
299 assert_eq!(d.file, "test.assura");
300 }
301
302 #[test]
303 fn test_diagnostic_multiple_secondary_spans() {
304 let d = Diagnostic::error("A03001", "type mismatch", 10..20)
305 .with_secondary(30..40, "expected type here")
306 .with_secondary(50..60, "found type here");
307 assert_eq!(d.secondary.len(), 2);
308 assert_eq!(d.secondary[0].message, "expected type here");
309 assert_eq!(d.secondary[0].span, 30..40);
310 assert_eq!(d.secondary[1].message, "found type here");
311 assert_eq!(d.secondary[1].span, 50..60);
312 }
313
314 #[test]
315 fn test_diagnostic_suggestion_fields() {
316 let d = Diagnostic::error("A01002", "unexpected token", 5..8).with_suggestion(
317 "add a colon",
318 7..8,
319 ":",
320 );
321 let s = d.suggestion.as_ref().unwrap();
322 assert_eq!(s.message, "add a colon");
323 assert_eq!(s.span, 7..8);
324 assert_eq!(s.replacement, ":");
325 }
326
327 #[test]
328 fn test_diagnostic_json_serialization() {
329 let d = Diagnostic::error("A03001", "type mismatch", 10..20)
330 .with_file("main.assura")
331 .with_secondary(30..40, "declared here");
332 let json = serde_json::to_string(&d).unwrap();
333 assert!(json.contains("A03001"));
334 assert!(json.contains("type mismatch"));
335 assert!(json.contains("main.assura"));
336 assert!(json.contains("declared here"));
337 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
338 assert_eq!(val["code"], "A03001");
339 assert_eq!(val["severity"], "error");
340 assert_eq!(val["message"], "type mismatch");
341 }
342
343 #[test]
344 fn test_diagnostic_collection() {
345 let diags = vec![
346 Diagnostic::error("A01001", "unexpected char", 0..1),
347 Diagnostic::warning("A02007", "unused import", 10..20),
348 Diagnostic::error("A03001", "type mismatch", 30..40),
349 ];
350 assert_eq!(diags.len(), 3);
351 let errors: Vec<_> = diags.iter().filter(|d| d.is_error()).collect();
352 assert_eq!(errors.len(), 2);
353 let warnings: Vec<_> = diags
354 .iter()
355 .filter(|d| d.severity == Severity::Warning)
356 .collect();
357 assert_eq!(warnings.len(), 1);
358 }
359
360 #[test]
361 fn test_diagnostic_empty_secondary_spans() {
362 let d = Diagnostic::error("A03001", "error", 0..5);
363 assert!(d.secondary.is_empty());
364 assert!(d.suggestion.is_none());
365 }
366
367 #[test]
368 fn test_error_code_formatting_display() {
369 let d = Diagnostic::error("A05001", "linear variable used twice", 0..10);
370 let display = format!("{d}");
371 assert_eq!(display, "[A05001] linear variable used twice");
372 }
373
374 #[test]
375 fn test_error_catalog_not_empty() {
376 let catalog = error_catalog();
377 assert!(!catalog.is_empty());
378 for entry in &catalog {
379 assert!(!entry.code.is_empty());
380 assert!(!entry.name.is_empty());
381 assert!(!entry.description.is_empty());
382 assert!(!entry.example.is_empty());
383 assert!(!entry.fix.is_empty());
384 }
385 }
386
387 #[test]
388 fn test_explain_known_code() {
389 let info = explain("A01001");
390 let info = info.unwrap();
391 assert_eq!(info.code, "A01001");
392 assert_eq!(info.name, "Unexpected character");
393 }
394
395 #[test]
396 fn test_explain_unknown_code() {
397 let info = explain("A00000");
398 assert!(info.is_none());
399 }
400
401 #[test]
406 fn high_traffic_index_codes_are_in_catalog() {
407 const CODES: &[&str] = &[
408 "A01001", "A01002", "A02001", "A02003", "A02005", "A03001", "A03002", "A03005",
409 "A03006", "A05001", "A05002", "A05003", "A05004", "A06001", "A06002", "A06003",
410 "A06004", "A07001", "A07002", "A07003", "A08001", "A08002", "A08003", "A08004",
411 "A08005", "A09001", "A09002", "A09003", "A09004", "A11001", "A11002", "A11003",
412 "A11004", "A12001", "A12002", "A12003", "A13001", "A13002", "A13003", "A16001",
413 "A16002", "A16003", "A17001", "A17002", "A17003", "A21001", "A21002", "A21003",
414 "A22001", "A22002", "A22003", "A05100", "A05101", "A05102", "A05103", "A10002",
415 "A01000", "A02006", "A02007", "A02008", "A02010", "A03007", "A03010", "A08102",
416 "A10001", "A10101", "A11005", "A14001", "A14002", "A04008", "A05025", "A05026",
417 "A08101", "A09101", "A23003", "A26001", "A43005", "A17004", "A23016", "A24001",
418 "A27003", "A28001", "A33001", "A37003", "A38001", "A42003", "A43001", "A43002",
419 "A44001", "A45001", "A47001", "A48002", "A49001", "A49002", "A50001", "A52001",
420 "A54001", "A55001", "A64001", "A31006", "A31007", "A32002", "A36003", "A52002",
421 "A46002", "A29001", "A25003", "A09103", "A53006", "A49003", "A35003", "A34003",
422 "A30002", "A23001", "A10104", "A09102", "A08103", "A51003", "A46003", "A36001",
423 "A35001", "A10102", "A10103", "A42001", "A20001", "A20002", "A18001", "A18003",
424 "A24003", "A25001", "A22004", "A44003", "A46001", "A55003", "A32001", "A48001",
425 "A34001", "A37001", "A30003", "A15004", "A15001", "A18002", "A33003", "A03012",
426 "A23002", "A45003", "A42002", "A31001", "A31003", "A32003", "A51001", "A48003",
427 "A54003", "A30001", "A29003", "A28003", "A27001", "A26004", "A26003", "A15002",
428 "A15003", "A33002", "A03011", "A03008", "A25002", "A24002", "A23019", "A47002",
429 "A47003", "A45002", "A38002", "A44002", "A55002", "A54002", "A43003", "A43004",
430 "A31002", "A53003", "A53001", "A53002", "A52003", "A50002", "A50003", "A36002",
431 "A38003", "A35002", "A34002", "A29002", "A28002", "A27002", "A05200", "A51002",
432 "A37002", "A03009",
433 ];
434 for code in CODES {
435 let info = explain(code).unwrap_or_else(|| {
436 panic!(
437 "{code}: listed in docs/error-codes.md high-traffic table but missing from catalog"
438 )
439 });
440 assert_eq!(info.code, *code);
441 assert!(
442 !info.name.is_empty(),
443 "{code}: catalog entry must have a non-empty name"
444 );
445 }
446 }
447
448 #[test]
449 fn test_explain_all_catalog_codes() {
450 let catalog = error_catalog();
451 for entry in &catalog {
452 let found = explain(entry.code).unwrap_or_else(|| {
453 panic!("should find {}", entry.code);
454 });
455 assert_eq!(found.code, entry.code);
456 }
457 }
458
459 #[test]
460 fn test_warning_serialization() {
461 let d = Diagnostic::warning("A02007", "unused import", 5..15);
462 let json = serde_json::to_string(&d).unwrap();
463 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
464 assert_eq!(val["severity"], "warning");
465 }
466
467 #[test]
468 fn test_suggestion_serialization() {
469 let s = Suggestion {
470 message: "add semicolon".to_string(),
471 span: 10..11,
472 replacement: ";".to_string(),
473 };
474 let json = serde_json::to_string(&s).unwrap();
475 assert!(json.contains("add semicolon"));
476 }
477
478 #[test]
479 fn test_secondary_label_equality() {
480 let a = SecondaryLabel {
481 span: 0..5,
482 message: "here".to_string(),
483 };
484 let b = SecondaryLabel {
485 span: 0..5,
486 message: "here".to_string(),
487 };
488 assert_eq!(a, b);
489 }
490
491 #[test]
493 fn test_no_duplicate_error_codes() {
494 let catalog = error_catalog();
495 let mut seen = std::collections::HashSet::new();
496 for entry in &catalog {
497 assert!(
498 seen.insert(entry.code),
499 "duplicate error code in catalog: {}",
500 entry.code
501 );
502 }
503 }
504
505 #[test]
507 fn test_a03005_catalog_is_unknown_field() {
508 let info = explain("A03005").expect("A03005 should exist");
509 assert_eq!(info.name, "Unknown field");
510 assert!(
511 info.description.to_lowercase().contains("field"),
512 "A03005 description should mention fields, got: {}",
513 info.description
514 );
515 let fix_lower = info.fix.to_lowercase();
516 assert!(
517 !fix_lower.contains("calling a function"),
518 "A03005 fix/Help must not mention calling a function (that was the bug): {}",
519 info.fix
520 );
521 assert!(
522 fix_lower.contains("field") || fix_lower.contains("tuple"),
523 "A03005 fix should be field-oriented, got: {}",
524 info.fix
525 );
526 assert!(
528 info.example.contains(".z") || info.example.contains("t.2"),
529 "A03005 example should show unknown field or OOB tuple index"
530 );
531 assert!(
532 !info.example.contains("Foo(42)"),
533 "A03005 example must not be the old type-as-call snippet"
534 );
535 }
536
537 #[test]
538 fn test_render_diagnostic_does_not_panic() {
539 let d = Diagnostic::error("A01001", "unexpected char", 0..1);
541 render_diagnostic(&d, "test.assura", "x");
542
543 let d = Diagnostic::warning("A02007", "unused import", 0..5)
544 .with_secondary(6..10, "imported here");
545 render_diagnostic(&d, "test.assura", "import std.math;");
546 }
547
548 #[test]
549 fn test_report_diagnostics_human_multiple() {
550 let diags = vec![
551 Diagnostic::error("A01001", "bad char", 0..1),
552 Diagnostic::warning("A02007", "unused", 2..5),
553 ];
554 report_diagnostics_human(&diags, "multi.assura", "x = 42;");
556 }
557
558 #[test]
559 fn test_error_code_as_str() {
560 let code = ErrorCode::from("A03001");
561 assert_eq!(code.as_str(), "A03001");
562 }
563
564 #[test]
565 fn test_error_code_from_string() {
566 let code = ErrorCode::from(String::from("A05001"));
567 assert_eq!(code, "A05001");
568 }
569
570 #[test]
571 fn test_error_code_partial_eq_str() {
572 let code = ErrorCode::from("A07003");
573 assert!(code == "A07003");
574 assert!(code == *"A07003");
575 }
576
577 #[test]
578 fn test_error_code_as_ref() {
579 let code = ErrorCode::from("A01002");
580 let s: &str = code.as_ref();
581 assert_eq!(s, "A01002");
582 }
583
584 #[test]
585 fn test_error_code_display() {
586 let code = ErrorCode::from("A03005");
587 assert_eq!(format!("{code}"), "A03005");
588 }
589
590 #[test]
591 fn test_error_code_ordering() {
592 let a = ErrorCode::from("A01001");
593 let b = ErrorCode::from("A03001");
594 assert!(a < b);
595 }
596
597 #[test]
598 fn test_error_catalog_entries_have_fields() {
599 let catalog = error_catalog();
600 for entry in &catalog {
601 assert!(!entry.code.is_empty(), "code must not be empty");
602 assert!(
603 !entry.name.is_empty(),
604 "name must not be empty for {}",
605 entry.code
606 );
607 assert!(
608 !entry.description.is_empty(),
609 "description must not be empty for {}",
610 entry.code
611 );
612 assert!(
613 !entry.fix.is_empty(),
614 "fix must not be empty for {}",
615 entry.code
616 );
617 }
618 }
619
620 #[test]
621 fn test_diagnostic_chaining() {
622 let d = Diagnostic::error("A03001", "mismatch", 10..20)
623 .with_file("test.assura")
624 .with_secondary(30..40, "defined here")
625 .with_suggestion("use Int", 10..20, "Int");
626 assert_eq!(d.file, "test.assura");
627 assert_eq!(d.secondary.len(), 1);
628 d.suggestion.unwrap();
629 }
630
631 #[test]
632 fn test_severity_serde() {
633 let json = serde_json::to_string(&Severity::Error).unwrap();
634 assert_eq!(json, "\"error\"");
635 let json = serde_json::to_string(&Severity::Warning).unwrap();
636 assert_eq!(json, "\"warning\"");
637 let json = serde_json::to_string(&Severity::Info).unwrap();
638 assert_eq!(json, "\"info\"");
639 }
640
641 #[test]
644 fn test_error_code_eq_string_owned() {
645 let code = ErrorCode::from("A03001");
646 assert!(code == String::from("A03001"));
647 }
648
649 #[test]
650 fn test_error_code_ne() {
651 let a = ErrorCode::from("A01001");
652 let b = ErrorCode::from("A03001");
653 assert_ne!(a, b);
654 }
655
656 #[test]
657 fn test_error_code_clone_eq() {
658 let code = ErrorCode::from("A05001");
659 let cloned = code.clone();
660 assert_eq!(code, cloned);
661 }
662
663 #[test]
664 fn test_error_code_hash_consistent() {
665 use std::collections::HashSet;
666 let mut set = HashSet::new();
667 set.insert(ErrorCode::from("A01001"));
668 set.insert(ErrorCode::from("A01001")); set.insert(ErrorCode::from("A03001"));
670 assert_eq!(set.len(), 2);
671 }
672
673 #[test]
674 fn test_error_code_empty() {
675 let code = ErrorCode::from("");
676 assert_eq!(code.as_str(), "");
677 assert_eq!(format!("{code}"), "");
678 }
679
680 #[test]
683 fn test_error_catalog_all_codes_valid_format() {
684 let catalog = error_catalog();
685 for entry in &catalog {
686 assert_eq!(
687 entry.code.len(),
688 6,
689 "error code '{}' should be 6 chars (Axxxxx)",
690 entry.code
691 );
692 assert!(
693 entry.code.starts_with('A'),
694 "error code '{}' should start with 'A'",
695 entry.code
696 );
697 assert!(
698 entry.code[1..].chars().all(|c| c.is_ascii_digit()),
699 "error code '{}' should have 5 digits after 'A'",
700 entry.code
701 );
702 }
703 }
704
705 #[test]
706 fn test_error_catalog_has_major_categories() {
707 let catalog = error_catalog();
708 let codes: Vec<&str> = catalog.iter().map(|e| e.code).collect();
709 assert!(
711 codes.iter().any(|c| c.starts_with("A01")),
712 "missing A01xxx (syntax)"
713 );
714 assert!(
715 codes.iter().any(|c| c.starts_with("A02")),
716 "missing A02xxx (resolve)"
717 );
718 assert!(
719 codes.iter().any(|c| c.starts_with("A03")),
720 "missing A03xxx (type)"
721 );
722 assert!(
723 codes.iter().any(|c| c.starts_with("A05")),
724 "missing A05xxx (linear)"
725 );
726 assert!(
727 codes.iter().any(|c| c.starts_with("A07")),
728 "missing A07xxx (effect)"
729 );
730 }
731
732 #[test]
733 fn test_error_catalog_size_reasonable() {
734 let catalog = error_catalog();
735 assert!(
736 catalog.len() >= 150,
737 "catalog should have 150+ entries (emitted + wired codes), got {}",
738 catalog.len()
739 );
740 }
741
742 #[test]
743 fn test_explain_empty_string() {
744 assert!(explain("").is_none());
745 }
746
747 #[test]
748 fn test_explain_partial_code() {
749 assert!(explain("A01").is_none());
750 assert!(explain("A").is_none());
751 }
752
753 #[test]
754 fn test_explain_nonexistent_category() {
755 assert!(explain("A88888").is_none());
756 }
757
758 #[test]
761 fn test_diagnostic_zero_length_span() {
762 let d = Diagnostic::error("A01001", "at position", 5..5);
763 assert_eq!(d.primary, 5..5);
764 assert!(d.primary.is_empty());
765 }
766
767 #[test]
768 fn test_diagnostic_large_span() {
769 let d = Diagnostic::error("A01001", "whole file", 0..100_000);
770 assert_eq!(d.primary, 0..100_000);
771 }
772
773 #[test]
774 fn test_diagnostic_empty_message() {
775 let d = Diagnostic::error("A01001", "", 0..1);
776 assert_eq!(d.message, "");
777 assert_eq!(format!("{d}"), "[A01001] ");
778 }
779
780 #[test]
781 fn test_diagnostic_default_file_empty() {
782 let d = Diagnostic::error("A01001", "err", 0..1);
783 assert!(d.file.is_empty());
784 }
785
786 #[test]
787 fn test_diagnostic_with_file_overwrites() {
788 let d = Diagnostic::error("A01001", "err", 0..1)
789 .with_file("first.assura")
790 .with_file("second.assura");
791 assert_eq!(d.file, "second.assura");
792 }
793
794 #[test]
795 fn test_render_diagnostic_with_suggestion() {
796 let d = Diagnostic::error("A01002", "missing colon", 8..9).with_suggestion(
797 "add colon",
798 8..9,
799 ":",
800 );
801 render_diagnostic(&d, "test.assura", "requires x > 0");
803 }
804
805 #[test]
806 fn test_render_advice_only_suggestion_no_empty_backticks() {
807 let d = Diagnostic::error("A03006", "requires clause must be Bool", 0..1).with_suggestion(
810 "Ensure clauses are boolean expressions",
811 0..1,
812 "",
813 );
814 render_diagnostic(&d, "test.assura", "x");
815 assert_eq!(d.suggestion.as_ref().unwrap().replacement, "");
816 }
817
818 #[test]
819 fn test_report_diagnostics_human_empty() {
820 report_diagnostics_human(&[], "empty.assura", "");
822 }
823
824 #[test]
825 fn test_render_diagnostic_info_severity() {
826 let d = Diagnostic {
827 code: ErrorCode::from("A99999"),
828 severity: Severity::Info,
829 message: "informational".into(),
830 file: String::new(),
831 primary: 0..1,
832 secondary: Vec::new(),
833 suggestion: None,
834 };
835 render_diagnostic(&d, "test.assura", "x");
837 }
838
839 #[test]
842 fn test_severity_equality() {
843 assert_eq!(Severity::Error, Severity::Error);
844 assert_ne!(Severity::Error, Severity::Warning);
845 assert_ne!(Severity::Warning, Severity::Info);
846 }
847
848 #[test]
849 fn test_severity_copy() {
850 let s = Severity::Error;
851 let s2 = s; assert_eq!(s, s2);
853 }
854
855 #[test]
858 fn test_secondary_label_inequality() {
859 let a = SecondaryLabel {
860 span: 0..5,
861 message: "here".to_string(),
862 };
863 let b = SecondaryLabel {
864 span: 0..5,
865 message: "there".to_string(),
866 };
867 assert_ne!(a, b);
868 }
869
870 #[test]
871 fn test_secondary_label_serialization() {
872 let label = SecondaryLabel {
873 span: 10..20,
874 message: "declared here".to_string(),
875 };
876 let json = serde_json::to_string(&label).unwrap();
877 assert!(json.contains("declared here"));
878 assert!(json.contains("\"start\":10"), "span start: {json}");
879 assert!(json.contains("\"end\":20"), "span end: {json}");
880 }
881
882 #[test]
885 fn test_suggestion_equality() {
886 let a = Suggestion {
887 message: "fix".into(),
888 span: 0..1,
889 replacement: ";".into(),
890 };
891 let b = Suggestion {
892 message: "fix".into(),
893 span: 0..1,
894 replacement: ";".into(),
895 };
896 assert_eq!(a, b);
897 }
898
899 #[test]
900 fn test_suggestion_inequality() {
901 let a = Suggestion {
902 message: "fix".into(),
903 span: 0..1,
904 replacement: ";".into(),
905 };
906 let b = Suggestion {
907 message: "fix".into(),
908 span: 0..1,
909 replacement: ":".into(),
910 };
911 assert_ne!(a, b);
912 }
913
914 #[test]
917 fn test_diagnostic_full_json_structure() {
918 let d = Diagnostic::error("A03001", "type mismatch", 10..20)
919 .with_file("test.assura")
920 .with_secondary(30..40, "expected here")
921 .with_secondary(50..60, "found here")
922 .with_suggestion("change type", 10..20, "Int");
923 let json = serde_json::to_string_pretty(&d).unwrap();
924 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
925 assert_eq!(val["code"], "A03001");
927 assert_eq!(val["severity"], "error");
928 assert_eq!(val["file"], "test.assura");
929 assert!(val["secondary"].is_array());
931 assert_eq!(val["secondary"].as_array().unwrap().len(), 2);
932 assert!(val["suggestion"].is_object());
934 assert_eq!(val["suggestion"]["replacement"], "Int");
935 }
936
937 #[test]
938 fn test_diagnostic_json_no_suggestion() {
939 let d = Diagnostic::warning("A02007", "unused", 0..5);
940 let json = serde_json::to_string(&d).unwrap();
941 let val: serde_json::Value = serde_json::from_str(&json).unwrap();
942 assert!(val["suggestion"].is_null());
943 }
944
945 #[test]
948 fn test_error_info_equality() {
949 let a = ErrorInfo {
950 code: "A01001",
951 name: "Unexpected character",
952 description: "desc",
953 example: "ex",
954 fix: "fix",
955 };
956 let b = ErrorInfo {
957 code: "A01001",
958 name: "Unexpected character",
959 description: "desc",
960 example: "ex",
961 fix: "fix",
962 };
963 assert_eq!(a, b);
964 }
965
966 #[test]
967 fn test_error_info_clone() {
968 let a = ErrorInfo {
969 code: "A01001",
970 name: "test",
971 description: "desc",
972 example: "ex",
973 fix: "fix",
974 };
975 let b = a.clone();
976 assert_eq!(a, b);
977 }
978
979 #[test]
982 fn test_explain_returns_same_as_catalog_entry() {
983 let catalog = error_catalog();
984 for code in &["A01001", "A02001", "A03001", "A05001", "A07003", "A10001"] {
986 let from_explain = explain(code).expect(&format!("{code} should exist"));
987 let from_catalog = catalog
988 .iter()
989 .find(|e| e.code == *code)
990 .expect("in catalog");
991 assert_eq!(from_explain.name, from_catalog.name);
992 assert_eq!(from_explain.description, from_catalog.description);
993 }
994 }
995}