1use depyler_hir::error::ErrorKind;
4use anyhow::Result;
5use colored::Colorize;
6use rustpython_ast::Ranged;
7use std::fmt;
8
9#[derive(Debug)]
11pub struct EnhancedError {
12 pub error: ErrorKind,
14 pub file_path: Option<String>,
16 pub line: Option<usize>,
18 pub column: Option<usize>,
20 pub source_line: Option<String>,
22 pub suggestion: Option<String>,
24 pub notes: Vec<String>,
26}
27
28impl EnhancedError {
29 pub fn new(error: ErrorKind) -> Self {
30 Self {
31 error,
32 file_path: None,
33 line: None,
34 column: None,
35 source_line: None,
36 suggestion: None,
37 notes: Vec::new(),
38 }
39 }
40
41 pub fn with_location(mut self, file: &str, line: usize, column: usize) -> Self {
42 self.file_path = Some(file.to_string());
43 self.line = Some(line);
44 self.column = Some(column);
45 self
46 }
47
48 pub fn with_source_line(mut self, line: &str) -> Self {
49 self.source_line = Some(line.to_string());
50 self
51 }
52
53 pub fn with_suggestion(mut self, suggestion: &str) -> Self {
54 self.suggestion = Some(suggestion.to_string());
55 self
56 }
57
58 pub fn add_note(mut self, note: &str) -> Self {
59 self.notes.push(note.to_string());
60 self
61 }
62
63 pub fn from_ast_node<T: Ranged>(error: ErrorKind, node: &T, source: &str) -> Self {
65 let range = node.range();
66 let (line, column) = get_line_column(source, range.start().into());
67
68 let mut enhanced = Self::new(error).with_location("<input>", line, column);
69
70 if let Some(line_text) = get_source_line(source, line) {
72 enhanced = enhanced.with_source_line(&line_text);
73 }
74
75 enhanced = add_automatic_suggestions(enhanced);
77
78 enhanced
79 }
80
81 #[inline]
83 fn format_location_info(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 if let (Some(file), Some(line), Some(column)) = (&self.file_path, self.line, self.column) {
85 writeln!(f, " {} {}:{}:{}", "-->".blue().bold(), file, line, column)?;
86 }
87 Ok(())
88 }
89
90 #[inline]
92 fn format_source_context(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 if let (Some(line_text), Some(line_num), Some(col)) =
94 (&self.source_line, self.line, self.column)
95 {
96 writeln!(f, " {} |", format!("{:4}", " ").dimmed())?;
97 writeln!(
98 f,
99 " {} | {}",
100 format!("{:4}", line_num).blue().bold(),
101 line_text
102 )?;
103 writeln!(
104 f,
105 " {} | {}{}",
106 format!("{:4}", " ").dimmed(),
107 " ".repeat(col.saturating_sub(1)),
108 "^".red().bold()
109 )?;
110 }
111 Ok(())
112 }
113
114 #[inline]
116 fn format_suggestion(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 if let Some(suggestion) = &self.suggestion {
118 writeln!(f, "\n{}: {}", "suggestion".green().bold(), suggestion)?;
119 }
120 Ok(())
121 }
122
123 #[inline]
125 fn format_notes(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 for note in &self.notes {
127 writeln!(f, " {} {}", "note:".yellow(), note)?;
128 }
129 Ok(())
130 }
131}
132
133impl fmt::Display for EnhancedError {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 writeln!(f, "{}: {}", "error".red().bold(), self.error)?;
137
138 self.format_location_info(f)?;
140
141 self.format_source_context(f)?;
143
144 self.format_suggestion(f)?;
146
147 self.format_notes(f)?;
149
150 Ok(())
151 }
152}
153
154fn get_line_column(source: &str, offset: u32) -> (usize, usize) {
156 let mut line = 1;
157 let mut column = 1;
158 let offset = offset as usize;
159
160 for (i, ch) in source.chars().enumerate() {
161 if i >= offset {
162 break;
163 }
164 if ch == '\n' {
165 line += 1;
166 column = 1;
167 } else {
168 column += 1;
169 }
170 }
171
172 (line, column)
173}
174
175fn get_source_line(source: &str, line_num: usize) -> Option<String> {
177 source
178 .lines()
179 .nth(line_num.saturating_sub(1))
180 .map(String::from)
181}
182
183fn add_automatic_suggestions(error: EnhancedError) -> EnhancedError {
185 let suggestion = match &error.error {
186 ErrorKind::TypeMismatch {
187 expected,
188 found,
189 context,
190 } => generate_type_mismatch_suggestion(expected, found, context),
191 ErrorKind::UnsupportedFeature(feature) => suggest_unsupported_feature(feature),
192 ErrorKind::TypeInferenceError(msg) => suggest_type_inference_fix(msg),
193 ErrorKind::InvalidTypeAnnotation(msg) => suggest_annotation_fix(msg),
194 _ => None,
195 };
196
197 apply_suggestion_to_error(error, suggestion)
198}
199
200#[inline]
202fn apply_suggestion_to_error(
203 mut error: EnhancedError,
204 suggestion: Option<(String, Vec<String>)>,
205) -> EnhancedError {
206 if let Some((suggestion_text, notes)) = suggestion {
207 error = error.with_suggestion(&suggestion_text);
208 for note in notes {
209 error = error.add_note(¬e);
210 }
211 }
212 error
213}
214
215#[inline]
217fn suggest_unsupported_feature(feature: &str) -> Option<(String, Vec<String>)> {
218 if feature == "yield" {
220 return Some((
221 "Generator functions are not supported. Consider returning a list instead.".to_string(),
222 vec!["Example: Instead of 'yield x', collect values and 'return values'".to_string()],
223 ));
224 }
225
226 Some((
228 format!(
229 "This Python feature '{feature}' is not yet supported. Consider using a simpler construct."
230 ),
231 vec![],
232 ))
233}
234
235#[inline]
237fn suggest_type_inference_fix(msg: &str) -> Option<(String, Vec<String>)> {
238 if msg.contains("incompatible types") {
239 Some((
240 "Check that all type annotations are correct and consistent.".to_string(),
241 vec!["Rust has stricter type checking than Python".to_string()],
242 ))
243 } else {
244 None
245 }
246}
247
248#[inline]
250fn suggest_annotation_fix(msg: &str) -> Option<(String, Vec<String>)> {
251 if msg.contains("borrow") {
252 Some((
253 "Consider using .clone() or restructuring to avoid multiple borrows.".to_string(),
254 vec!["Rust's borrow checker ensures memory safety".to_string()],
255 ))
256 } else {
257 None
258 }
259}
260
261fn generate_type_mismatch_suggestion(
263 expected: &str,
264 found: &str,
265 context: &str,
266) -> Option<(String, Vec<String>)> {
267 if let Some(suggestion) = suggest_string_mismatch(expected, found) {
269 return Some(suggestion);
270 }
271 if let Some(suggestion) = suggest_division_mismatch(expected, found) {
272 return Some(suggestion);
273 }
274 if let Some(suggestion) = suggest_option_mismatch(expected, found) {
275 return Some(suggestion);
276 }
277 if let Some(suggestion) = suggest_ownership_mismatch(expected, found) {
278 return Some(suggestion);
279 }
280 if let Some(suggestion) = suggest_collection_mismatch(expected, found) {
281 return Some(suggestion);
282 }
283
284 suggest_generic_mismatch(expected, found, context)
286}
287
288#[inline]
290fn suggest_string_mismatch(expected: &str, found: &str) -> Option<(String, Vec<String>)> {
291 if (expected == "String" && found == "&str") || (expected == "str" && found == "String") {
292 Some((
293 "String type mismatch - Python 'str' maps to both Rust '&str' and 'String'".to_string(),
294 vec![
295 "In Rust:".to_string(),
296 " • '&str' is a borrowed string slice (cheap, read-only)".to_string(),
297 " • 'String' is an owned, heap-allocated string".to_string(),
298 "Python string methods (.upper(), .lower(), .strip()) return owned String"
299 .to_string(),
300 "Use '.to_string()' to convert &str → String, or '&s' to convert String → &str"
301 .to_string(),
302 ],
303 ))
304 } else {
305 None
306 }
307}
308
309#[inline]
311fn suggest_division_mismatch(expected: &str, found: &str) -> Option<(String, Vec<String>)> {
312 if expected.contains("f64") && found.contains("i") {
313 Some((
314 "Division result type mismatch - Python '/' always returns float".to_string(),
315 vec![
316 "In Python: 10 / 3 = 3.333... (always float)".to_string(),
317 "In Rust: 10 / 3 = 3 (integer), 10.0 / 3.0 = 3.333... (float)".to_string(),
318 "Use '.as_f64()' or ensure operands are floats for division".to_string(),
319 ],
320 ))
321 } else {
322 None
323 }
324}
325
326#[inline]
328fn suggest_option_mismatch(expected: &str, found: &str) -> Option<(String, Vec<String>)> {
329 if expected.contains("Option") && (found == "None" || found == "()") {
330 Some((
331 "None type mismatch - Python None maps to Rust Option<T>".to_string(),
332 vec![
333 "In Rust, optional values use Option<T>:".to_string(),
334 " • Some(value) for present values".to_string(),
335 " • None for absent values".to_string(),
336 "Return type must be Option<T> if function can return None".to_string(),
337 ],
338 ))
339 } else {
340 None
341 }
342}
343
344#[inline]
346fn suggest_ownership_mismatch(expected: &str, found: &str) -> Option<(String, Vec<String>)> {
347 if expected.starts_with('&') && !found.starts_with('&') {
348 Some((
349 format!("Ownership mismatch - expected borrowed reference '{expected}', found owned value '{found}'"),
350 vec![
351 "Rust distinguishes between owned values and borrowed references".to_string(),
352 format!("Add '&' to borrow the value: '&{found}'"),
353 "Or use .as_ref() to get a reference without moving the value".to_string(),
354 ],
355 ))
356 } else {
357 None
358 }
359}
360
361#[inline]
363fn suggest_collection_mismatch(expected: &str, found: &str) -> Option<(String, Vec<String>)> {
364 if (expected.contains("Vec") && found.contains("list"))
365 || (expected.contains("list") && found.contains("Vec"))
366 {
367 Some((
368 "Collection type mismatch - Python list maps to Rust Vec<T>".to_string(),
369 vec![
370 "Python lists are dynamic arrays, similar to Rust Vec<T>".to_string(),
371 "Ensure element types match: Python list[int] → Rust Vec<i32>".to_string(),
372 ],
373 ))
374 } else {
375 None
376 }
377}
378
379#[inline]
381fn suggest_generic_mismatch(
382 expected: &str,
383 found: &str,
384 context: &str,
385) -> Option<(String, Vec<String>)> {
386 if context.contains("return") {
387 Some((
388 format!(
389 "Return type mismatch in {context}: expected '{expected}', found '{found}'"
390 ),
391 vec![
392 "Check that your function's return type annotation matches what you're actually returning".to_string(),
393 "Python and Rust may have different type representations".to_string(),
394 ],
395 ))
396 } else {
397 Some((
398 format!("Type mismatch in {context}: expected '{expected}', found '{found}'"),
399 vec![
400 "Rust's type system is stricter than Python's".to_string(),
401 "Ensure all type annotations are explicit and consistent".to_string(),
402 ],
403 ))
404 }
405}
406
407pub struct ErrorReporter {
409 errors: Vec<EnhancedError>,
410 source: String,
411 file_path: String,
412}
413
414impl ErrorReporter {
415 pub fn new(source: String, file_path: String) -> Self {
416 Self {
417 errors: Vec::new(),
418 source,
419 file_path,
420 }
421 }
422
423 pub fn report_error(&mut self, error: ErrorKind) {
424 let enhanced = EnhancedError::new(error);
425 self.errors.push(enhanced);
426 }
427
428 pub fn report_error_at<T: Ranged>(&mut self, error: ErrorKind, node: &T) {
429 let enhanced = EnhancedError::from_ast_node(error, node, &self.source).with_location(
430 &self.file_path,
431 0,
432 0,
433 ); self.errors.push(enhanced);
435 }
436
437 pub fn has_errors(&self) -> bool {
438 !self.errors.is_empty()
439 }
440
441 pub fn display_errors(&self) {
442 for (i, error) in self.errors.iter().enumerate() {
443 if i > 0 {
444 println!();
445 }
446 println!("{}", error);
447 }
448
449 if self.errors.len() > 1 {
450 println!(
451 "\n{}: Found {} errors",
452 "summary".red().bold(),
453 self.errors.len()
454 );
455 }
456 }
457
458 pub fn into_result<T>(self, value: T) -> Result<T> {
459 if self.has_errors() {
460 self.display_errors();
461 anyhow::bail!("Transpilation failed with {} errors", self.errors.len())
462 } else {
463 Ok(value)
464 }
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_enhanced_error_display() {
474 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("yield".to_string()))
475 .with_location("test.py", 5, 10)
476 .with_source_line(" yield x")
477 .with_suggestion("Use return with a list instead")
478 .add_note("Generators are not supported");
479
480 let display = format!("{}", error);
481 assert!(display.contains("yield"));
482 assert!(display.contains("test.py:5:10"));
483 assert!(display.contains("yield x"));
484 assert!(display.contains("suggestion"));
485 }
486
487 #[test]
488 fn test_line_column_calculation() {
489 let source = "line1\nline2\nline3";
490 assert_eq!(get_line_column(source, 0), (1, 1));
491 assert_eq!(get_line_column(source, 6), (2, 1));
492 assert_eq!(get_line_column(source, 12), (3, 1));
493 }
494
495 #[test]
496 fn test_automatic_suggestions() {
497 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("yield".to_string()));
498 let enhanced = add_automatic_suggestions(error);
499
500 assert!(enhanced.suggestion.is_some());
501 assert!(enhanced.suggestion.unwrap().contains("Generator"));
502 }
503
504 #[test]
505 fn test_string_type_mismatch_suggestion() {
506 let error = EnhancedError::new(ErrorKind::TypeMismatch {
507 expected: "String".to_string(),
508 found: "&str".to_string(),
509 context: "return type".to_string(),
510 });
511 let enhanced = add_automatic_suggestions(error);
512
513 assert!(enhanced.suggestion.is_some());
514 let suggestion = enhanced.suggestion.unwrap();
515 assert!(suggestion.contains("String type mismatch"));
516 assert!(!enhanced.notes.is_empty());
517 assert!(enhanced.notes.iter().any(|n| n.contains("&str")));
518 }
519
520 #[test]
521 fn test_division_type_mismatch_suggestion() {
522 let error = EnhancedError::new(ErrorKind::TypeMismatch {
523 expected: "f64".to_string(),
524 found: "i32".to_string(),
525 context: "division result".to_string(),
526 });
527 let enhanced = add_automatic_suggestions(error);
528
529 assert!(enhanced.suggestion.is_some());
530 let suggestion = enhanced.suggestion.unwrap();
531 assert!(suggestion.contains("Division result type mismatch"));
532 assert!(enhanced.notes.iter().any(|n| n.contains("always float")));
533 }
534
535 #[test]
536 fn test_option_type_mismatch_suggestion() {
537 let error = EnhancedError::new(ErrorKind::TypeMismatch {
538 expected: "Option<i32>".to_string(),
539 found: "None".to_string(),
540 context: "return value".to_string(),
541 });
542 let enhanced = add_automatic_suggestions(error);
543
544 assert!(enhanced.suggestion.is_some());
545 let suggestion = enhanced.suggestion.unwrap();
546 assert!(suggestion.contains("None type mismatch"));
547 assert!(enhanced.notes.iter().any(|n| n.contains("Option<T>")));
548 }
549
550 #[test]
551 fn test_ownership_mismatch_suggestion() {
552 let error = EnhancedError::new(ErrorKind::TypeMismatch {
553 expected: "&String".to_string(),
554 found: "String".to_string(),
555 context: "parameter".to_string(),
556 });
557 let enhanced = add_automatic_suggestions(error);
558
559 assert!(enhanced.suggestion.is_some());
560 let suggestion = enhanced.suggestion.unwrap();
561 assert!(suggestion.contains("Ownership mismatch"));
562 assert!(enhanced
563 .notes
564 .iter()
565 .any(|n| n.contains("borrowed reference")));
566 }
567
568 #[test]
573 fn test_enhanced_error_new() {
574 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("async".to_string()));
575 assert!(matches!(&error.error, ErrorKind::UnsupportedFeature(f) if f == "async"));
576 assert!(error.file_path.is_none());
577 assert!(error.line.is_none());
578 assert!(error.column.is_none());
579 assert!(error.source_line.is_none());
580 assert!(error.suggestion.is_none());
581 assert!(error.notes.is_empty());
582 }
583
584 #[test]
585 fn test_enhanced_error_with_location() {
586 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()))
587 .with_location("file.py", 10, 5);
588 assert_eq!(error.file_path, Some("file.py".to_string()));
589 assert_eq!(error.line, Some(10));
590 assert_eq!(error.column, Some(5));
591 }
592
593 #[test]
594 fn test_enhanced_error_with_source_line() {
595 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()))
596 .with_source_line("x = 10");
597 assert_eq!(error.source_line, Some("x = 10".to_string()));
598 }
599
600 #[test]
601 fn test_enhanced_error_with_suggestion() {
602 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()))
603 .with_suggestion("Try this instead");
604 assert_eq!(error.suggestion, Some("Try this instead".to_string()));
605 }
606
607 #[test]
608 fn test_enhanced_error_add_note() {
609 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()))
610 .add_note("Note 1")
611 .add_note("Note 2");
612 assert_eq!(error.notes.len(), 2);
613 assert_eq!(error.notes[0], "Note 1");
614 assert_eq!(error.notes[1], "Note 2");
615 }
616
617 #[test]
618 fn test_get_source_line_valid() {
619 let source = "line1\nline2\nline3";
620 assert_eq!(get_source_line(source, 1), Some("line1".to_string()));
621 assert_eq!(get_source_line(source, 2), Some("line2".to_string()));
622 assert_eq!(get_source_line(source, 3), Some("line3".to_string()));
623 }
624
625 #[test]
626 fn test_get_source_line_invalid() {
627 let source = "line1\nline2";
628 assert!(get_source_line(source, 10).is_none());
629 assert!(get_source_line(source, 0).is_some()); }
631
632 #[test]
633 fn test_get_line_column_start_of_lines() {
634 let source = "ab\ncde\nfgh";
635 assert_eq!(get_line_column(source, 0), (1, 1));
637 assert_eq!(get_line_column(source, 3), (2, 1));
639 assert_eq!(get_line_column(source, 7), (3, 1));
641 }
642
643 #[test]
644 fn test_get_line_column_middle_of_line() {
645 let source = "hello world";
646 assert_eq!(get_line_column(source, 0), (1, 1));
647 assert_eq!(get_line_column(source, 5), (1, 6)); assert_eq!(get_line_column(source, 10), (1, 11));
649 }
650
651 #[test]
652 fn test_get_line_column_end_of_source() {
653 let source = "abc";
654 assert_eq!(get_line_column(source, 100), (1, 4)); }
656
657 #[test]
658 fn test_suggest_unsupported_feature_yield() {
659 let result = suggest_unsupported_feature("yield");
660 assert!(result.is_some());
661 let (suggestion, notes) = result.unwrap();
662 assert!(suggestion.contains("Generator"));
663 assert!(!notes.is_empty());
664 }
665
666 #[test]
667 fn test_suggest_unsupported_feature_other() {
668 let result = suggest_unsupported_feature("async");
669 assert!(result.is_some());
670 let (suggestion, _) = result.unwrap();
671 assert!(suggestion.contains("async"));
672 assert!(suggestion.contains("not yet supported"));
673 }
674
675 #[test]
676 fn test_suggest_type_inference_fix_incompatible() {
677 let result = suggest_type_inference_fix("incompatible types in expression");
678 assert!(result.is_some());
679 let (suggestion, notes) = result.unwrap();
680 assert!(suggestion.contains("type annotations"));
681 assert!(notes.iter().any(|n| n.contains("Rust")));
682 }
683
684 #[test]
685 fn test_suggest_type_inference_fix_other() {
686 let result = suggest_type_inference_fix("unknown type");
687 assert!(result.is_none());
688 }
689
690 #[test]
691 fn test_suggest_annotation_fix_borrow() {
692 let result = suggest_annotation_fix("cannot borrow twice");
693 assert!(result.is_some());
694 let (suggestion, notes) = result.unwrap();
695 assert!(suggestion.contains(".clone()"));
696 assert!(notes.iter().any(|n| n.contains("borrow checker")));
697 }
698
699 #[test]
700 fn test_suggest_annotation_fix_other() {
701 let result = suggest_annotation_fix("unknown annotation issue");
702 assert!(result.is_none());
703 }
704
705 #[test]
706 fn test_suggest_collection_mismatch_vec_list() {
707 let result = suggest_collection_mismatch("Vec<i32>", "list[int]");
708 assert!(result.is_some());
709 let (suggestion, notes) = result.unwrap();
710 assert!(suggestion.contains("Collection type mismatch"));
711 assert!(notes.iter().any(|n| n.contains("dynamic arrays")));
712 }
713
714 #[test]
715 fn test_suggest_collection_mismatch_list_vec() {
716 let result = suggest_collection_mismatch("list[str]", "Vec<String>");
717 assert!(result.is_some());
718 }
719
720 #[test]
721 fn test_suggest_collection_mismatch_no_match() {
722 let result = suggest_collection_mismatch("int", "float");
723 assert!(result.is_none());
724 }
725
726 #[test]
727 fn test_suggest_generic_mismatch_return() {
728 let result = suggest_generic_mismatch("int", "str", "return statement");
729 assert!(result.is_some());
730 let (suggestion, notes) = result.unwrap();
731 assert!(suggestion.contains("Return type mismatch"));
732 assert!(notes.iter().any(|n| n.contains("return type annotation")));
733 }
734
735 #[test]
736 fn test_suggest_generic_mismatch_non_return() {
737 let result = suggest_generic_mismatch("int", "str", "assignment");
738 assert!(result.is_some());
739 let (suggestion, notes) = result.unwrap();
740 assert!(suggestion.contains("Type mismatch in assignment"));
741 assert!(notes.iter().any(|n| n.contains("stricter")));
742 }
743
744 #[test]
745 fn test_suggest_string_mismatch_str_to_string() {
746 let result = suggest_string_mismatch("str", "String");
747 assert!(result.is_some());
748 let (suggestion, notes) = result.unwrap();
749 assert!(suggestion.contains("String type mismatch"));
750 assert!(notes.iter().any(|n| n.contains(".to_string()")));
751 }
752
753 #[test]
754 fn test_suggest_string_mismatch_no_match() {
755 let result = suggest_string_mismatch("int", "float");
756 assert!(result.is_none());
757 }
758
759 #[test]
760 fn test_suggest_division_mismatch_no_match() {
761 let result = suggest_division_mismatch("String", "i32");
762 assert!(result.is_none());
763 }
764
765 #[test]
766 fn test_suggest_option_mismatch_with_unit() {
767 let result = suggest_option_mismatch("Option<String>", "()");
768 assert!(result.is_some());
769 let (suggestion, _) = result.unwrap();
770 assert!(suggestion.contains("None type mismatch"));
771 }
772
773 #[test]
774 fn test_suggest_option_mismatch_no_match() {
775 let result = suggest_option_mismatch("String", "int");
776 assert!(result.is_none());
777 }
778
779 #[test]
780 fn test_suggest_ownership_mismatch_no_match() {
781 let result = suggest_ownership_mismatch("String", "String");
782 assert!(result.is_none());
783 }
784
785 #[test]
786 fn test_apply_suggestion_to_error_with_suggestion() {
787 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()));
788 let suggestion = Some((
789 "Use this".to_string(),
790 vec!["Note 1".to_string(), "Note 2".to_string()],
791 ));
792 let result = apply_suggestion_to_error(error, suggestion);
793 assert_eq!(result.suggestion, Some("Use this".to_string()));
794 assert_eq!(result.notes.len(), 2);
795 }
796
797 #[test]
798 fn test_apply_suggestion_to_error_without_suggestion() {
799 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()));
800 let result = apply_suggestion_to_error(error, None);
801 assert!(result.suggestion.is_none());
802 assert!(result.notes.is_empty());
803 }
804
805 #[test]
806 fn test_error_reporter_new() {
807 let reporter = ErrorReporter::new("source code".to_string(), "test.py".to_string());
808 assert!(!reporter.has_errors());
809 }
810
811 #[test]
812 fn test_error_reporter_report_error() {
813 let mut reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
814 reporter.report_error(ErrorKind::UnsupportedFeature("async".to_string()));
815 assert!(reporter.has_errors());
816 }
817
818 #[test]
819 fn test_error_reporter_has_errors_false() {
820 let reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
821 assert!(!reporter.has_errors());
822 }
823
824 #[test]
825 fn test_error_reporter_has_errors_true() {
826 let mut reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
827 reporter.report_error(ErrorKind::UnsupportedFeature("test".to_string()));
828 assert!(reporter.has_errors());
829 }
830
831 #[test]
832 fn test_error_reporter_into_result_success() {
833 let reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
834 let result = reporter.into_result(42);
835 assert!(result.is_ok());
836 assert_eq!(result.unwrap(), 42);
837 }
838
839 #[test]
840 fn test_error_reporter_into_result_failure() {
841 let mut reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
842 reporter.report_error(ErrorKind::UnsupportedFeature("test".to_string()));
843 let result = reporter.into_result::<i32>(42);
844 assert!(result.is_err());
845 }
846
847 #[test]
848 fn test_enhanced_error_display_no_location() {
849 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()));
850 let display = format!("{}", error);
851 assert!(display.contains("Unsupported Python feature"));
853 assert!(!display.contains("-->"));
855 }
856
857 #[test]
858 fn test_enhanced_error_display_with_notes() {
859 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()))
860 .add_note("First note")
861 .add_note("Second note");
862 let display = format!("{}", error);
863 assert!(display.contains("First note"));
864 assert!(display.contains("Second note"));
865 }
866
867 #[test]
868 fn test_add_automatic_suggestions_type_inference() {
869 let error = EnhancedError::new(ErrorKind::TypeInferenceError(
870 "incompatible types".to_string(),
871 ));
872 let enhanced = add_automatic_suggestions(error);
873 assert!(enhanced.suggestion.is_some());
874 }
875
876 #[test]
877 fn test_add_automatic_suggestions_invalid_annotation() {
878 let error = EnhancedError::new(ErrorKind::InvalidTypeAnnotation(
879 "cannot borrow".to_string(),
880 ));
881 let enhanced = add_automatic_suggestions(error);
882 assert!(enhanced.suggestion.is_some());
883 }
884
885 #[test]
886 fn test_add_automatic_suggestions_other_error() {
887 let error = EnhancedError::new(ErrorKind::ParseError);
889 let enhanced = add_automatic_suggestions(error);
890 assert!(enhanced.suggestion.is_none());
892 }
893
894 #[test]
895 fn test_generate_type_mismatch_suggestion_string() {
896 let result = generate_type_mismatch_suggestion("String", "&str", "return type");
897 assert!(result.is_some());
898 }
899
900 #[test]
901 fn test_generate_type_mismatch_suggestion_division() {
902 let result = generate_type_mismatch_suggestion("f64", "i32", "division");
903 assert!(result.is_some());
904 }
905
906 #[test]
907 fn test_generate_type_mismatch_suggestion_option() {
908 let result = generate_type_mismatch_suggestion("Option<i32>", "None", "return");
909 assert!(result.is_some());
910 }
911
912 #[test]
913 fn test_generate_type_mismatch_suggestion_ownership() {
914 let result = generate_type_mismatch_suggestion("&str", "String", "parameter");
915 assert!(result.is_some());
916 }
917
918 #[test]
919 fn test_generate_type_mismatch_suggestion_collection() {
920 let result = generate_type_mismatch_suggestion("Vec<i32>", "list[int]", "assignment");
921 assert!(result.is_some());
922 }
923
924 #[test]
925 fn test_generate_type_mismatch_suggestion_fallback() {
926 let result = generate_type_mismatch_suggestion("CustomType", "OtherType", "expression");
927 assert!(result.is_some());
928 let (suggestion, _) = result.unwrap();
929 assert!(suggestion.contains("Type mismatch"));
930 }
931
932 #[test]
933 fn test_enhanced_error_chaining() {
934 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()))
935 .with_location("file.py", 1, 1)
936 .with_source_line("test code")
937 .with_suggestion("Try this")
938 .add_note("Note 1")
939 .add_note("Note 2");
940
941 assert_eq!(error.file_path, Some("file.py".to_string()));
942 assert_eq!(error.line, Some(1));
943 assert_eq!(error.column, Some(1));
944 assert_eq!(error.source_line, Some("test code".to_string()));
945 assert_eq!(error.suggestion, Some("Try this".to_string()));
946 assert_eq!(error.notes.len(), 2);
947 }
948
949 #[test]
950 fn test_enhanced_error_display_full() {
951 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("test".to_string()))
952 .with_location("file.py", 5, 10)
953 .with_source_line(" test_code()")
954 .with_suggestion("Use something else")
955 .add_note("Important note");
956
957 let display = format!("{}", error);
958 assert!(display.contains("error"));
959 assert!(display.contains("file.py:5:10"));
960 assert!(display.contains("test_code()"));
961 assert!(display.contains("suggestion"));
962 assert!(display.contains("note:"));
963 }
964
965 #[test]
966 fn test_get_line_column_empty_source() {
967 let source = "";
968 assert_eq!(get_line_column(source, 0), (1, 1));
969 }
970
971 #[test]
972 fn test_get_source_line_empty_source() {
973 let source = "";
974 assert!(get_source_line(source, 1).is_none());
975 }
976
977 #[test]
978 fn test_get_source_line_single_line() {
979 let source = "only one line";
980 assert_eq!(
981 get_source_line(source, 1),
982 Some("only one line".to_string())
983 );
984 assert!(get_source_line(source, 2).is_none());
985 }
986
987 #[test]
988 fn test_error_reporter_display_errors_single() {
989 let mut reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
990 reporter.report_error(ErrorKind::UnsupportedFeature("async".to_string()));
991 reporter.display_errors();
992 assert!(reporter.has_errors());
993 }
994
995 #[test]
996 fn test_error_reporter_display_errors_multiple() {
997 let mut reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
998 reporter.report_error(ErrorKind::UnsupportedFeature("async".to_string()));
999 reporter.report_error(ErrorKind::ParseError);
1000 reporter.report_error(ErrorKind::TypeInferenceError("test".to_string()));
1001 reporter.display_errors();
1002 assert_eq!(reporter.errors.len(), 3);
1003 }
1004
1005 #[test]
1006 fn test_error_reporter_display_errors_empty() {
1007 let reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
1008 reporter.display_errors();
1009 assert!(!reporter.has_errors());
1010 }
1011
1012 #[test]
1013 fn test_enhanced_error_format_location_info_complete() {
1014 let error = EnhancedError::new(ErrorKind::ParseError)
1015 .with_location("test.py", 10, 20);
1016 let display = format!("{}", error);
1017 assert!(display.contains("-->"));
1018 assert!(display.contains("test.py:10:20"));
1019 }
1020
1021 #[test]
1022 fn test_enhanced_error_format_location_info_incomplete() {
1023 let mut error = EnhancedError::new(ErrorKind::ParseError);
1024 error.file_path = Some("test.py".to_string());
1025 error.line = Some(10);
1026 let display = format!("{}", error);
1027 assert!(!display.contains("-->"));
1028 }
1029
1030 #[test]
1031 fn test_enhanced_error_format_source_context_complete() {
1032 let error = EnhancedError::new(ErrorKind::ParseError)
1033 .with_location("test.py", 5, 10)
1034 .with_source_line(" x = 10");
1035 let display = format!("{}", error);
1036 assert!(display.contains("x = 10"));
1037 assert!(display.contains("|"));
1038 }
1039
1040 #[test]
1041 fn test_enhanced_error_format_source_context_incomplete() {
1042 let mut error = EnhancedError::new(ErrorKind::ParseError);
1043 error.source_line = Some("x = 10".to_string());
1044 error.line = Some(5);
1045 let display = format!("{}", error);
1046 assert!(!display.contains("|"));
1047 }
1048
1049 #[test]
1050 fn test_enhanced_error_format_suggestion_present() {
1051 let error = EnhancedError::new(ErrorKind::ParseError)
1052 .with_suggestion("Try using a different syntax");
1053 let display = format!("{}", error);
1054 assert!(display.contains("suggestion"));
1055 assert!(display.contains("Try using a different syntax"));
1056 }
1057
1058 #[test]
1059 fn test_enhanced_error_format_suggestion_absent() {
1060 let error = EnhancedError::new(ErrorKind::ParseError);
1061 let display = format!("{}", error);
1062 assert!(!display.contains("suggestion"));
1063 }
1064
1065 #[test]
1066 fn test_enhanced_error_format_notes_present() {
1067 let error = EnhancedError::new(ErrorKind::ParseError)
1068 .add_note("First note")
1069 .add_note("Second note");
1070 let display = format!("{}", error);
1071 assert!(display.contains("note:"));
1072 assert!(display.contains("First note"));
1073 assert!(display.contains("Second note"));
1074 }
1075
1076 #[test]
1077 fn test_enhanced_error_format_notes_absent() {
1078 let error = EnhancedError::new(ErrorKind::ParseError);
1079 let display = format!("{}", error);
1080 let note_count = display.matches("note:").count();
1081 assert_eq!(note_count, 0);
1082 }
1083
1084 #[test]
1085 fn test_generate_type_mismatch_suggestion_multiple_patterns() {
1086 let result1 = generate_type_mismatch_suggestion("String", "&str", "context");
1087 assert!(result1.is_some());
1088
1089 let result2 = generate_type_mismatch_suggestion("f64", "i32", "context");
1090 assert!(result2.is_some());
1091
1092 let result3 = generate_type_mismatch_suggestion("Option<T>", "None", "context");
1093 assert!(result3.is_some());
1094
1095 let result4 = generate_type_mismatch_suggestion("&String", "String", "context");
1096 assert!(result4.is_some());
1097
1098 let result5 = generate_type_mismatch_suggestion("Vec<i32>", "list", "context");
1099 assert!(result5.is_some());
1100 }
1101
1102 #[test]
1103 fn test_suggest_division_mismatch_with_various_int_types() {
1104 assert!(suggest_division_mismatch("f64", "i8").is_some());
1105 assert!(suggest_division_mismatch("f64", "i16").is_some());
1106 assert!(suggest_division_mismatch("f64", "i32").is_some());
1107 assert!(suggest_division_mismatch("f64", "i64").is_some());
1108 assert!(suggest_division_mismatch("f32", "i32").is_none());
1109 }
1110
1111 #[test]
1112 fn test_get_line_column_multiline_with_various_offsets() {
1113 let source = "abc\ndef\nghi";
1114 assert_eq!(get_line_column(source, 0), (1, 1));
1115 assert_eq!(get_line_column(source, 1), (1, 2));
1116 assert_eq!(get_line_column(source, 2), (1, 3));
1117 assert_eq!(get_line_column(source, 3), (1, 4));
1118 assert_eq!(get_line_column(source, 4), (2, 1));
1119 assert_eq!(get_line_column(source, 5), (2, 2));
1120 }
1121
1122 #[test]
1123 fn test_get_source_line_with_multiple_lines() {
1124 let source = "first\nsecond\nthird\nfourth";
1125 assert_eq!(get_source_line(source, 1), Some("first".to_string()));
1126 assert_eq!(get_source_line(source, 2), Some("second".to_string()));
1127 assert_eq!(get_source_line(source, 3), Some("third".to_string()));
1128 assert_eq!(get_source_line(source, 4), Some("fourth".to_string()));
1129 }
1130
1131 #[test]
1132 fn test_enhanced_error_multiple_notes() {
1133 let error = EnhancedError::new(ErrorKind::ParseError)
1134 .add_note("Note 1")
1135 .add_note("Note 2")
1136 .add_note("Note 3");
1137 assert_eq!(error.notes.len(), 3);
1138 let display = format!("{}", error);
1139 assert!(display.contains("Note 1"));
1140 assert!(display.contains("Note 2"));
1141 assert!(display.contains("Note 3"));
1142 }
1143
1144 #[test]
1145 fn test_error_reporter_multiple_operations() {
1146 let mut reporter = ErrorReporter::new("code\nmore code".to_string(), "file.py".to_string());
1147 assert!(!reporter.has_errors());
1148
1149 reporter.report_error(ErrorKind::ParseError);
1150 assert!(reporter.has_errors());
1151
1152 reporter.report_error(ErrorKind::UnsupportedFeature("test".to_string()));
1153 assert_eq!(reporter.errors.len(), 2);
1154 }
1155
1156 #[test]
1157 fn test_add_automatic_suggestions_type_mismatch() {
1158 let error = EnhancedError::new(ErrorKind::TypeMismatch {
1159 expected: "String".to_string(),
1160 found: "&str".to_string(),
1161 context: "test".to_string(),
1162 });
1163 let enhanced = add_automatic_suggestions(error);
1164 assert!(enhanced.suggestion.is_some());
1165 assert!(!enhanced.notes.is_empty());
1166 }
1167
1168 #[test]
1169 fn test_add_automatic_suggestions_unsupported_feature_non_yield() {
1170 let error = EnhancedError::new(ErrorKind::UnsupportedFeature("walrus".to_string()));
1171 let enhanced = add_automatic_suggestions(error);
1172 assert!(enhanced.suggestion.is_some());
1173 assert!(enhanced.suggestion.unwrap().contains("walrus"));
1174 }
1175
1176 #[test]
1177 fn test_enhanced_error_display_with_column_edge_cases() {
1178 let error = EnhancedError::new(ErrorKind::ParseError)
1179 .with_location("test.py", 1, 1)
1180 .with_source_line("x");
1181 let display = format!("{}", error);
1182 assert!(display.contains("^"));
1183 }
1184
1185 #[test]
1186 fn test_enhanced_error_display_with_large_column() {
1187 let error = EnhancedError::new(ErrorKind::ParseError)
1188 .with_location("test.py", 1, 100)
1189 .with_source_line("short line");
1190 let display = format!("{}", error);
1191 assert!(display.contains("^"));
1192 }
1193
1194 #[test]
1195 fn test_get_line_column_unicode() {
1196 let source = "hello\n世界\ntest";
1197 assert_eq!(get_line_column(source, 0), (1, 1));
1198 assert_eq!(get_line_column(source, 6), (2, 1));
1199 assert_eq!(get_line_column(source, 7), (2, 2));
1200 }
1201
1202 #[test]
1203 fn test_suggest_ownership_mismatch_with_ampersand() {
1204 let result = suggest_ownership_mismatch("&Vec<i32>", "Vec<i32>");
1205 assert!(result.is_some());
1206 let (suggestion, notes) = result.unwrap();
1207 assert!(suggestion.contains("Ownership mismatch"));
1208 assert!(notes.iter().any(|n| n.contains("borrowed")));
1209 }
1210
1211 #[test]
1212 fn test_suggest_generic_mismatch_with_return_context() {
1213 let result = suggest_generic_mismatch("A", "B", "return value");
1214 assert!(result.is_some());
1215 let (suggestion, _) = result.unwrap();
1216 assert!(suggestion.contains("Return type mismatch"));
1217 }
1218
1219 #[test]
1220 fn test_error_reporter_into_result_with_value() {
1221 let reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
1222 let result = reporter.into_result("success");
1223 assert!(result.is_ok());
1224 assert_eq!(result.unwrap(), "success");
1225 }
1226
1227 #[test]
1228 fn test_error_reporter_into_result_with_errors() {
1229 let mut reporter = ErrorReporter::new("source".to_string(), "test.py".to_string());
1230 reporter.report_error(ErrorKind::ParseError);
1231 let result = reporter.into_result::<&str>("value");
1232 assert!(result.is_err());
1233 assert!(result.unwrap_err().to_string().contains("1 error"));
1234 }
1235
1236 #[test]
1237 fn test_enhanced_error_debug_output() {
1238 let error = EnhancedError::new(ErrorKind::ParseError)
1239 .with_location("test.py", 1, 1);
1240 let debug = format!("{:?}", error);
1241 assert!(debug.contains("EnhancedError"));
1242 }
1243
1244 #[test]
1245 fn test_suggest_annotation_fix_with_borrow_keyword() {
1246 let result = suggest_annotation_fix("cannot borrow as mutable");
1247 assert!(result.is_some());
1248 let (suggestion, _) = result.unwrap();
1249 assert!(suggestion.contains(".clone()"));
1250 }
1251
1252 #[test]
1253 fn test_suggest_type_inference_fix_no_match() {
1254 let result = suggest_type_inference_fix("some other error");
1255 assert!(result.is_none());
1256 }
1257
1258 #[test]
1259 fn test_enhanced_error_all_fields_populated() {
1260 let error = EnhancedError::new(ErrorKind::TypeMismatch {
1261 expected: "i32".to_string(),
1262 found: "str".to_string(),
1263 context: "assignment".to_string(),
1264 })
1265 .with_location("file.py", 42, 17)
1266 .with_source_line(" result = \"text\"")
1267 .with_suggestion("Convert to the correct type")
1268 .add_note("First note")
1269 .add_note("Second note");
1270
1271 assert!(error.file_path.is_some());
1272 assert!(error.line.is_some());
1273 assert!(error.column.is_some());
1274 assert!(error.source_line.is_some());
1275 assert!(error.suggestion.is_some());
1276 assert_eq!(error.notes.len(), 2);
1277 }
1278
1279 #[test]
1280 fn test_error_reporter_with_empty_source() {
1281 let reporter = ErrorReporter::new(String::new(), "empty.py".to_string());
1282 assert!(!reporter.has_errors());
1283 assert_eq!(reporter.source, "");
1284 }
1285
1286 #[test]
1287 fn test_get_source_line_zero_line() {
1288 let source = "line1\nline2\nline3";
1289 let result = get_source_line(source, 0);
1290 assert!(result.is_some());
1291 }
1292
1293 #[test]
1294 fn test_enhanced_error_partial_fields() {
1295 let mut error = EnhancedError::new(ErrorKind::ParseError);
1296 error.file_path = Some("test.py".to_string());
1297 error.column = Some(5);
1298 let display = format!("{}", error);
1299 assert!(!display.contains("-->"));
1300 }
1301
1302 #[test]
1303 fn test_from_ast_node_with_simple_source() {
1304 use rustpython_ast::Suite;
1305 use rustpython_parser::Parse;
1306
1307 let source = "x = 1\ny = 2\nz = 3";
1308 let parsed = Suite::parse(source, "<test>").unwrap();
1309
1310 if let Some(stmt) = parsed.first() {
1311 let error = EnhancedError::from_ast_node(
1312 ErrorKind::UnsupportedFeature("test".to_string()),
1313 stmt,
1314 source,
1315 );
1316
1317 assert_eq!(error.file_path, Some("<input>".to_string()));
1318 assert!(error.line.is_some());
1319 assert!(error.column.is_some());
1320 assert!(error.source_line.is_some());
1321 }
1322 }
1323
1324 #[test]
1325 fn test_from_ast_node_with_multiline_source() {
1326 use rustpython_ast::Suite;
1327 use rustpython_parser::Parse;
1328
1329 let source = "def foo():\n pass\n\nclass Bar:\n pass";
1330 let parsed = Suite::parse(source, "<test>").unwrap();
1331
1332 if let Some(stmt) = parsed.get(1) {
1333 let error = EnhancedError::from_ast_node(
1334 ErrorKind::TypeMismatch {
1335 expected: "int".to_string(),
1336 found: "str".to_string(),
1337 context: "test".to_string(),
1338 },
1339 stmt,
1340 source,
1341 );
1342
1343 assert!(error.line.is_some());
1344 assert!(error.column.is_some());
1345 }
1346 }
1347
1348 #[test]
1349 fn test_report_error_at_with_ast_node() {
1350 use rustpython_ast::Suite;
1351 use rustpython_parser::Parse;
1352
1353 let source = "x = 1 + 2";
1354 let parsed = Suite::parse(source, "<test>").unwrap();
1355
1356 let mut reporter = ErrorReporter::new(source.to_string(), "test.py".to_string());
1357
1358 if let Some(stmt) = parsed.first() {
1359 reporter.report_error_at(ErrorKind::UnsupportedFeature("addition".to_string()), stmt);
1360 assert!(reporter.has_errors());
1361 assert_eq!(reporter.errors.len(), 1);
1362 }
1363 }
1364
1365 #[test]
1366 fn test_error_reporter_multiple_report_error_at() {
1367 use rustpython_ast::Suite;
1368 use rustpython_parser::Parse;
1369
1370 let source = "x = 1\ny = 2\nz = 3";
1371 let parsed = Suite::parse(source, "<test>").unwrap();
1372
1373 let mut reporter = ErrorReporter::new(source.to_string(), "multi.py".to_string());
1374
1375 for stmt in &parsed {
1376 reporter.report_error_at(ErrorKind::ParseError, stmt);
1377 }
1378
1379 assert_eq!(reporter.errors.len(), 3);
1380 }
1381
1382 #[test]
1383 fn test_enhanced_error_column_saturation() {
1384 let error = EnhancedError::new(ErrorKind::ParseError)
1385 .with_location("test.py", 1, 0)
1386 .with_source_line("test");
1387 let display = format!("{}", error);
1388 assert!(display.contains("^"));
1389 }
1390
1391 #[test]
1392 fn test_get_line_column_at_newline() {
1393 let source = "line1\nline2";
1394 assert_eq!(get_line_column(source, 5), (1, 6));
1395 }
1396
1397 #[test]
1398 fn test_error_kinds_with_automatic_suggestions() {
1399 let error1 = EnhancedError::new(ErrorKind::CodeGenerationError("test".to_string()));
1400 let enhanced1 = add_automatic_suggestions(error1);
1401 assert!(enhanced1.suggestion.is_none());
1402
1403 let error2 = EnhancedError::new(ErrorKind::VerificationError("test".to_string()));
1404 let enhanced2 = add_automatic_suggestions(error2);
1405 assert!(enhanced2.suggestion.is_none());
1406 }
1407
1408 #[test]
1409 fn test_enhanced_error_display_minimal() {
1410 let error = EnhancedError::new(ErrorKind::InternalError("minimal".to_string()));
1411 let display = format!("{}", error);
1412 assert!(display.contains("error"));
1413 assert!(display.contains("Internal error"));
1414 }
1415
1416 #[test]
1417 fn test_format_methods_edge_cases() {
1418 let mut error = EnhancedError::new(ErrorKind::ParseError);
1419 error.file_path = Some("test.py".to_string());
1420 error.line = None;
1421 error.column = Some(10);
1422 let display = format!("{}", error);
1423 assert!(!display.contains("-->"));
1424
1425 error.line = Some(5);
1426 error.column = None;
1427 let display2 = format!("{}", error);
1428 assert!(!display2.contains("-->"));
1429 }
1430}