1use oxur_smap::{SourceMap, SourcePos};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12#[derive(Debug, Error)]
14pub enum TranslationError {
15 #[error("Failed to parse rustc JSON: {0}")]
16 JsonParseFailed(#[from] serde_json::Error),
17
18 #[error("No source map available")]
19 NoSourceMap,
20
21 #[error("Position lookup failed: {0}")]
22 LookupFailed(String),
23}
24
25pub type Result<T> = std::result::Result<T, TranslationError>;
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29#[serde(rename_all = "lowercase")]
30pub enum DiagnosticLevel {
31 Error,
32 Warning,
33 Note,
34 Help,
35 #[serde(rename = "failure-note")]
36 FailureNote,
37 #[serde(other)]
38 Other,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RustcSpan {
44 pub file_name: String,
45 pub byte_start: usize,
46 pub byte_end: usize,
47 pub line_start: usize,
48 pub line_end: usize,
49 pub column_start: usize,
50 pub column_end: usize,
51 #[serde(default)]
52 pub is_primary: bool,
53 #[serde(default)]
54 pub label: Option<String>,
55}
56
57#[allow(dead_code)]
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct RustcSuggestion {
61 pub message: String,
62 pub applicability: String,
63 pub spans: Vec<RustcSpan>,
64 #[serde(default)]
65 pub replacement: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct RustcDiagnostic {
71 pub message: String,
72 pub code: Option<RustcCode>,
73 pub level: DiagnosticLevel,
74 pub spans: Vec<RustcSpan>,
75 #[serde(default)]
76 pub children: Vec<RustcDiagnostic>,
77 #[serde(default)]
78 pub rendered: Option<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct RustcCode {
84 pub code: String,
85 pub explanation: Option<String>,
86}
87
88#[derive(Debug, Clone)]
90pub struct TranslatedDiagnostic {
91 pub message: String,
92 pub level: DiagnosticLevel,
93 pub code: Option<String>,
94 pub primary_span: Option<TranslatedSpan>,
95 pub secondary_spans: Vec<TranslatedSpan>,
96 pub children: Vec<TranslatedDiagnostic>,
97 pub suggestion: Option<String>,
98}
99
100#[derive(Debug, Clone)]
102pub struct TranslatedSpan {
103 pub pos: SourcePos,
104 pub label: Option<String>,
105 pub source_text: Option<String>,
106}
107
108pub struct ErrorTranslator {
130 source_map: Option<SourceMap>,
131}
132
133impl ErrorTranslator {
134 pub fn new() -> Self {
138 Self { source_map: None }
139 }
140
141 pub fn with_source_map(source_map: SourceMap) -> Self {
143 Self { source_map: Some(source_map) }
144 }
145
146 pub fn parse_and_translate(&self, stderr: &str) -> Result<Vec<TranslatedDiagnostic>> {
156 let mut diagnostics = Vec::new();
157
158 for line in stderr.lines() {
160 if line.trim().is_empty() {
162 continue;
163 }
164
165 match serde_json::from_str::<RustcDiagnostic>(line) {
167 Ok(diag) => {
168 let translated = self.translate_diagnostic(&diag)?;
170 diagnostics.push(translated);
171 }
172 Err(e) => {
173 if line.starts_with('{') {
176 return Err(TranslationError::JsonParseFailed(e));
177 }
178 }
179 }
180 }
181
182 Ok(diagnostics)
183 }
184
185 fn translate_diagnostic(&self, diag: &RustcDiagnostic) -> Result<TranslatedDiagnostic> {
187 let primary_span =
189 diag.spans.iter().find(|s| s.is_primary).and_then(|s| self.translate_span(s).ok());
190
191 let secondary_spans = diag
193 .spans
194 .iter()
195 .filter(|s| !s.is_primary)
196 .filter_map(|s| self.translate_span(s).ok())
197 .collect();
198
199 let children = diag
201 .children
202 .iter()
203 .filter_map(|child| self.translate_diagnostic(child).ok())
204 .collect();
205
206 Ok(TranslatedDiagnostic {
207 message: diag.message.clone(),
208 level: diag.level.clone(),
209 code: diag.code.as_ref().map(|c| c.code.clone()),
210 primary_span,
211 secondary_spans,
212 children,
213 suggestion: None, })
215 }
216
217 fn translate_span(&self, span: &RustcSpan) -> Result<TranslatedSpan> {
219 let pos = if let Some(source_map) = &self.source_map {
221 match self.extract_node_id_from_file(
223 &span.file_name,
224 span.line_start,
225 span.column_start,
226 ) {
227 Ok(node_id) => {
228 source_map.lookup(&node_id).unwrap_or_else(|| {
230 SourcePos::repl(
232 span.line_start as u32,
233 span.column_start as u32,
234 (span.byte_end - span.byte_start) as u32,
235 )
236 })
237 }
238 Err(_) => {
239 SourcePos::repl(
241 span.line_start as u32,
242 span.column_start as u32,
243 (span.byte_end - span.byte_start) as u32,
244 )
245 }
246 }
247 } else {
248 SourcePos::repl(
250 span.line_start as u32,
251 span.column_start as u32,
252 (span.byte_end - span.byte_start) as u32,
253 )
254 };
255
256 Ok(TranslatedSpan { pos, label: span.label.clone(), source_text: None })
257 }
258
259 fn extract_node_id_from_file(
264 &self,
265 file_path: &str,
266 line: usize,
267 column: usize,
268 ) -> Result<oxur_smap::NodeId> {
269 let source = std::fs::read_to_string(file_path).map_err(|e| {
271 TranslationError::LookupFailed(format!("Failed to read {}: {}", file_path, e))
272 })?;
273
274 let line_content = source.lines().nth(line.saturating_sub(1)).ok_or_else(|| {
276 TranslationError::LookupFailed(format!("Line {} not found in {}", line, file_path))
277 })?;
278
279 self.extract_node_id_from_line(line_content, column)
281 }
282
283 fn extract_node_id_from_line(&self, line: &str, column: usize) -> Result<oxur_smap::NodeId> {
288 use regex::Regex;
289
290 let pattern = Regex::new(r"/\*\s*oxur_node=(\d+)\s*\*/").map_err(|e| {
292 TranslationError::LookupFailed(format!("Regex compilation failed: {}", e))
293 })?;
294
295 let mut best_match: Option<(usize, u32)> = None;
297
298 for capture in pattern.captures_iter(line) {
299 if let Some(whole_match) = capture.get(0) {
300 if let Some(node_id_str) = capture.get(1) {
301 let match_start = whole_match.start();
302 let node_id: u32 = node_id_str.as_str().parse().map_err(|e| {
303 TranslationError::LookupFailed(format!("Invalid node_id: {}", e))
304 })?;
305
306 let distance = (match_start as i32 - column.saturating_sub(1) as i32)
308 .unsigned_abs() as usize;
309
310 if best_match.is_none() || distance < best_match.unwrap().0 {
312 best_match = Some((distance, node_id));
313 }
314 }
315 }
316 }
317
318 best_match.map(|(_, id)| oxur_smap::NodeId::from_raw(id)).ok_or_else(|| {
320 TranslationError::LookupFailed("No oxur_node comment found in line".to_string())
321 })
322 }
323}
324
325impl Default for ErrorTranslator {
326 fn default() -> Self {
327 Self::new()
328 }
329}
330
331impl TranslatedDiagnostic {
332 pub fn format(&self) -> String {
334 let mut output = String::new();
335
336 let level_str = match self.level {
338 DiagnosticLevel::Error => "error",
339 DiagnosticLevel::Warning => "warning",
340 DiagnosticLevel::Note => "note",
341 DiagnosticLevel::Help => "help",
342 DiagnosticLevel::FailureNote => "failure-note",
343 DiagnosticLevel::Other => "diagnostic",
344 };
345
346 if let Some(code) = &self.code {
347 output.push_str(&format!("{}[{}]: {}\n", level_str, code, self.message));
348 } else {
349 output.push_str(&format!("{}: {}\n", level_str, self.message));
350 }
351
352 if let Some(span) = &self.primary_span {
354 output.push_str(&format!(" --> line {}, column {}\n", span.pos.line, span.pos.column));
355
356 if let Some(label) = &span.label {
357 output.push_str(&format!(" | {}\n", label));
358 }
359 }
360
361 for child in &self.children {
363 let child_str = child.format();
364 for line in child_str.lines() {
365 output.push_str(&format!(" {}\n", line));
366 }
367 }
368
369 output
370 }
371
372 pub fn display_with_ariadne(&self, source: &str) -> String {
409 use ariadne::{Color, Label, Report, ReportKind, Source};
410
411 let kind = match self.level {
413 DiagnosticLevel::Error => ReportKind::Error,
414 DiagnosticLevel::Warning => ReportKind::Warning,
415 DiagnosticLevel::Note | DiagnosticLevel::Help => ReportKind::Advice,
416 DiagnosticLevel::FailureNote | DiagnosticLevel::Other => {
417 ReportKind::Custom("note", Color::Cyan)
418 }
419 };
420
421 let mut report_builder = Report::build(kind, "<repl>", 0).with_message(&self.message);
423
424 if let Some(code) = &self.code {
426 report_builder = report_builder.with_code(code.clone());
427 }
428
429 if let Some(primary) = &self.primary_span {
431 let byte_offset = self.calculate_byte_offset(source, &primary.pos);
433 let end_offset = byte_offset + primary.pos.length as usize;
434
435 let mut label = Label::new(("<repl>", byte_offset..end_offset)).with_color(Color::Red);
436
437 if let Some(msg) = &primary.label {
438 label = label.with_message(msg);
439 }
440
441 report_builder = report_builder.with_label(label);
442 }
443
444 for secondary in &self.secondary_spans {
446 let byte_offset = self.calculate_byte_offset(source, &secondary.pos);
447 let end_offset = byte_offset + secondary.pos.length as usize;
448
449 let mut label =
450 Label::new(("<repl>", byte_offset..end_offset)).with_color(Color::Yellow);
451
452 if let Some(msg) = &secondary.label {
453 label = label.with_message(msg);
454 }
455
456 report_builder = report_builder.with_label(label);
457 }
458
459 for child in &self.children {
461 if child.level == DiagnosticLevel::Help {
462 report_builder = report_builder.with_help(&child.message);
463 } else if child.level == DiagnosticLevel::Note {
464 report_builder = report_builder.with_note(&child.message);
465 }
466 }
467
468 let mut output = Vec::new();
470
471 report_builder
473 .finish()
474 .write(("<repl>", Source::from(source)), &mut output)
475 .expect("Failed to write ariadne report");
476
477 String::from_utf8(output).unwrap_or_else(|_| "Error formatting diagnostic".to_string())
478 }
479
480 fn calculate_byte_offset(&self, source: &str, pos: &SourcePos) -> usize {
482 let mut offset = 0;
483 let mut current_line = 1;
484
485 for (idx, ch) in source.char_indices() {
486 if current_line >= pos.line {
487 let column_offset = source[offset..]
489 .chars()
490 .take((pos.column.saturating_sub(1)) as usize)
491 .map(|c| c.len_utf8())
492 .sum::<usize>();
493 return offset + column_offset;
494 }
495
496 if ch == '\n' {
497 current_line += 1;
498 offset = idx + 1;
499 }
500 }
501
502 offset
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn test_error_translator_creation() {
513 let translator = ErrorTranslator::new();
514 assert!(translator.source_map.is_none());
515 }
516
517 #[test]
518 fn test_parse_simple_error() {
519 let translator = ErrorTranslator::new();
520
521 let json = r#"{"message":"cannot find value `x` in this scope","code":{"code":"E0425","explanation":"..."},"level":"error","spans":[{"file_name":"test.rs","byte_start":0,"byte_end":1,"line_start":1,"line_end":1,"column_start":5,"column_end":6,"is_primary":true,"label":"not found in this scope"}],"children":[]}"#;
522
523 let result = translator.parse_and_translate(json);
524 assert!(result.is_ok());
525
526 let diagnostics = result.unwrap();
527 assert_eq!(diagnostics.len(), 1);
528
529 let diag = &diagnostics[0];
530 assert_eq!(diag.message, "cannot find value `x` in this scope");
531 assert_eq!(diag.level, DiagnosticLevel::Error);
532 assert_eq!(diag.code, Some("E0425".to_string()));
533 }
534
535 #[test]
536 fn test_parse_empty_input() {
537 let translator = ErrorTranslator::new();
538 let result = translator.parse_and_translate("");
539 assert!(result.is_ok());
540 assert_eq!(result.unwrap().len(), 0);
541 }
542
543 #[test]
544 fn test_format_diagnostic() {
545 let diag = TranslatedDiagnostic {
546 message: "test error".to_string(),
547 level: DiagnosticLevel::Error,
548 code: Some("E0001".to_string()),
549 primary_span: Some(TranslatedSpan {
550 pos: SourcePos::repl(10, 15, 5),
551 label: Some("here".to_string()),
552 source_text: None,
553 }),
554 secondary_spans: vec![],
555 children: vec![],
556 suggestion: None,
557 };
558
559 let formatted = diag.format();
560 assert!(formatted.contains("error[E0001]: test error"));
561 assert!(formatted.contains("line 10, column 15"));
562 assert!(formatted.contains("here"));
563 }
564
565 #[test]
568 fn test_extract_node_id_from_line() {
569 let translator = ErrorTranslator::new();
570
571 let line = "/* oxur_node=123 */ let x = 42;";
573 let result = translator.extract_node_id_from_line(line, 5);
574 assert!(result.is_ok());
575 assert_eq!(result.unwrap().as_raw(), 123);
576 }
577
578 #[test]
579 fn test_extract_node_id_multiple_comments() {
580 let translator = ErrorTranslator::new();
581
582 let line = "/* oxur_node=10 */ let x = /* oxur_node=20 */ 42;";
584
585 let result1 = translator.extract_node_id_from_line(line, 5);
587 assert!(result1.is_ok());
588 assert_eq!(result1.unwrap().as_raw(), 10);
589
590 let result2 = translator.extract_node_id_from_line(line, 30);
592 assert!(result2.is_ok());
593 assert_eq!(result2.unwrap().as_raw(), 20);
594 }
595
596 #[test]
597 fn test_extract_node_id_with_whitespace() {
598 let translator = ErrorTranslator::new();
599
600 let line = "/* oxur_node=456 */ let y = 100;";
602 let result = translator.extract_node_id_from_line(line, 10);
603 assert!(result.is_ok());
604 assert_eq!(result.unwrap().as_raw(), 456);
605 }
606
607 #[test]
608 fn test_extract_node_id_no_comment() {
609 let translator = ErrorTranslator::new();
610
611 let line = "let x = 42; // regular comment";
613 let result = translator.extract_node_id_from_line(line, 5);
614 assert!(result.is_err());
615 assert!(matches!(result, Err(TranslationError::LookupFailed(_))));
616 }
617
618 #[test]
619 fn test_extract_node_id_invalid_number() {
620 let translator = ErrorTranslator::new();
621
622 let line = "/* oxur_node=abc */ let x = 42;";
624 let result = translator.extract_node_id_from_line(line, 5);
625 assert!(result.is_err());
626 }
627
628 #[test]
631 fn test_display_with_ariadne() {
632 let diag = TranslatedDiagnostic {
633 message: "cannot find value `x` in this scope".to_string(),
634 level: DiagnosticLevel::Error,
635 code: Some("E0425".to_string()),
636 primary_span: Some(TranslatedSpan {
637 pos: SourcePos::repl(2, 10, 1),
638 label: Some("not found in this scope".to_string()),
639 source_text: None,
640 }),
641 secondary_spans: vec![],
642 children: vec![],
643 suggestion: None,
644 };
645
646 let source = "(def y 42)\n(+ y x z)";
647 let output = diag.display_with_ariadne(source);
648
649 assert!(output.contains("E0425"));
651 assert!(output.contains("cannot find value `x` in this scope"));
652
653 assert!(!output.is_empty());
655 }
656
657 #[test]
658 fn test_display_with_ariadne_multiline() {
659 let diag = TranslatedDiagnostic {
660 message: "type mismatch".to_string(),
661 level: DiagnosticLevel::Error,
662 code: Some("E0308".to_string()),
663 primary_span: Some(TranslatedSpan {
664 pos: SourcePos::repl(3, 5, 3),
665 label: Some("expected i32, found &str".to_string()),
666 source_text: None,
667 }),
668 secondary_spans: vec![],
669 children: vec![TranslatedDiagnostic {
670 message: "consider using .parse()".to_string(),
671 level: DiagnosticLevel::Help,
672 code: None,
673 primary_span: None,
674 secondary_spans: vec![],
675 children: vec![],
676 suggestion: None,
677 }],
678 suggestion: None,
679 };
680
681 let source = "(def x 42)\n(def y \"hello\")\n(+ x y)";
682 let output = diag.display_with_ariadne(source);
683
684 assert!(output.contains("E0308"));
686 assert!(output.contains("type mismatch"));
687 assert!(output.contains("consider using .parse()"));
688 }
689
690 #[test]
691 fn test_display_with_ariadne_warning() {
692 let diag = TranslatedDiagnostic {
693 message: "unused variable".to_string(),
694 level: DiagnosticLevel::Warning,
695 code: Some("W0001".to_string()),
696 primary_span: Some(TranslatedSpan {
697 pos: SourcePos::repl(1, 6, 1),
698 label: Some("never used".to_string()),
699 source_text: None,
700 }),
701 secondary_spans: vec![],
702 children: vec![],
703 suggestion: None,
704 };
705
706 let source = "(def x 42)";
707 let output = diag.display_with_ariadne(source);
708
709 assert!(output.contains("W0001"));
711 assert!(output.contains("unused variable"));
712 }
713
714 #[test]
715 fn test_calculate_byte_offset() {
716 let diag = TranslatedDiagnostic {
717 message: "test".to_string(),
718 level: DiagnosticLevel::Error,
719 code: None,
720 primary_span: None,
721 secondary_spans: vec![],
722 children: vec![],
723 suggestion: None,
724 };
725
726 let source = "line1\nline2\nline3";
727
728 let pos1 = SourcePos::repl(1, 1, 1);
730 assert_eq!(diag.calculate_byte_offset(source, &pos1), 0);
731
732 let pos2 = SourcePos::repl(2, 1, 1);
734 assert_eq!(diag.calculate_byte_offset(source, &pos2), 6);
735
736 let pos3 = SourcePos::repl(2, 3, 1);
738 assert_eq!(diag.calculate_byte_offset(source, &pos3), 8);
739 }
740
741 #[test]
742 fn test_calculate_byte_offset_unicode() {
743 let diag = TranslatedDiagnostic {
744 message: "test".to_string(),
745 level: DiagnosticLevel::Error,
746 code: None,
747 primary_span: None,
748 secondary_spans: vec![],
749 children: vec![],
750 suggestion: None,
751 };
752
753 let source = "hello 世界\nworld";
755
756 let pos = SourcePos::repl(1, 7, 1);
758 let offset = diag.calculate_byte_offset(source, &pos);
759
760 assert_eq!(offset, 6);
762 }
763
764 #[test]
779 fn test_phase_4_end_to_end_error_translation() {
780 use oxur_smap::{new_node_id, SourceMap, SourcePos};
781
782 let oxur_source = "(deffn add [a b] (+ a b))";
784
785 let mut source_map = SourceMap::new();
787 let deffn_node = new_node_id();
788 source_map.record_surface_node(deffn_node, SourcePos::repl(1, 1, 25));
789
790 let generated_rust = format!(
792 r#"
793// Generated by Oxur REPL
794#[no_mangle]
795pub extern "C" fn oxur_eval_test() {{
796 /* oxur_node={} */
797 fn add(a: i32, b: i32) -> i32 {{
798 a + b
799 }}
800}}
801"#,
802 deffn_node.as_raw()
803 );
804
805 let rustc_json = r#"{
807 "message": "mismatched types",
808 "code": {"code": "E0308", "explanation": "..."},
809 "level": "error",
810 "spans": [{
811 "file_name": "/tmp/repl_test.rs",
812 "byte_start": 120,
813 "byte_end": 121,
814 "line_start": 6,
815 "line_end": 6,
816 "column_start": 8,
817 "column_end": 9,
818 "text": [{"text": " a + b", "highlight_start": 8, "highlight_end": 9}],
819 "label": "expected `String`, found `i32`",
820 "is_primary": true
821 }],
822 "children": []
823 }"#;
824
825 let translator = ErrorTranslator::with_source_map(source_map);
827 let diagnostic: RustcDiagnostic = serde_json::from_str(rustc_json).unwrap();
828
829 let lines: Vec<&str> = generated_rust.lines().collect();
832
833 let error_line_idx = 5; let mut node_id_result = None;
836
837 for i in (0..=error_line_idx).rev() {
838 if let Some(line) = lines.get(i) {
839 match translator.extract_node_id_from_line(line, 8) {
840 Ok(node) => {
841 node_id_result = Some(node);
842 break;
843 }
844 Err(_) => continue,
845 }
846 }
847 }
848
849 assert!(node_id_result.is_some(), "Should extract NodeId from comment in generated code");
850
851 let extracted_node = node_id_result.unwrap();
852 assert_eq!(
853 extracted_node.as_raw(),
854 deffn_node.as_raw(),
855 "Should extract the correct NodeId"
856 );
857
858 let oxur_diagnostic = translator.translate_diagnostic(&diagnostic).unwrap();
860
861 assert_eq!(oxur_diagnostic.level, DiagnosticLevel::Error);
863 assert_eq!(oxur_diagnostic.message, "mismatched types");
864 assert!(oxur_diagnostic.code.is_some());
865 assert_eq!(oxur_diagnostic.code.as_ref().unwrap(), "E0308");
866
867 assert!(oxur_diagnostic.primary_span.is_some());
872 let primary = oxur_diagnostic.primary_span.as_ref().unwrap();
873 assert_eq!(primary.pos.file, "<repl>");
874 assert_eq!(primary.pos.line, 6);
876 assert_eq!(primary.pos.column, 8);
877
878 let ariadne_output = oxur_diagnostic.display_with_ariadne(oxur_source);
880 assert!(
881 ariadne_output.contains("mismatched types"),
882 "Ariadne output should contain error message"
883 );
884 assert!(ariadne_output.contains("Error"), "Ariadne output should show error severity");
885
886 }
890
891 #[test]
896 fn test_error_translation_missing_node_id() {
897 use oxur_smap::{new_node_id, SourceMap, SourcePos};
898
899 let mut source_map = SourceMap::new();
900 let node1 = new_node_id();
901 source_map.record_surface_node(node1, SourcePos::repl(1, 1, 10));
902
903 let node2 = new_node_id();
905 let _generated_rust = format!(
906 r#"
907// Generated code
908#[no_mangle]
909pub extern "C" fn test() {{
910 /* oxur_node={} */
911 let x = 42;
912}}
913"#,
914 node2.as_raw() );
916
917 let rustc_json = r#"{
918 "message": "unused variable",
919 "code": null,
920 "level": "warning",
921 "spans": [{
922 "file_name": "/tmp/test.rs",
923 "byte_start": 100,
924 "byte_end": 101,
925 "line_start": 5,
926 "line_end": 5,
927 "column_start": 9,
928 "column_end": 10,
929 "label": null,
930 "is_primary": true
931 }],
932 "children": []
933 }"#;
934
935 let translator = ErrorTranslator::with_source_map(source_map);
936 let diagnostic: RustcDiagnostic = serde_json::from_str(rustc_json).unwrap();
937
938 let oxur_diagnostic = translator.translate_diagnostic(&diagnostic).unwrap();
940
941 assert_eq!(oxur_diagnostic.level, DiagnosticLevel::Warning);
943 assert_eq!(oxur_diagnostic.message, "unused variable");
944 }
945
946 #[test]
948 fn test_multiple_errors_with_different_nodes() {
949 use oxur_smap::{new_node_id, SourceMap, SourcePos};
950
951 let mut source_map = SourceMap::new();
952
953 let node1 = new_node_id();
955 source_map.record_surface_node(node1, SourcePos::repl(1, 1, 10));
956
957 let node2 = new_node_id();
958 source_map.record_surface_node(node2, SourcePos::repl(2, 1, 15));
959
960 let translator = ErrorTranslator::with_source_map(source_map);
961
962 let _node1_id = node1.as_raw(); let error1_json = r#"{
966 "message": "first error",
967 "code": null,
968 "level": "error",
969 "spans": [{
970 "file_name": "/tmp/test.rs",
971 "byte_start": 80,
972 "byte_end": 81,
973 "line_start": 4,
974 "line_end": 4,
975 "column_start": 5,
976 "column_end": 6,
977 "label": "error here",
978 "is_primary": true
979 }],
980 "children": []
981 }"#;
982
983 let _node2_id = node2.as_raw(); let error2_json = r#"{
986 "message": "second error",
987 "code": null,
988 "level": "error",
989 "spans": [{
990 "file_name": "/tmp/test.rs",
991 "byte_start": 120,
992 "byte_end": 121,
993 "line_start": 5,
994 "line_end": 5,
995 "column_start": 5,
996 "column_end": 6,
997 "label": "error here",
998 "is_primary": true
999 }],
1000 "children": []
1001 }"#;
1002
1003 let diag1: RustcDiagnostic = serde_json::from_str(&error1_json).unwrap();
1004 let diag2: RustcDiagnostic = serde_json::from_str(&error2_json).unwrap();
1005
1006 let oxur_diag1 = translator.translate_diagnostic(&diag1).unwrap();
1007 let oxur_diag2 = translator.translate_diagnostic(&diag2).unwrap();
1008
1009 assert_eq!(oxur_diag1.message, "first error");
1011 assert_eq!(oxur_diag2.message, "second error");
1012
1013 let pos1 = &oxur_diag1.primary_span.as_ref().unwrap().pos;
1015 let pos2 = &oxur_diag2.primary_span.as_ref().unwrap().pos;
1016
1017 assert_eq!(pos1.line, 4);
1019 assert_eq!(pos2.line, 5);
1020
1021 assert_ne!(pos1.line, pos2.line);
1023 }
1024}