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