1use rustc_hash::FxHashMap as HashMap;
26use std::io::IsTerminal;
27
28use annotate_snippets::{Level, Renderer, Snippet};
29use gruel_util::span::byte_offset_to_line_col;
30use serde::Serialize;
31
32use crate::{CompileError, CompileErrors, CompileWarning, Diagnostic, ErrorCode, FileId, Span};
33
34type FileSpanMap<'a> = HashMap<FileId, Vec<(Span, Option<&'a str>, Level)>>;
36
37#[derive(Debug, Clone)]
42pub struct SourceInfo<'a> {
43 pub source: &'a str,
45 pub path: &'a str,
47}
48
49impl<'a> SourceInfo<'a> {
50 pub fn new(source: &'a str, path: &'a str) -> Self {
52 Self { source, path }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum ColorChoice {
59 #[default]
61 Auto,
62 Always,
64 Never,
66}
67
68struct SourceMap<'a> {
70 sources: HashMap<FileId, SourceInfo<'a>>,
71}
72
73impl<'a> SourceMap<'a> {
74 fn new(sources: impl IntoIterator<Item = (FileId, SourceInfo<'a>)>) -> Self {
75 Self {
76 sources: sources.into_iter().collect(),
77 }
78 }
79
80 fn get_or_fallback(&self, file_id: FileId) -> Option<&SourceInfo<'a>> {
82 self.sources
83 .get(&file_id)
84 .or_else(|| self.sources.get(&FileId::DEFAULT))
85 .or_else(|| self.sources.values().next())
86 }
87}
88
89pub struct MultiFileFormatter<'a> {
115 sources: SourceMap<'a>,
116 renderer: Renderer,
117}
118
119impl<'a> MultiFileFormatter<'a> {
120 pub fn new(sources: impl IntoIterator<Item = (FileId, SourceInfo<'a>)>) -> Self {
124 Self::with_color_choice(sources, ColorChoice::Auto)
125 }
126
127 pub fn with_color_choice(
129 sources: impl IntoIterator<Item = (FileId, SourceInfo<'a>)>,
130 color_choice: ColorChoice,
131 ) -> Self {
132 let use_color = match color_choice {
133 ColorChoice::Auto => std::io::stderr().is_terminal(),
134 ColorChoice::Always => true,
135 ColorChoice::Never => false,
136 };
137 let renderer = if use_color {
138 Renderer::styled()
139 } else {
140 Renderer::plain()
141 };
142 Self {
143 sources: SourceMap::new(sources),
144 renderer,
145 }
146 }
147
148 pub fn format_error(&self, error: &CompileError) -> String {
159 let message_with_code = format!("[{}]: {}", error.kind.code(), error.kind);
160
161 let is_ice = matches!(
163 error.kind.code(),
164 ErrorCode::INTERNAL_ERROR | ErrorCode::INTERNAL_CODEGEN_ERROR
165 );
166
167 let mut output = self.format_diagnostic_impl(
168 Level::Error,
169 &message_with_code,
170 error.span(),
171 error.diagnostic(),
172 );
173
174 if is_ice {
176 output.push_str("\nnote: this is a bug in the Gruel compiler\n");
177 output.push_str(
178 "help: please report this issue at https://github.com/ysimonson/gruel/issues\n",
179 );
180 }
181
182 output
183 }
184
185 pub fn format_errors(&self, errors: &CompileErrors) -> String {
190 if errors.is_empty() {
191 return String::new();
192 }
193
194 let mut output = String::new();
195 for error in errors.iter() {
196 if !output.is_empty() {
197 output.push('\n');
198 }
199 output.push_str(&self.format_error(error));
200 }
201
202 if errors.len() > 1 {
203 output.push_str(&format!(
204 "\nerror: aborting due to {} previous errors\n",
205 errors.len()
206 ));
207 }
208
209 output
210 }
211
212 pub fn format_warnings(&self, warnings: &[CompileWarning]) -> String {
214 if warnings.is_empty() {
215 return String::new();
216 }
217
218 let mut var_name_counts: HashMap<&str, usize> = HashMap::default();
220 for warning in warnings {
221 if let Some(name) = warning.kind.unused_variable_name() {
222 *var_name_counts.entry(name).or_insert(0) += 1;
223 }
224 }
225
226 let mut output = String::new();
228 for warning in warnings {
229 let needs_line_number = warning
230 .kind
231 .unused_variable_name()
232 .is_some_and(|name| var_name_counts.get(name).copied().unwrap_or(0) > 1);
233
234 if !output.is_empty() {
235 output.push('\n');
236 }
237 output.push_str(&self.format_warning_internal(warning, needs_line_number));
238 }
239 output
240 }
241
242 pub fn format_warning(&self, warning: &CompileWarning) -> String {
244 self.format_warning_internal(warning, false)
245 }
246
247 fn format_warning_internal(
248 &self,
249 warning: &CompileWarning,
250 include_line_number: bool,
251 ) -> String {
252 let message = if include_line_number {
254 if let Some(span) = warning.span() {
255 if let Some(source_info) = self.sources.get_or_fallback(span.file_id) {
256 let line = span.line_number(source_info.source);
257 warning.kind.format_with_line(Some(line))
258 } else {
259 warning.to_string()
260 }
261 } else {
262 warning.to_string()
263 }
264 } else {
265 warning.to_string()
266 };
267
268 self.format_diagnostic_impl(
269 Level::Warning,
270 &message,
271 warning.span(),
272 warning.diagnostic(),
273 )
274 }
275
276 fn format_diagnostic_impl(
278 &self,
279 level: Level,
280 message: &str,
281 span: Option<Span>,
282 diagnostic: &Diagnostic,
283 ) -> String {
284 let Some(primary_span) = span else {
286 let mut report = level.title(message);
287 for note in &diagnostic.notes {
288 report = report.footer(Level::Note.title(note.0.as_str()));
289 }
290 for help in &diagnostic.helps {
291 report = report.footer(Level::Help.title(help.0.as_str()));
292 }
293 return format!("{}", self.renderer.render(report));
294 };
295
296 let mut file_spans = FileSpanMap::default();
298
299 file_spans
301 .entry(primary_span.file_id)
302 .or_default()
303 .push((primary_span, None, level));
304
305 for label in &diagnostic.labels {
307 file_spans.entry(label.span.file_id).or_default().push((
308 label.span,
309 Some(label.message.as_str()),
310 Level::Info,
311 ));
312 }
313
314 let mut report = level.title(message);
316
317 let mut file_ids: Vec<_> = file_spans.keys().copied().collect();
319 file_ids.sort_by_key(|id| id.0);
320
321 for file_id in file_ids {
322 let spans = &file_spans[&file_id];
323
324 let Some(source_info) = self.sources.get_or_fallback(file_id) else {
326 continue;
328 };
329
330 let mut snippet = Snippet::source(source_info.source)
332 .origin(source_info.path)
333 .fold(true);
334
335 let source_len = source_info.source.len();
336 for (span, label, span_level) in spans {
337 let start = (span.start as usize).min(source_len);
339 let end = (span.end as usize).min(source_len).max(start);
340
341 let annotation = span_level.span(start..end);
342 let annotation = if let Some(label_text) = label {
343 annotation.label(label_text)
344 } else {
345 annotation
346 };
347 snippet = snippet.annotation(annotation);
348 }
349
350 report = report.snippet(snippet);
351 }
352
353 for note in &diagnostic.notes {
355 report = report.footer(Level::Note.title(note.0.as_str()));
356 }
357 for help in &diagnostic.helps {
358 report = report.footer(Level::Help.title(help.0.as_str()));
359 }
360
361 format!("{}", self.renderer.render(report))
362 }
363}
364
365#[derive(Debug, Serialize)]
375pub struct JsonDiagnostic {
376 pub code: String,
378 pub message: String,
380 pub severity: &'static str,
382 pub spans: Vec<JsonSpan>,
384 pub suggestions: Vec<JsonSuggestion>,
386 pub notes: Vec<String>,
388 pub helps: Vec<String>,
390}
391
392#[derive(Debug, Serialize)]
394pub struct JsonSpan {
395 pub file: String,
397 pub start: u32,
399 pub end: u32,
401 pub line: u32,
403 pub column: u32,
405 pub label: Option<String>,
407 pub primary: bool,
409}
410
411#[derive(Debug, Clone, Serialize, serde::Deserialize)]
413pub struct JsonSuggestion {
414 pub message: String,
416 pub file: String,
418 pub start: u32,
420 pub end: u32,
422 pub replacement: String,
424 pub applicability: String,
426}
427
428impl JsonDiagnostic {
429 pub fn to_json(&self) -> String {
431 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
432 }
433}
434
435pub struct MultiFileJsonFormatter<'a> {
458 sources: SourceMap<'a>,
459}
460
461impl<'a> MultiFileJsonFormatter<'a> {
462 pub fn new(sources: impl IntoIterator<Item = (FileId, SourceInfo<'a>)>) -> Self {
464 Self {
465 sources: SourceMap::new(sources),
466 }
467 }
468
469 fn source_for_span(&self, span: Span) -> Option<(&str, &str)> {
471 self.sources
472 .get_or_fallback(span.file_id)
473 .map(|info| (info.path, info.source))
474 }
475
476 fn make_span(&self, span: Span, label: Option<String>, primary: bool) -> Option<JsonSpan> {
477 self.source_for_span(span).map(|(path, source)| {
478 let (line, col) = byte_offset_to_line_col(source, span.start as usize);
479 JsonSpan {
480 file: path.to_string(),
481 start: span.start,
482 end: span.end,
483 line: line as u32,
484 column: col as u32,
485 label,
486 primary,
487 }
488 })
489 }
490
491 fn build_diagnostic(
492 &self,
493 code: String,
494 message: String,
495 severity: &'static str,
496 primary: Option<Span>,
497 diag: &Diagnostic,
498 ) -> JsonDiagnostic {
499 let mut spans: Vec<JsonSpan> = primary
500 .and_then(|span| self.make_span(span, None, true))
501 .into_iter()
502 .collect();
503 spans.extend(
504 diag.labels
505 .iter()
506 .filter_map(|l| self.make_span(l.span, Some(l.message.clone()), false)),
507 );
508
509 let suggestions = diag
510 .suggestions
511 .iter()
512 .filter_map(|s| {
513 self.source_for_span(s.span)
514 .map(|(path, _)| JsonSuggestion {
515 message: s.message.clone(),
516 file: path.to_string(),
517 start: s.span.start,
518 end: s.span.end,
519 replacement: s.replacement.clone(),
520 applicability: s.applicability.to_string(),
521 })
522 })
523 .collect();
524
525 JsonDiagnostic {
526 code,
527 message,
528 severity,
529 spans,
530 suggestions,
531 notes: diag.notes.iter().map(|n| n.0.clone()).collect(),
532 helps: diag.helps.iter().map(|h| h.0.clone()).collect(),
533 }
534 }
535
536 pub fn format_error(&self, error: &CompileError) -> JsonDiagnostic {
538 self.build_diagnostic(
539 format!("{}", error.kind.code()),
540 format!("{}", error.kind),
541 "error",
542 error.span(),
543 error.diagnostic(),
544 )
545 }
546
547 pub fn format_warning(&self, warning: &CompileWarning) -> JsonDiagnostic {
549 self.build_diagnostic(
550 String::new(), format!("{}", warning.kind),
552 "warning",
553 warning.span(),
554 warning.diagnostic(),
555 )
556 }
557
558 pub fn format_errors(&self, errors: &CompileErrors) -> String {
560 let diagnostics: Vec<String> = errors
561 .iter()
562 .map(|e| self.format_error(e).to_json())
563 .collect();
564 format!("[{}]", diagnostics.join(","))
565 }
566
567 pub fn format_warnings(&self, warnings: &[CompileWarning]) -> String {
569 let diagnostics: Vec<String> = warnings
570 .iter()
571 .map(|w| self.format_warning(w).to_json())
572 .collect();
573 format!("[{}]", diagnostics.join(","))
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use crate::{ErrorKind, Suggestion, WarningKind};
581
582 #[test]
583 fn test_format_error_with_span() {
584 let source = "fn main() -> i32 { 1 + true }";
585 let source_info = SourceInfo::new(source, "test.gruel");
586 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
587
588 let error = CompileError::new(
589 ErrorKind::TypeMismatch {
590 expected: "i32".to_string(),
591 found: "bool".to_string(),
592 },
593 Span::new(23, 27),
594 );
595
596 let output = formatter.format_error(&error);
597 assert!(output.contains("[E0206]"));
599 assert!(output.contains("type mismatch"));
600 assert!(output.contains("expected i32"));
601 assert!(output.contains("found bool"));
602 assert!(output.contains("test.gruel"));
603 }
604
605 #[test]
606 fn test_format_error_without_span() {
607 let source = "fn foo() -> i32 { 42 }";
608 let source_info = SourceInfo::new(source, "test.gruel");
609 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
610
611 let error = CompileError::without_span(ErrorKind::NoMainFunction);
612
613 let output = formatter.format_error(&error);
614 assert!(output.contains("[E0200]"));
616 assert!(output.contains("no main function"));
617 }
618
619 #[test]
620 fn test_format_warning() {
621 let source = "fn main() -> i32 { let x = 42; 0 }";
622 let source_info = SourceInfo::new(source, "test.gruel");
623 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
624
625 let warning = CompileWarning::new(
626 WarningKind::UnusedVariable("x".to_string()),
627 Span::new(23, 24),
628 );
629
630 let output = formatter.format_warning(&warning);
631 assert!(output.contains("unused variable"));
632 assert!(output.contains("'x'"));
633 }
634
635 #[test]
636 fn test_format_warnings_with_duplicates() {
637 let source = "fn main() -> i32 {\n let x = 1;\n let x = 2;\n 0\n}";
638 let source_info = SourceInfo::new(source, "test.gruel");
639 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
640
641 let warnings = vec![
642 CompileWarning::new(
643 WarningKind::UnusedVariable("x".to_string()),
644 Span::new(23, 24),
645 ),
646 CompileWarning::new(
647 WarningKind::UnusedVariable("x".to_string()),
648 Span::new(36, 37),
649 ),
650 ];
651
652 let output = formatter.format_warnings(&warnings);
653 assert!(output.contains("line"));
655 }
656
657 #[test]
658 fn test_format_warnings_empty() {
659 let source = "fn main() -> i32 { 42 }";
660 let source_info = SourceInfo::new(source, "test.gruel");
661 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
662
663 let output = formatter.format_warnings(&[]);
664 assert!(output.is_empty());
665 }
666
667 #[test]
668 fn test_format_error_with_help() {
669 let source = "fn main() -> i32 { x = 1; 0 }";
670 let source_info = SourceInfo::new(source, "test.gruel");
671 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
672
673 let error = CompileError::new(
674 ErrorKind::AssignToImmutable("x".to_string()),
675 Span::new(19, 20),
676 )
677 .with_help("consider making `x` mutable: `let mut x`");
678
679 let output = formatter.format_error(&error);
680 assert!(output.contains("help"));
681 assert!(output.contains("mutable"));
682 }
683
684 #[test]
685 fn test_format_error_with_label() {
686 let source = "fn main() -> i32 { if true { 1 } else { false } }";
687 let source_info = SourceInfo::new(source, "test.gruel");
688 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
689
690 let error = CompileError::new(
691 ErrorKind::TypeMismatch {
692 expected: "i32".to_string(),
693 found: "bool".to_string(),
694 },
695 Span::new(40, 45),
696 )
697 .with_label("then branch is here", Span::new(29, 30));
698
699 let output = formatter.format_error(&error);
700 assert!(output.contains("then branch"));
701 }
702
703 #[test]
704 fn test_format_errors_empty() {
705 let source = "fn main() -> i32 { 42 }";
706 let source_info = SourceInfo::new(source, "test.gruel");
707 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
708
709 let errors = CompileErrors::new();
710 let output = formatter.format_errors(&errors);
711 assert!(output.is_empty());
712 }
713
714 #[test]
715 fn test_format_errors_single() {
716 let source = "fn main() -> i32 { 1 + true }";
717 let source_info = SourceInfo::new(source, "test.gruel");
718 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
719
720 let mut errors = CompileErrors::new();
721 errors.push(CompileError::new(
722 ErrorKind::TypeMismatch {
723 expected: "i32".to_string(),
724 found: "bool".to_string(),
725 },
726 Span::new(23, 27),
727 ));
728
729 let output = formatter.format_errors(&errors);
730 assert!(output.contains("type mismatch"));
731 assert!(!output.contains("aborting"));
733 }
734
735 #[test]
736 fn test_format_errors_multiple() {
737 let source = "fn main() -> i32 {\n let x = 1 + true;\n let y = false - 1;\n 0\n}";
738 let source_info = SourceInfo::new(source, "test.gruel");
739 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
740
741 let mut errors = CompileErrors::new();
742 errors.push(CompileError::new(
743 ErrorKind::TypeMismatch {
744 expected: "i32".to_string(),
745 found: "bool".to_string(),
746 },
747 Span::new(32, 36),
748 ));
749 errors.push(CompileError::new(
750 ErrorKind::TypeMismatch {
751 expected: "bool".to_string(),
752 found: "i32".to_string(),
753 },
754 Span::new(58, 59),
755 ));
756
757 let output = formatter.format_errors(&errors);
758 assert!(output.contains("expected i32, found bool"));
760 assert!(output.contains("expected bool, found i32"));
761 assert!(output.contains("aborting due to 2 previous errors"));
763 }
764
765 #[test]
766 fn test_color_choice_never() {
767 let source = "fn main() -> i32 { 1 + true }";
768 let source_info = SourceInfo::new(source, "test.gruel");
769 let formatter = MultiFileFormatter::with_color_choice(
770 [(FileId::DEFAULT, source_info)],
771 ColorChoice::Never,
772 );
773
774 let error = CompileError::new(
775 ErrorKind::TypeMismatch {
776 expected: "i32".to_string(),
777 found: "bool".to_string(),
778 },
779 Span::new(23, 27),
780 );
781
782 let output = formatter.format_error(&error);
783 assert!(!output.contains("\x1b["));
785 assert!(output.contains("type mismatch"));
786 }
787
788 #[test]
789 fn test_format_error_with_invalid_span() {
790 let source = "fn main() -> i32 { 42 }";
791 let source_info = SourceInfo::new(source, "test.gruel");
792 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
793
794 let error = CompileError::new(
796 ErrorKind::TypeMismatch {
797 expected: "i32".to_string(),
798 found: "bool".to_string(),
799 },
800 Span::new(20, 1000), );
802
803 let output = formatter.format_error(&error);
805 assert!(output.contains("[E0206]"));
806 assert!(output.contains("type mismatch"));
807 }
808
809 #[test]
810 fn test_format_error_with_reversed_span() {
811 let source = "fn main() -> i32 { 42 }";
812 let source_info = SourceInfo::new(source, "test.gruel");
813 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
814
815 let error = CompileError::new(
817 ErrorKind::TypeMismatch {
818 expected: "i32".to_string(),
819 found: "bool".to_string(),
820 },
821 Span::new(20, 10), );
823
824 let output = formatter.format_error(&error);
826 assert!(output.contains("[E0206]"));
827 assert!(output.contains("type mismatch"));
828 }
829
830 #[test]
831 fn test_color_choice_always() {
832 let source = "fn main() -> i32 { 1 + true }";
833 let source_info = SourceInfo::new(source, "test.gruel");
834 let formatter = MultiFileFormatter::with_color_choice(
835 [(FileId::DEFAULT, source_info)],
836 ColorChoice::Always,
837 );
838
839 let error = CompileError::new(
840 ErrorKind::TypeMismatch {
841 expected: "i32".to_string(),
842 found: "bool".to_string(),
843 },
844 Span::new(23, 27),
845 );
846
847 let output = formatter.format_error(&error);
848 assert!(output.contains("\x1b["));
850 assert!(output.contains("type mismatch"));
851 }
852
853 #[test]
854 fn test_ice_formatting() {
855 let source = "fn main() -> i32 { 42 }";
856 let source_info = SourceInfo::new(source, "test.gruel");
857 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
858
859 let error = CompileError::without_span(ErrorKind::InternalError(
860 "unexpected type in codegen".to_string(),
861 ));
862
863 let output = formatter.format_error(&error);
864 assert!(output.contains("[E9000]"));
866 assert!(output.contains("internal compiler error"));
867 assert!(output.contains("note: this is a bug in the Gruel compiler"));
869 assert!(output.contains("help: please report this issue"));
870 assert!(output.contains("github.com/ysimonson/gruel/issues"));
871 }
872
873 #[test]
874 fn test_ice_codegen_formatting() {
875 let source = "fn main() -> i32 { 42 }";
876 let source_info = SourceInfo::new(source, "test.gruel");
877 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
878
879 let error = CompileError::without_span(ErrorKind::InternalCodegenError(
880 "failed to emit instruction".to_string(),
881 ));
882
883 let output = formatter.format_error(&error);
884 assert!(output.contains("[E9001]"));
886 assert!(output.contains("internal codegen error"));
887 assert!(output.contains("note: this is a bug in the Gruel compiler"));
889 assert!(output.contains("help: please report this issue"));
890 }
891
892 #[test]
893 fn test_non_ice_no_extra_formatting() {
894 let source = "fn main() -> i32 { 1 + true }";
895 let source_info = SourceInfo::new(source, "test.gruel");
896 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
897
898 let error = CompileError::new(
899 ErrorKind::TypeMismatch {
900 expected: "i32".to_string(),
901 found: "bool".to_string(),
902 },
903 Span::new(23, 27),
904 );
905
906 let output = formatter.format_error(&error);
907 assert!(!output.contains("note: this is a bug in the Gruel compiler"));
909 assert!(!output.contains("please report this issue"));
910 }
911
912 #[test]
917 fn test_json_format_error() {
918 let source = "fn main() -> i32 { 1 + true }";
919 let source_info = SourceInfo::new(source, "test.gruel");
920 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
921
922 let error = CompileError::new(
923 ErrorKind::TypeMismatch {
924 expected: "i32".to_string(),
925 found: "bool".to_string(),
926 },
927 Span::new(23, 27),
928 );
929
930 let json_diag = formatter.format_error(&error);
931 assert_eq!(json_diag.severity, "error");
932 assert_eq!(json_diag.code, "E0206");
933 assert!(json_diag.message.contains("type mismatch"));
934 assert_eq!(json_diag.spans.len(), 1);
935 assert_eq!(json_diag.spans[0].file, "test.gruel");
936 assert_eq!(json_diag.spans[0].start, 23);
937 assert_eq!(json_diag.spans[0].end, 27);
938 assert!(json_diag.spans[0].primary);
939 }
940
941 #[test]
942 fn test_json_format_error_line_col() {
943 let source = "fn main() -> i32 {\n 1 + true\n}";
944 let source_info = SourceInfo::new(source, "test.gruel");
946 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
947
948 let error = CompileError::new(
949 ErrorKind::TypeMismatch {
950 expected: "i32".to_string(),
951 found: "bool".to_string(),
952 },
953 Span::new(27, 31), );
955
956 let json_diag = formatter.format_error(&error);
957 assert_eq!(json_diag.spans[0].line, 2);
958 assert!(json_diag.spans[0].column > 1); }
960
961 #[test]
962 fn test_json_format_error_with_suggestion() {
963 let source = "fn main() -> i32 { x = 1; 0 }";
964 let source_info = SourceInfo::new(source, "test.gruel");
965 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
966
967 let error = CompileError::new(
968 ErrorKind::AssignToImmutable("x".to_string()),
969 Span::new(19, 20),
970 )
971 .with_suggestion(Suggestion::machine_applicable(
972 "add mut",
973 Span::new(4, 5),
974 "mut x",
975 ));
976
977 let json_diag = formatter.format_error(&error);
978 assert_eq!(json_diag.suggestions.len(), 1);
979 assert_eq!(json_diag.suggestions[0].message, "add mut");
980 assert_eq!(json_diag.suggestions[0].replacement, "mut x");
981 assert_eq!(json_diag.suggestions[0].applicability, "MachineApplicable");
982 }
983
984 #[test]
985 fn test_json_format_warning() {
986 let source = "fn main() -> i32 { let x = 42; 0 }";
987 let source_info = SourceInfo::new(source, "test.gruel");
988 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
989
990 let warning = CompileWarning::new(
991 WarningKind::UnusedVariable("x".to_string()),
992 Span::new(23, 24),
993 );
994
995 let json_diag = formatter.format_warning(&warning);
996 assert_eq!(json_diag.severity, "warning");
997 assert!(json_diag.message.contains("unused variable"));
998 }
999
1000 #[test]
1001 fn test_json_to_string() {
1002 let source = "fn main() -> i32 { 1 + true }";
1003 let source_info = SourceInfo::new(source, "test.gruel");
1004 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
1005
1006 let error = CompileError::new(
1007 ErrorKind::TypeMismatch {
1008 expected: "i32".to_string(),
1009 found: "bool".to_string(),
1010 },
1011 Span::new(23, 27),
1012 );
1013
1014 let json_diag = formatter.format_error(&error);
1015 let json_str = json_diag.to_json();
1016
1017 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1019 assert_eq!(parsed["severity"], "error");
1020 assert_eq!(parsed["code"], "E0206");
1021 assert!(parsed["spans"].is_array());
1022 assert_eq!(parsed["spans"][0]["primary"], true);
1023 }
1024
1025 #[test]
1026 fn test_json_format_errors_array() {
1027 let source = "fn main() -> i32 {\n 1 + true\n}";
1028 let source_info = SourceInfo::new(source, "test.gruel");
1029 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
1030
1031 let mut errors = CompileErrors::new();
1032 errors.push(CompileError::new(
1033 ErrorKind::TypeMismatch {
1034 expected: "i32".to_string(),
1035 found: "bool".to_string(),
1036 },
1037 Span::new(27, 31),
1038 ));
1039
1040 let json_str = formatter.format_errors(&errors);
1041 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1042 assert!(parsed.is_array());
1043 assert_eq!(parsed.as_array().unwrap().len(), 1);
1044 }
1045
1046 #[test]
1051 fn test_multi_file_formatter_single_file() {
1052 let source = "fn main() -> i32 { 1 + true }";
1053 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1054 let formatter = MultiFileFormatter::new(sources);
1055
1056 let error = CompileError::new(
1057 ErrorKind::TypeMismatch {
1058 expected: "i32".to_string(),
1059 found: "bool".to_string(),
1060 },
1061 Span::with_file(FileId::new(1), 23, 27),
1062 );
1063
1064 let output = formatter.format_error(&error);
1065 assert!(output.contains("[E0206]"));
1066 assert!(output.contains("type mismatch"));
1067 assert!(output.contains("test.gruel"));
1068 }
1069
1070 #[test]
1071 fn test_multi_file_formatter_multiple_files() {
1072 let source1 = "fn main() -> i32 { helper() }";
1073 let source2 = "fn helper() -> bool { true }";
1074 let sources = vec![
1075 (FileId::new(1), SourceInfo::new(source1, "main.gruel")),
1076 (FileId::new(2), SourceInfo::new(source2, "helper.gruel")),
1077 ];
1078 let formatter = MultiFileFormatter::new(sources);
1079
1080 let error = CompileError::new(
1082 ErrorKind::TypeMismatch {
1083 expected: "i32".to_string(),
1084 found: "bool".to_string(),
1085 },
1086 Span::with_file(FileId::new(1), 19, 27),
1087 )
1088 .with_label(
1089 "function returns bool here",
1090 Span::with_file(FileId::new(2), 15, 19),
1091 );
1092
1093 let output = formatter.format_error(&error);
1094 assert!(output.contains("main.gruel"));
1096 assert!(output.contains("helper.gruel"));
1097 assert!(output.contains("function returns bool here"));
1098 }
1099
1100 #[test]
1101 fn test_multi_file_formatter_without_span() {
1102 let source = "fn foo() -> i32 { 42 }";
1103 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1104 let formatter = MultiFileFormatter::new(sources);
1105
1106 let error = CompileError::without_span(ErrorKind::NoMainFunction);
1107
1108 let output = formatter.format_error(&error);
1109 assert!(output.contains("[E0200]"));
1110 assert!(output.contains("no main function"));
1111 }
1112
1113 #[test]
1114 fn test_multi_file_formatter_format_errors() {
1115 let source = "fn main() -> i32 { 1 + true }";
1116 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1117 let formatter = MultiFileFormatter::new(sources);
1118
1119 let mut errors = CompileErrors::new();
1120 errors.push(CompileError::new(
1121 ErrorKind::TypeMismatch {
1122 expected: "i32".to_string(),
1123 found: "bool".to_string(),
1124 },
1125 Span::with_file(FileId::new(1), 23, 27),
1126 ));
1127 errors.push(CompileError::without_span(ErrorKind::NoMainFunction));
1128
1129 let output = formatter.format_errors(&errors);
1130 assert!(output.contains("type mismatch"));
1131 assert!(output.contains("no main function"));
1132 assert!(output.contains("aborting due to 2 previous errors"));
1133 }
1134
1135 #[test]
1136 fn test_multi_file_formatter_format_warnings() {
1137 let source = "fn main() -> i32 { let x = 42; 0 }";
1138 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1139 let formatter = MultiFileFormatter::new(sources);
1140
1141 let warnings = vec![CompileWarning::new(
1142 WarningKind::UnusedVariable("x".to_string()),
1143 Span::with_file(FileId::new(1), 23, 24),
1144 )];
1145
1146 let output = formatter.format_warnings(&warnings);
1147 assert!(output.contains("unused variable"));
1148 assert!(output.contains("'x'"));
1149 }
1150
1151 #[test]
1152 fn test_multi_file_formatter_color_choice() {
1153 let source = "fn main() -> i32 { 1 + true }";
1154 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1155 let formatter = MultiFileFormatter::with_color_choice(sources, ColorChoice::Never);
1156
1157 let error = CompileError::new(
1158 ErrorKind::TypeMismatch {
1159 expected: "i32".to_string(),
1160 found: "bool".to_string(),
1161 },
1162 Span::with_file(FileId::new(1), 23, 27),
1163 );
1164
1165 let output = formatter.format_error(&error);
1166 assert!(!output.contains("\x1b["));
1168 }
1169
1170 #[test]
1171 fn test_multi_file_formatter_invalid_span() {
1172 let source = "fn main() -> i32 { 42 }";
1173 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1174 let formatter = MultiFileFormatter::new(sources);
1175
1176 let error = CompileError::new(
1178 ErrorKind::TypeMismatch {
1179 expected: "i32".to_string(),
1180 found: "bool".to_string(),
1181 },
1182 Span::with_file(FileId::new(1), 20, 1000),
1183 );
1184
1185 let output = formatter.format_error(&error);
1187 assert!(output.contains("[E0206]"));
1188 assert!(output.contains("type mismatch"));
1189 }
1190
1191 #[test]
1192 fn test_multi_file_formatter_reversed_span() {
1193 let source = "fn main() -> i32 { 42 }";
1194 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1195 let formatter = MultiFileFormatter::new(sources);
1196
1197 let error = CompileError::new(
1199 ErrorKind::TypeMismatch {
1200 expected: "i32".to_string(),
1201 found: "bool".to_string(),
1202 },
1203 Span::with_file(FileId::new(1), 20, 10),
1204 );
1205
1206 let output = formatter.format_error(&error);
1208 assert!(output.contains("[E0206]"));
1209 assert!(output.contains("type mismatch"));
1210 }
1211
1212 #[test]
1217 fn test_multi_file_json_formatter_single_file() {
1218 let source = "fn main() -> i32 { 1 + true }";
1219 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1220 let formatter = MultiFileJsonFormatter::new(sources);
1221
1222 let error = CompileError::new(
1223 ErrorKind::TypeMismatch {
1224 expected: "i32".to_string(),
1225 found: "bool".to_string(),
1226 },
1227 Span::with_file(FileId::new(1), 23, 27),
1228 );
1229
1230 let json_diag = formatter.format_error(&error);
1231 assert_eq!(json_diag.severity, "error");
1232 assert_eq!(json_diag.code, "E0206");
1233 assert_eq!(json_diag.spans.len(), 1);
1234 assert_eq!(json_diag.spans[0].file, "test.gruel");
1235 assert!(json_diag.spans[0].primary);
1236 }
1237
1238 #[test]
1239 fn test_multi_file_json_formatter_multiple_files() {
1240 let source1 = "fn main() -> i32 { helper() }";
1241 let source2 = "fn helper() -> bool { true }";
1242 let sources = vec![
1243 (FileId::new(1), SourceInfo::new(source1, "main.gruel")),
1244 (FileId::new(2), SourceInfo::new(source2, "helper.gruel")),
1245 ];
1246 let formatter = MultiFileJsonFormatter::new(sources);
1247
1248 let error = CompileError::new(
1250 ErrorKind::TypeMismatch {
1251 expected: "i32".to_string(),
1252 found: "bool".to_string(),
1253 },
1254 Span::with_file(FileId::new(1), 19, 27),
1255 )
1256 .with_label(
1257 "function returns bool here",
1258 Span::with_file(FileId::new(2), 15, 19),
1259 );
1260
1261 let json_diag = formatter.format_error(&error);
1262 assert_eq!(json_diag.spans.len(), 2);
1263
1264 let primary = json_diag.spans.iter().find(|s| s.primary).unwrap();
1266 assert_eq!(primary.file, "main.gruel");
1267
1268 let secondary = json_diag.spans.iter().find(|s| !s.primary).unwrap();
1270 assert_eq!(secondary.file, "helper.gruel");
1271 assert_eq!(
1272 secondary.label,
1273 Some("function returns bool here".to_string())
1274 );
1275 }
1276
1277 #[test]
1278 fn test_multi_file_json_formatter_format_errors() {
1279 let source = "fn main() -> i32 { 1 + true }";
1280 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1281 let formatter = MultiFileJsonFormatter::new(sources);
1282
1283 let mut errors = CompileErrors::new();
1284 errors.push(CompileError::new(
1285 ErrorKind::TypeMismatch {
1286 expected: "i32".to_string(),
1287 found: "bool".to_string(),
1288 },
1289 Span::with_file(FileId::new(1), 23, 27),
1290 ));
1291
1292 let json_str = formatter.format_errors(&errors);
1293 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1294 assert!(parsed.is_array());
1295 assert_eq!(parsed.as_array().unwrap().len(), 1);
1296 }
1297
1298 #[test]
1299 fn test_multi_file_json_formatter_format_warning() {
1300 let source = "fn main() -> i32 { let x = 42; 0 }";
1301 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1302 let formatter = MultiFileJsonFormatter::new(sources);
1303
1304 let warning = CompileWarning::new(
1305 WarningKind::UnusedVariable("x".to_string()),
1306 Span::with_file(FileId::new(1), 23, 24),
1307 );
1308
1309 let json_diag = formatter.format_warning(&warning);
1310 assert_eq!(json_diag.severity, "warning");
1311 assert!(json_diag.message.contains("unused variable"));
1312 assert_eq!(json_diag.spans.len(), 1);
1313 assert_eq!(json_diag.spans[0].file, "test.gruel");
1314 }
1315}