1use std::collections::HashMap;
26use std::io::IsTerminal;
27
28use annotate_snippets::{Level, Renderer, Snippet};
29
30use crate::{CompileError, CompileErrors, CompileWarning, Diagnostic, ErrorCode, FileId, Span};
31
32type FileSpanMap<'a> = HashMap<FileId, Vec<(Span, Option<&'a str>, Level)>>;
34
35#[derive(Debug, Clone)]
40pub struct SourceInfo<'a> {
41 pub source: &'a str,
43 pub path: &'a str,
45}
46
47impl<'a> SourceInfo<'a> {
48 pub fn new(source: &'a str, path: &'a str) -> Self {
50 Self { source, path }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum ColorChoice {
57 #[default]
59 Auto,
60 Always,
62 Never,
64}
65
66pub struct MultiFileFormatter<'a> {
92 sources: HashMap<FileId, SourceInfo<'a>>,
94 renderer: Renderer,
96}
97
98impl<'a> MultiFileFormatter<'a> {
99 pub fn new(sources: impl IntoIterator<Item = (FileId, SourceInfo<'a>)>) -> Self {
103 Self::with_color_choice(sources, ColorChoice::Auto)
104 }
105
106 pub fn with_color_choice(
108 sources: impl IntoIterator<Item = (FileId, SourceInfo<'a>)>,
109 color_choice: ColorChoice,
110 ) -> Self {
111 let use_color = match color_choice {
112 ColorChoice::Auto => std::io::stderr().is_terminal(),
113 ColorChoice::Always => true,
114 ColorChoice::Never => false,
115 };
116 let renderer = if use_color {
117 Renderer::styled()
118 } else {
119 Renderer::plain()
120 };
121 Self {
122 sources: sources.into_iter().collect(),
123 renderer,
124 }
125 }
126
127 fn get_source(&self, file_id: FileId) -> Option<&SourceInfo<'a>> {
129 self.sources.get(&file_id)
130 }
131
132 fn fallback_source(&self) -> Option<&SourceInfo<'a>> {
137 self.sources
139 .get(&FileId::DEFAULT)
140 .or_else(|| self.sources.values().next())
141 }
142
143 pub fn format_error(&self, error: &CompileError) -> String {
154 let message_with_code = format!("[{}]: {}", error.kind.code(), error.kind);
155
156 let is_ice = matches!(
158 error.kind.code(),
159 ErrorCode::INTERNAL_ERROR | ErrorCode::INTERNAL_CODEGEN_ERROR
160 );
161
162 let mut output = self.format_diagnostic_impl(
163 Level::Error,
164 &message_with_code,
165 error.span(),
166 error.diagnostic(),
167 );
168
169 if is_ice {
171 output.push_str("\nnote: this is a bug in the Gruel compiler\n");
172 output.push_str(
173 "help: please report this issue at https://github.com/ysimonson/gruel/issues\n",
174 );
175 }
176
177 output
178 }
179
180 pub fn format_errors(&self, errors: &CompileErrors) -> String {
185 if errors.is_empty() {
186 return String::new();
187 }
188
189 let mut output = String::new();
190 for error in errors.iter() {
191 if !output.is_empty() {
192 output.push('\n');
193 }
194 output.push_str(&self.format_error(error));
195 }
196
197 if errors.len() > 1 {
198 output.push_str(&format!(
199 "\nerror: aborting due to {} previous errors\n",
200 errors.len()
201 ));
202 }
203
204 output
205 }
206
207 pub fn format_warnings(&self, warnings: &[CompileWarning]) -> String {
209 if warnings.is_empty() {
210 return String::new();
211 }
212
213 let mut var_name_counts: HashMap<&str, usize> = HashMap::new();
215 for warning in warnings {
216 if let Some(name) = warning.kind.unused_variable_name() {
217 *var_name_counts.entry(name).or_insert(0) += 1;
218 }
219 }
220
221 let mut output = String::new();
223 for warning in warnings {
224 let needs_line_number = warning
225 .kind
226 .unused_variable_name()
227 .is_some_and(|name| var_name_counts.get(name).copied().unwrap_or(0) > 1);
228
229 if !output.is_empty() {
230 output.push('\n');
231 }
232 output.push_str(&self.format_warning_internal(warning, needs_line_number));
233 }
234 output
235 }
236
237 pub fn format_warning(&self, warning: &CompileWarning) -> String {
239 self.format_warning_internal(warning, false)
240 }
241
242 fn format_warning_internal(
243 &self,
244 warning: &CompileWarning,
245 include_line_number: bool,
246 ) -> String {
247 let message = if include_line_number {
249 if let Some(span) = warning.span() {
250 if let Some(source_info) = self
251 .get_source(span.file_id)
252 .or_else(|| self.fallback_source())
253 {
254 let line = span.line_number(source_info.source);
255 warning.kind.format_with_line(Some(line))
256 } else {
257 warning.to_string()
258 }
259 } else {
260 warning.to_string()
261 }
262 } else {
263 warning.to_string()
264 };
265
266 self.format_diagnostic_impl(
267 Level::Warning,
268 &message,
269 warning.span(),
270 warning.diagnostic(),
271 )
272 }
273
274 fn format_diagnostic_impl(
276 &self,
277 level: Level,
278 message: &str,
279 span: Option<Span>,
280 diagnostic: &Diagnostic,
281 ) -> String {
282 let Some(primary_span) = span else {
284 let mut report = level.title(message);
285 for note in &diagnostic.notes {
286 report = report.footer(Level::Note.title(note.0.as_str()));
287 }
288 for help in &diagnostic.helps {
289 report = report.footer(Level::Help.title(help.0.as_str()));
290 }
291 return format!("{}", self.renderer.render(report));
292 };
293
294 let mut file_spans = FileSpanMap::new();
296
297 file_spans
299 .entry(primary_span.file_id)
300 .or_default()
301 .push((primary_span, None, level));
302
303 for label in &diagnostic.labels {
305 file_spans.entry(label.span.file_id).or_default().push((
306 label.span,
307 Some(label.message.as_str()),
308 Level::Info,
309 ));
310 }
311
312 let mut report = level.title(message);
314
315 let mut file_ids: Vec<_> = file_spans.keys().copied().collect();
317 file_ids.sort_by_key(|id| id.0);
318
319 for file_id in file_ids {
320 let spans = &file_spans[&file_id];
321
322 let source_info = self.get_source(file_id).or_else(|| self.fallback_source());
324
325 let Some(source_info) = source_info 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)]
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)]
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)]
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 let mut obj = serde_json::Map::new();
432 obj.insert(
433 "code".to_string(),
434 serde_json::Value::String(self.code.clone()),
435 );
436 obj.insert(
437 "message".to_string(),
438 serde_json::Value::String(self.message.clone()),
439 );
440 obj.insert(
441 "severity".to_string(),
442 serde_json::Value::String(self.severity.to_string()),
443 );
444
445 let spans: Vec<serde_json::Value> = self
447 .spans
448 .iter()
449 .map(|s| {
450 let mut span = serde_json::Map::new();
451 span.insert(
452 "file".to_string(),
453 serde_json::Value::String(s.file.clone()),
454 );
455 span.insert(
456 "start".to_string(),
457 serde_json::Value::Number(s.start.into()),
458 );
459 span.insert("end".to_string(), serde_json::Value::Number(s.end.into()));
460 span.insert("line".to_string(), serde_json::Value::Number(s.line.into()));
461 span.insert(
462 "column".to_string(),
463 serde_json::Value::Number(s.column.into()),
464 );
465 if let Some(label) = &s.label {
466 span.insert(
467 "label".to_string(),
468 serde_json::Value::String(label.clone()),
469 );
470 } else {
471 span.insert("label".to_string(), serde_json::Value::Null);
472 }
473 span.insert("primary".to_string(), serde_json::Value::Bool(s.primary));
474 serde_json::Value::Object(span)
475 })
476 .collect();
477 obj.insert("spans".to_string(), serde_json::Value::Array(spans));
478
479 let suggestions: Vec<serde_json::Value> = self
481 .suggestions
482 .iter()
483 .map(|s| {
484 let mut sugg = serde_json::Map::new();
485 sugg.insert(
486 "message".to_string(),
487 serde_json::Value::String(s.message.clone()),
488 );
489 sugg.insert(
490 "file".to_string(),
491 serde_json::Value::String(s.file.clone()),
492 );
493 sugg.insert(
494 "start".to_string(),
495 serde_json::Value::Number(s.start.into()),
496 );
497 sugg.insert("end".to_string(), serde_json::Value::Number(s.end.into()));
498 sugg.insert(
499 "replacement".to_string(),
500 serde_json::Value::String(s.replacement.clone()),
501 );
502 sugg.insert(
503 "applicability".to_string(),
504 serde_json::Value::String(s.applicability.clone()),
505 );
506 serde_json::Value::Object(sugg)
507 })
508 .collect();
509 obj.insert(
510 "suggestions".to_string(),
511 serde_json::Value::Array(suggestions),
512 );
513
514 let notes: Vec<serde_json::Value> = self
516 .notes
517 .iter()
518 .map(|n| serde_json::Value::String(n.clone()))
519 .collect();
520 obj.insert("notes".to_string(), serde_json::Value::Array(notes));
521
522 let helps: Vec<serde_json::Value> = self
523 .helps
524 .iter()
525 .map(|h| serde_json::Value::String(h.clone()))
526 .collect();
527 obj.insert("helps".to_string(), serde_json::Value::Array(helps));
528
529 serde_json::to_string(&serde_json::Value::Object(obj)).unwrap_or_else(|_| "{}".to_string())
530 }
531}
532
533pub struct MultiFileJsonFormatter<'a> {
556 sources: HashMap<FileId, SourceInfo<'a>>,
558}
559
560impl<'a> MultiFileJsonFormatter<'a> {
561 pub fn new(sources: impl IntoIterator<Item = (FileId, SourceInfo<'a>)>) -> Self {
563 Self {
564 sources: sources.into_iter().collect(),
565 }
566 }
567
568 fn get_source(&self, file_id: FileId) -> Option<&SourceInfo<'a>> {
570 self.sources.get(&file_id)
571 }
572
573 fn fallback_source(&self) -> Option<&SourceInfo<'a>> {
575 self.sources
576 .get(&FileId::DEFAULT)
577 .or_else(|| self.sources.values().next())
578 }
579
580 fn offset_to_line_col(&self, source: &str, offset: u32) -> (u32, u32) {
582 let offset = offset as usize;
583 let mut line = 1u32;
584 let mut col = 1u32;
585 for (i, ch) in source.char_indices() {
586 if i >= offset {
587 break;
588 }
589 if ch == '\n' {
590 line += 1;
591 col = 1;
592 } else {
593 col += 1;
594 }
595 }
596 (line, col)
597 }
598
599 fn source_for_span(&self, span: Span) -> Option<(&str, &str)> {
601 self.get_source(span.file_id)
602 .or_else(|| self.fallback_source())
603 .map(|info| (info.path, info.source))
604 }
605
606 pub fn format_error(&self, error: &CompileError) -> JsonDiagnostic {
608 let diag = error.diagnostic();
609
610 let primary_span = error.span().and_then(|span| {
611 self.source_for_span(span).map(|(path, source)| {
612 let (line, col) = self.offset_to_line_col(source, span.start);
613 JsonSpan {
614 file: path.to_string(),
615 start: span.start,
616 end: span.end,
617 line,
618 column: col,
619 label: None,
620 primary: true,
621 }
622 })
623 });
624
625 let secondary_spans: Vec<JsonSpan> = diag
626 .labels
627 .iter()
628 .filter_map(|label| {
629 self.source_for_span(label.span).map(|(path, source)| {
630 let (line, col) = self.offset_to_line_col(source, label.span.start);
631 JsonSpan {
632 file: path.to_string(),
633 start: label.span.start,
634 end: label.span.end,
635 line,
636 column: col,
637 label: Some(label.message.clone()),
638 primary: false,
639 }
640 })
641 })
642 .collect();
643
644 let mut spans: Vec<JsonSpan> = primary_span.into_iter().collect();
645 spans.extend(secondary_spans);
646
647 let suggestions: Vec<JsonSuggestion> = diag
648 .suggestions
649 .iter()
650 .filter_map(|s| {
651 self.source_for_span(s.span)
652 .map(|(path, _)| JsonSuggestion {
653 message: s.message.clone(),
654 file: path.to_string(),
655 start: s.span.start,
656 end: s.span.end,
657 replacement: s.replacement.clone(),
658 applicability: s.applicability.to_string(),
659 })
660 })
661 .collect();
662
663 JsonDiagnostic {
664 code: format!("{}", error.kind.code()),
665 message: format!("{}", error.kind),
666 severity: "error",
667 spans,
668 suggestions,
669 notes: diag.notes.iter().map(|n| n.0.clone()).collect(),
670 helps: diag.helps.iter().map(|h| h.0.clone()).collect(),
671 }
672 }
673
674 pub fn format_warning(&self, warning: &CompileWarning) -> JsonDiagnostic {
676 let diag = warning.diagnostic();
677
678 let primary_span = warning.span().and_then(|span| {
679 self.source_for_span(span).map(|(path, source)| {
680 let (line, col) = self.offset_to_line_col(source, span.start);
681 JsonSpan {
682 file: path.to_string(),
683 start: span.start,
684 end: span.end,
685 line,
686 column: col,
687 label: None,
688 primary: true,
689 }
690 })
691 });
692
693 let secondary_spans: Vec<JsonSpan> = diag
694 .labels
695 .iter()
696 .filter_map(|label| {
697 self.source_for_span(label.span).map(|(path, source)| {
698 let (line, col) = self.offset_to_line_col(source, label.span.start);
699 JsonSpan {
700 file: path.to_string(),
701 start: label.span.start,
702 end: label.span.end,
703 line,
704 column: col,
705 label: Some(label.message.clone()),
706 primary: false,
707 }
708 })
709 })
710 .collect();
711
712 let mut spans: Vec<JsonSpan> = primary_span.into_iter().collect();
713 spans.extend(secondary_spans);
714
715 let suggestions: Vec<JsonSuggestion> = diag
716 .suggestions
717 .iter()
718 .filter_map(|s| {
719 self.source_for_span(s.span)
720 .map(|(path, _)| JsonSuggestion {
721 message: s.message.clone(),
722 file: path.to_string(),
723 start: s.span.start,
724 end: s.span.end,
725 replacement: s.replacement.clone(),
726 applicability: s.applicability.to_string(),
727 })
728 })
729 .collect();
730
731 JsonDiagnostic {
732 code: String::new(), message: format!("{}", warning.kind),
734 severity: "warning",
735 spans,
736 suggestions,
737 notes: diag.notes.iter().map(|n| n.0.clone()).collect(),
738 helps: diag.helps.iter().map(|h| h.0.clone()).collect(),
739 }
740 }
741
742 pub fn format_errors(&self, errors: &CompileErrors) -> String {
744 let diagnostics: Vec<String> = errors
745 .iter()
746 .map(|e| self.format_error(e).to_json())
747 .collect();
748 format!("[{}]", diagnostics.join(","))
749 }
750
751 pub fn format_warnings(&self, warnings: &[CompileWarning]) -> String {
753 let diagnostics: Vec<String> = warnings
754 .iter()
755 .map(|w| self.format_warning(w).to_json())
756 .collect();
757 format!("[{}]", diagnostics.join(","))
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764 use crate::{ErrorKind, Suggestion, WarningKind};
765
766 #[test]
767 fn test_format_error_with_span() {
768 let source = "fn main() -> i32 { 1 + true }";
769 let source_info = SourceInfo::new(source, "test.gruel");
770 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
771
772 let error = CompileError::new(
773 ErrorKind::TypeMismatch {
774 expected: "i32".to_string(),
775 found: "bool".to_string(),
776 },
777 Span::new(23, 27),
778 );
779
780 let output = formatter.format_error(&error);
781 assert!(output.contains("[E0206]"));
783 assert!(output.contains("type mismatch"));
784 assert!(output.contains("expected i32"));
785 assert!(output.contains("found bool"));
786 assert!(output.contains("test.gruel"));
787 }
788
789 #[test]
790 fn test_format_error_without_span() {
791 let source = "fn foo() -> i32 { 42 }";
792 let source_info = SourceInfo::new(source, "test.gruel");
793 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
794
795 let error = CompileError::without_span(ErrorKind::NoMainFunction);
796
797 let output = formatter.format_error(&error);
798 assert!(output.contains("[E0200]"));
800 assert!(output.contains("no main function"));
801 }
802
803 #[test]
804 fn test_format_warning() {
805 let source = "fn main() -> i32 { let x = 42; 0 }";
806 let source_info = SourceInfo::new(source, "test.gruel");
807 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
808
809 let warning = CompileWarning::new(
810 WarningKind::UnusedVariable("x".to_string()),
811 Span::new(23, 24),
812 );
813
814 let output = formatter.format_warning(&warning);
815 assert!(output.contains("unused variable"));
816 assert!(output.contains("'x'"));
817 }
818
819 #[test]
820 fn test_format_warnings_with_duplicates() {
821 let source = "fn main() -> i32 {\n let x = 1;\n let x = 2;\n 0\n}";
822 let source_info = SourceInfo::new(source, "test.gruel");
823 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
824
825 let warnings = vec![
826 CompileWarning::new(
827 WarningKind::UnusedVariable("x".to_string()),
828 Span::new(23, 24),
829 ),
830 CompileWarning::new(
831 WarningKind::UnusedVariable("x".to_string()),
832 Span::new(36, 37),
833 ),
834 ];
835
836 let output = formatter.format_warnings(&warnings);
837 assert!(output.contains("line"));
839 }
840
841 #[test]
842 fn test_format_warnings_empty() {
843 let source = "fn main() -> i32 { 42 }";
844 let source_info = SourceInfo::new(source, "test.gruel");
845 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
846
847 let output = formatter.format_warnings(&[]);
848 assert!(output.is_empty());
849 }
850
851 #[test]
852 fn test_format_error_with_help() {
853 let source = "fn main() -> i32 { x = 1; 0 }";
854 let source_info = SourceInfo::new(source, "test.gruel");
855 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
856
857 let error = CompileError::new(
858 ErrorKind::AssignToImmutable("x".to_string()),
859 Span::new(19, 20),
860 )
861 .with_help("consider making `x` mutable: `let mut x`");
862
863 let output = formatter.format_error(&error);
864 assert!(output.contains("help"));
865 assert!(output.contains("mutable"));
866 }
867
868 #[test]
869 fn test_format_error_with_label() {
870 let source = "fn main() -> i32 { if true { 1 } else { false } }";
871 let source_info = SourceInfo::new(source, "test.gruel");
872 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
873
874 let error = CompileError::new(
875 ErrorKind::TypeMismatch {
876 expected: "i32".to_string(),
877 found: "bool".to_string(),
878 },
879 Span::new(40, 45),
880 )
881 .with_label("then branch is here", Span::new(29, 30));
882
883 let output = formatter.format_error(&error);
884 assert!(output.contains("then branch"));
885 }
886
887 #[test]
888 fn test_format_errors_empty() {
889 let source = "fn main() -> i32 { 42 }";
890 let source_info = SourceInfo::new(source, "test.gruel");
891 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
892
893 let errors = CompileErrors::new();
894 let output = formatter.format_errors(&errors);
895 assert!(output.is_empty());
896 }
897
898 #[test]
899 fn test_format_errors_single() {
900 let source = "fn main() -> i32 { 1 + true }";
901 let source_info = SourceInfo::new(source, "test.gruel");
902 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
903
904 let mut errors = CompileErrors::new();
905 errors.push(CompileError::new(
906 ErrorKind::TypeMismatch {
907 expected: "i32".to_string(),
908 found: "bool".to_string(),
909 },
910 Span::new(23, 27),
911 ));
912
913 let output = formatter.format_errors(&errors);
914 assert!(output.contains("type mismatch"));
915 assert!(!output.contains("aborting"));
917 }
918
919 #[test]
920 fn test_format_errors_multiple() {
921 let source = "fn main() -> i32 {\n let x = 1 + true;\n let y = false - 1;\n 0\n}";
922 let source_info = SourceInfo::new(source, "test.gruel");
923 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
924
925 let mut errors = CompileErrors::new();
926 errors.push(CompileError::new(
927 ErrorKind::TypeMismatch {
928 expected: "i32".to_string(),
929 found: "bool".to_string(),
930 },
931 Span::new(32, 36),
932 ));
933 errors.push(CompileError::new(
934 ErrorKind::TypeMismatch {
935 expected: "bool".to_string(),
936 found: "i32".to_string(),
937 },
938 Span::new(58, 59),
939 ));
940
941 let output = formatter.format_errors(&errors);
942 assert!(output.contains("expected i32, found bool"));
944 assert!(output.contains("expected bool, found i32"));
945 assert!(output.contains("aborting due to 2 previous errors"));
947 }
948
949 #[test]
950 fn test_color_choice_never() {
951 let source = "fn main() -> i32 { 1 + true }";
952 let source_info = SourceInfo::new(source, "test.gruel");
953 let formatter = MultiFileFormatter::with_color_choice(
954 [(FileId::DEFAULT, source_info)],
955 ColorChoice::Never,
956 );
957
958 let error = CompileError::new(
959 ErrorKind::TypeMismatch {
960 expected: "i32".to_string(),
961 found: "bool".to_string(),
962 },
963 Span::new(23, 27),
964 );
965
966 let output = formatter.format_error(&error);
967 assert!(!output.contains("\x1b["));
969 assert!(output.contains("type mismatch"));
970 }
971
972 #[test]
973 fn test_format_error_with_invalid_span() {
974 let source = "fn main() -> i32 { 42 }";
975 let source_info = SourceInfo::new(source, "test.gruel");
976 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
977
978 let error = CompileError::new(
980 ErrorKind::TypeMismatch {
981 expected: "i32".to_string(),
982 found: "bool".to_string(),
983 },
984 Span::new(20, 1000), );
986
987 let output = formatter.format_error(&error);
989 assert!(output.contains("[E0206]"));
990 assert!(output.contains("type mismatch"));
991 }
992
993 #[test]
994 fn test_format_error_with_reversed_span() {
995 let source = "fn main() -> i32 { 42 }";
996 let source_info = SourceInfo::new(source, "test.gruel");
997 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
998
999 let error = CompileError::new(
1001 ErrorKind::TypeMismatch {
1002 expected: "i32".to_string(),
1003 found: "bool".to_string(),
1004 },
1005 Span::new(20, 10), );
1007
1008 let output = formatter.format_error(&error);
1010 assert!(output.contains("[E0206]"));
1011 assert!(output.contains("type mismatch"));
1012 }
1013
1014 #[test]
1015 fn test_color_choice_always() {
1016 let source = "fn main() -> i32 { 1 + true }";
1017 let source_info = SourceInfo::new(source, "test.gruel");
1018 let formatter = MultiFileFormatter::with_color_choice(
1019 [(FileId::DEFAULT, source_info)],
1020 ColorChoice::Always,
1021 );
1022
1023 let error = CompileError::new(
1024 ErrorKind::TypeMismatch {
1025 expected: "i32".to_string(),
1026 found: "bool".to_string(),
1027 },
1028 Span::new(23, 27),
1029 );
1030
1031 let output = formatter.format_error(&error);
1032 assert!(output.contains("\x1b["));
1034 assert!(output.contains("type mismatch"));
1035 }
1036
1037 #[test]
1038 fn test_ice_formatting() {
1039 let source = "fn main() -> i32 { 42 }";
1040 let source_info = SourceInfo::new(source, "test.gruel");
1041 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
1042
1043 let error = CompileError::without_span(ErrorKind::InternalError(
1044 "unexpected type in codegen".to_string(),
1045 ));
1046
1047 let output = formatter.format_error(&error);
1048 assert!(output.contains("[E9000]"));
1050 assert!(output.contains("internal compiler error"));
1051 assert!(output.contains("note: this is a bug in the Gruel compiler"));
1053 assert!(output.contains("help: please report this issue"));
1054 assert!(output.contains("github.com/ysimonson/gruel/issues"));
1055 }
1056
1057 #[test]
1058 fn test_ice_codegen_formatting() {
1059 let source = "fn main() -> i32 { 42 }";
1060 let source_info = SourceInfo::new(source, "test.gruel");
1061 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
1062
1063 let error = CompileError::without_span(ErrorKind::InternalCodegenError(
1064 "failed to emit instruction".to_string(),
1065 ));
1066
1067 let output = formatter.format_error(&error);
1068 assert!(output.contains("[E9001]"));
1070 assert!(output.contains("internal codegen error"));
1071 assert!(output.contains("note: this is a bug in the Gruel compiler"));
1073 assert!(output.contains("help: please report this issue"));
1074 }
1075
1076 #[test]
1077 fn test_non_ice_no_extra_formatting() {
1078 let source = "fn main() -> i32 { 1 + true }";
1079 let source_info = SourceInfo::new(source, "test.gruel");
1080 let formatter = MultiFileFormatter::new([(FileId::DEFAULT, source_info)]);
1081
1082 let error = CompileError::new(
1083 ErrorKind::TypeMismatch {
1084 expected: "i32".to_string(),
1085 found: "bool".to_string(),
1086 },
1087 Span::new(23, 27),
1088 );
1089
1090 let output = formatter.format_error(&error);
1091 assert!(!output.contains("note: this is a bug in the Gruel compiler"));
1093 assert!(!output.contains("please report this issue"));
1094 }
1095
1096 #[test]
1101 fn test_json_format_error() {
1102 let source = "fn main() -> i32 { 1 + true }";
1103 let source_info = SourceInfo::new(source, "test.gruel");
1104 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
1105
1106 let error = CompileError::new(
1107 ErrorKind::TypeMismatch {
1108 expected: "i32".to_string(),
1109 found: "bool".to_string(),
1110 },
1111 Span::new(23, 27),
1112 );
1113
1114 let json_diag = formatter.format_error(&error);
1115 assert_eq!(json_diag.severity, "error");
1116 assert_eq!(json_diag.code, "E0206");
1117 assert!(json_diag.message.contains("type mismatch"));
1118 assert_eq!(json_diag.spans.len(), 1);
1119 assert_eq!(json_diag.spans[0].file, "test.gruel");
1120 assert_eq!(json_diag.spans[0].start, 23);
1121 assert_eq!(json_diag.spans[0].end, 27);
1122 assert!(json_diag.spans[0].primary);
1123 }
1124
1125 #[test]
1126 fn test_json_format_error_line_col() {
1127 let source = "fn main() -> i32 {\n 1 + true\n}";
1128 let source_info = SourceInfo::new(source, "test.gruel");
1130 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
1131
1132 let error = CompileError::new(
1133 ErrorKind::TypeMismatch {
1134 expected: "i32".to_string(),
1135 found: "bool".to_string(),
1136 },
1137 Span::new(27, 31), );
1139
1140 let json_diag = formatter.format_error(&error);
1141 assert_eq!(json_diag.spans[0].line, 2);
1142 assert!(json_diag.spans[0].column > 1); }
1144
1145 #[test]
1146 fn test_json_format_error_with_suggestion() {
1147 let source = "fn main() -> i32 { x = 1; 0 }";
1148 let source_info = SourceInfo::new(source, "test.gruel");
1149 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
1150
1151 let error = CompileError::new(
1152 ErrorKind::AssignToImmutable("x".to_string()),
1153 Span::new(19, 20),
1154 )
1155 .with_suggestion(Suggestion::machine_applicable(
1156 "add mut",
1157 Span::new(4, 5),
1158 "mut x",
1159 ));
1160
1161 let json_diag = formatter.format_error(&error);
1162 assert_eq!(json_diag.suggestions.len(), 1);
1163 assert_eq!(json_diag.suggestions[0].message, "add mut");
1164 assert_eq!(json_diag.suggestions[0].replacement, "mut x");
1165 assert_eq!(json_diag.suggestions[0].applicability, "MachineApplicable");
1166 }
1167
1168 #[test]
1169 fn test_json_format_warning() {
1170 let source = "fn main() -> i32 { let x = 42; 0 }";
1171 let source_info = SourceInfo::new(source, "test.gruel");
1172 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
1173
1174 let warning = CompileWarning::new(
1175 WarningKind::UnusedVariable("x".to_string()),
1176 Span::new(23, 24),
1177 );
1178
1179 let json_diag = formatter.format_warning(&warning);
1180 assert_eq!(json_diag.severity, "warning");
1181 assert!(json_diag.message.contains("unused variable"));
1182 }
1183
1184 #[test]
1185 fn test_json_to_string() {
1186 let source = "fn main() -> i32 { 1 + true }";
1187 let source_info = SourceInfo::new(source, "test.gruel");
1188 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
1189
1190 let error = CompileError::new(
1191 ErrorKind::TypeMismatch {
1192 expected: "i32".to_string(),
1193 found: "bool".to_string(),
1194 },
1195 Span::new(23, 27),
1196 );
1197
1198 let json_diag = formatter.format_error(&error);
1199 let json_str = json_diag.to_json();
1200
1201 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1203 assert_eq!(parsed["severity"], "error");
1204 assert_eq!(parsed["code"], "E0206");
1205 assert!(parsed["spans"].is_array());
1206 assert_eq!(parsed["spans"][0]["primary"], true);
1207 }
1208
1209 #[test]
1210 fn test_json_format_errors_array() {
1211 let source = "fn main() -> i32 {\n 1 + true\n}";
1212 let source_info = SourceInfo::new(source, "test.gruel");
1213 let formatter = MultiFileJsonFormatter::new([(FileId::DEFAULT, source_info)]);
1214
1215 let mut errors = CompileErrors::new();
1216 errors.push(CompileError::new(
1217 ErrorKind::TypeMismatch {
1218 expected: "i32".to_string(),
1219 found: "bool".to_string(),
1220 },
1221 Span::new(27, 31),
1222 ));
1223
1224 let json_str = formatter.format_errors(&errors);
1225 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1226 assert!(parsed.is_array());
1227 assert_eq!(parsed.as_array().unwrap().len(), 1);
1228 }
1229
1230 #[test]
1235 fn test_multi_file_formatter_single_file() {
1236 let source = "fn main() -> i32 { 1 + true }";
1237 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1238 let formatter = MultiFileFormatter::new(sources);
1239
1240 let error = CompileError::new(
1241 ErrorKind::TypeMismatch {
1242 expected: "i32".to_string(),
1243 found: "bool".to_string(),
1244 },
1245 Span::with_file(FileId::new(1), 23, 27),
1246 );
1247
1248 let output = formatter.format_error(&error);
1249 assert!(output.contains("[E0206]"));
1250 assert!(output.contains("type mismatch"));
1251 assert!(output.contains("test.gruel"));
1252 }
1253
1254 #[test]
1255 fn test_multi_file_formatter_multiple_files() {
1256 let source1 = "fn main() -> i32 { helper() }";
1257 let source2 = "fn helper() -> bool { true }";
1258 let sources = vec![
1259 (FileId::new(1), SourceInfo::new(source1, "main.gruel")),
1260 (FileId::new(2), SourceInfo::new(source2, "helper.gruel")),
1261 ];
1262 let formatter = MultiFileFormatter::new(sources);
1263
1264 let error = CompileError::new(
1266 ErrorKind::TypeMismatch {
1267 expected: "i32".to_string(),
1268 found: "bool".to_string(),
1269 },
1270 Span::with_file(FileId::new(1), 19, 27),
1271 )
1272 .with_label(
1273 "function returns bool here",
1274 Span::with_file(FileId::new(2), 15, 19),
1275 );
1276
1277 let output = formatter.format_error(&error);
1278 assert!(output.contains("main.gruel"));
1280 assert!(output.contains("helper.gruel"));
1281 assert!(output.contains("function returns bool here"));
1282 }
1283
1284 #[test]
1285 fn test_multi_file_formatter_without_span() {
1286 let source = "fn foo() -> i32 { 42 }";
1287 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1288 let formatter = MultiFileFormatter::new(sources);
1289
1290 let error = CompileError::without_span(ErrorKind::NoMainFunction);
1291
1292 let output = formatter.format_error(&error);
1293 assert!(output.contains("[E0200]"));
1294 assert!(output.contains("no main function"));
1295 }
1296
1297 #[test]
1298 fn test_multi_file_formatter_format_errors() {
1299 let source = "fn main() -> i32 { 1 + true }";
1300 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1301 let formatter = MultiFileFormatter::new(sources);
1302
1303 let mut errors = CompileErrors::new();
1304 errors.push(CompileError::new(
1305 ErrorKind::TypeMismatch {
1306 expected: "i32".to_string(),
1307 found: "bool".to_string(),
1308 },
1309 Span::with_file(FileId::new(1), 23, 27),
1310 ));
1311 errors.push(CompileError::without_span(ErrorKind::NoMainFunction));
1312
1313 let output = formatter.format_errors(&errors);
1314 assert!(output.contains("type mismatch"));
1315 assert!(output.contains("no main function"));
1316 assert!(output.contains("aborting due to 2 previous errors"));
1317 }
1318
1319 #[test]
1320 fn test_multi_file_formatter_format_warnings() {
1321 let source = "fn main() -> i32 { let x = 42; 0 }";
1322 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1323 let formatter = MultiFileFormatter::new(sources);
1324
1325 let warnings = vec![CompileWarning::new(
1326 WarningKind::UnusedVariable("x".to_string()),
1327 Span::with_file(FileId::new(1), 23, 24),
1328 )];
1329
1330 let output = formatter.format_warnings(&warnings);
1331 assert!(output.contains("unused variable"));
1332 assert!(output.contains("'x'"));
1333 }
1334
1335 #[test]
1336 fn test_multi_file_formatter_color_choice() {
1337 let source = "fn main() -> i32 { 1 + true }";
1338 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1339 let formatter = MultiFileFormatter::with_color_choice(sources, ColorChoice::Never);
1340
1341 let error = CompileError::new(
1342 ErrorKind::TypeMismatch {
1343 expected: "i32".to_string(),
1344 found: "bool".to_string(),
1345 },
1346 Span::with_file(FileId::new(1), 23, 27),
1347 );
1348
1349 let output = formatter.format_error(&error);
1350 assert!(!output.contains("\x1b["));
1352 }
1353
1354 #[test]
1355 fn test_multi_file_formatter_invalid_span() {
1356 let source = "fn main() -> i32 { 42 }";
1357 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1358 let formatter = MultiFileFormatter::new(sources);
1359
1360 let error = CompileError::new(
1362 ErrorKind::TypeMismatch {
1363 expected: "i32".to_string(),
1364 found: "bool".to_string(),
1365 },
1366 Span::with_file(FileId::new(1), 20, 1000),
1367 );
1368
1369 let output = formatter.format_error(&error);
1371 assert!(output.contains("[E0206]"));
1372 assert!(output.contains("type mismatch"));
1373 }
1374
1375 #[test]
1376 fn test_multi_file_formatter_reversed_span() {
1377 let source = "fn main() -> i32 { 42 }";
1378 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1379 let formatter = MultiFileFormatter::new(sources);
1380
1381 let error = CompileError::new(
1383 ErrorKind::TypeMismatch {
1384 expected: "i32".to_string(),
1385 found: "bool".to_string(),
1386 },
1387 Span::with_file(FileId::new(1), 20, 10),
1388 );
1389
1390 let output = formatter.format_error(&error);
1392 assert!(output.contains("[E0206]"));
1393 assert!(output.contains("type mismatch"));
1394 }
1395
1396 #[test]
1401 fn test_multi_file_json_formatter_single_file() {
1402 let source = "fn main() -> i32 { 1 + true }";
1403 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1404 let formatter = MultiFileJsonFormatter::new(sources);
1405
1406 let error = CompileError::new(
1407 ErrorKind::TypeMismatch {
1408 expected: "i32".to_string(),
1409 found: "bool".to_string(),
1410 },
1411 Span::with_file(FileId::new(1), 23, 27),
1412 );
1413
1414 let json_diag = formatter.format_error(&error);
1415 assert_eq!(json_diag.severity, "error");
1416 assert_eq!(json_diag.code, "E0206");
1417 assert_eq!(json_diag.spans.len(), 1);
1418 assert_eq!(json_diag.spans[0].file, "test.gruel");
1419 assert!(json_diag.spans[0].primary);
1420 }
1421
1422 #[test]
1423 fn test_multi_file_json_formatter_multiple_files() {
1424 let source1 = "fn main() -> i32 { helper() }";
1425 let source2 = "fn helper() -> bool { true }";
1426 let sources = vec![
1427 (FileId::new(1), SourceInfo::new(source1, "main.gruel")),
1428 (FileId::new(2), SourceInfo::new(source2, "helper.gruel")),
1429 ];
1430 let formatter = MultiFileJsonFormatter::new(sources);
1431
1432 let error = CompileError::new(
1434 ErrorKind::TypeMismatch {
1435 expected: "i32".to_string(),
1436 found: "bool".to_string(),
1437 },
1438 Span::with_file(FileId::new(1), 19, 27),
1439 )
1440 .with_label(
1441 "function returns bool here",
1442 Span::with_file(FileId::new(2), 15, 19),
1443 );
1444
1445 let json_diag = formatter.format_error(&error);
1446 assert_eq!(json_diag.spans.len(), 2);
1447
1448 let primary = json_diag.spans.iter().find(|s| s.primary).unwrap();
1450 assert_eq!(primary.file, "main.gruel");
1451
1452 let secondary = json_diag.spans.iter().find(|s| !s.primary).unwrap();
1454 assert_eq!(secondary.file, "helper.gruel");
1455 assert_eq!(
1456 secondary.label,
1457 Some("function returns bool here".to_string())
1458 );
1459 }
1460
1461 #[test]
1462 fn test_multi_file_json_formatter_format_errors() {
1463 let source = "fn main() -> i32 { 1 + true }";
1464 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1465 let formatter = MultiFileJsonFormatter::new(sources);
1466
1467 let mut errors = CompileErrors::new();
1468 errors.push(CompileError::new(
1469 ErrorKind::TypeMismatch {
1470 expected: "i32".to_string(),
1471 found: "bool".to_string(),
1472 },
1473 Span::with_file(FileId::new(1), 23, 27),
1474 ));
1475
1476 let json_str = formatter.format_errors(&errors);
1477 let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1478 assert!(parsed.is_array());
1479 assert_eq!(parsed.as_array().unwrap().len(), 1);
1480 }
1481
1482 #[test]
1483 fn test_multi_file_json_formatter_format_warning() {
1484 let source = "fn main() -> i32 { let x = 42; 0 }";
1485 let sources = vec![(FileId::new(1), SourceInfo::new(source, "test.gruel"))];
1486 let formatter = MultiFileJsonFormatter::new(sources);
1487
1488 let warning = CompileWarning::new(
1489 WarningKind::UnusedVariable("x".to_string()),
1490 Span::with_file(FileId::new(1), 23, 24),
1491 );
1492
1493 let json_diag = formatter.format_warning(&warning);
1494 assert_eq!(json_diag.severity, "warning");
1495 assert!(json_diag.message.contains("unused variable"));
1496 assert_eq!(json_diag.spans.len(), 1);
1497 assert_eq!(json_diag.spans[0].file, "test.gruel");
1498 }
1499}