1#[cfg(feature = "decision-tracing")]
7use depyler_hir::decision_trace::DecisionCategory;
8use depyler_hir::hir::{AssignTarget, HirExpr, HirFunction, HirStmt, Type as PythonType};
9use depyler_hir::trace_decision;
10use crate::type_mapper::{RustType, TypeMapper};
11use indexmap::IndexMap;
12use std::collections::{HashMap, HashSet};
13
14#[derive(Debug)]
16pub struct BorrowingContext {
17 param_usage: HashMap<String, ParameterUsagePattern>,
19 moved_vars: HashSet<String>,
21 mut_borrowed_vars: HashSet<String>,
23 immut_borrowed_vars: HashSet<String>,
25 context_stack: Vec<AnalysisContext>,
27 return_type: Option<PythonType>,
29}
30
31#[derive(Debug, Clone, Default)]
33pub struct ParameterUsagePattern {
34 pub is_read: bool,
36 pub is_mutated: bool,
38 pub is_moved: bool,
40 pub escapes_through_return: bool,
42 pub is_stored: bool,
44 pub used_in_closure: bool,
46 pub used_in_loop: bool,
48 pub field_accesses: HashSet<String>,
50 pub method_calls: HashSet<String>,
52 pub usage_sites: Vec<UsageSite>,
54}
55
56#[derive(Debug, Clone)]
58pub struct UsageSite {
59 pub usage_type: UsageType,
61 pub in_loop: bool,
63 pub in_conditional: bool,
65 pub borrow_depth: usize,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum UsageType {
72 Read,
74 Write,
76 MethodCall(String),
78 FunctionArg { takes_ownership: bool },
80 Return,
82 Store,
84 Closure { captures_by_value: bool },
86 FieldAccess(String),
88 IndexAccess,
90}
91
92#[derive(Debug, Clone)]
94#[allow(dead_code)]
95enum AnalysisContext {
96 Loop,
97 Conditional,
98 Closure { captures: HashSet<String> },
99 Function,
100}
101
102#[derive(Debug, Clone)]
104pub struct BorrowingAnalysisResult {
105 pub param_strategies: IndexMap<String, BorrowingStrategy>,
107 pub insights: Vec<BorrowingInsight>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum BorrowingStrategy {
114 TakeOwnership,
116 BorrowImmutable { lifetime: Option<String> },
118 BorrowMutable { lifetime: Option<String> },
120 UseCow { lifetime: String },
122 UseSharedOwnership { is_thread_safe: bool },
124}
125
126#[derive(Debug, Clone)]
128pub enum BorrowingInsight {
129 UnnecessaryMove(String),
131 LifetimeOptimization { param: String, suggestion: String },
133 SuggestCopyDerive(String),
135 PotentialBorrowConflict {
137 param: String,
138 locations: Vec<String>,
139 },
140}
141
142impl BorrowingContext {
143 pub fn new(return_type: Option<PythonType>) -> Self {
144 Self {
145 param_usage: HashMap::new(),
146 moved_vars: HashSet::new(),
147 mut_borrowed_vars: HashSet::new(),
148 immut_borrowed_vars: HashSet::new(),
149 context_stack: vec![AnalysisContext::Function],
150 return_type,
151 }
152 }
153
154 pub fn analyze_function(
156 &mut self,
157 func: &HirFunction,
158 type_mapper: &TypeMapper,
159 ) -> BorrowingAnalysisResult {
160 for param in &func.params {
162 self.param_usage
163 .insert(param.name.clone(), ParameterUsagePattern::default());
164 }
165
166 for stmt in &func.body {
168 self.analyze_statement(stmt);
169 }
170
171 self.determine_strategies(func, type_mapper)
173 }
174
175 fn analyze_statement(&mut self, stmt: &HirStmt) {
177 match stmt {
178 HirStmt::Assign { target, value, .. } => {
179 let in_loop = self.is_in_loop();
182 let in_conditional = self.is_in_conditional();
183 self.check_target_mutation(target, in_loop, in_conditional);
184 self.analyze_expression(value, 0);
185 }
186 HirStmt::Return(expr) => {
187 if let Some(e) = expr {
188 self.analyze_expression_for_return(e);
189 }
190 }
191 HirStmt::If {
192 condition,
193 then_body,
194 else_body,
195 } => {
196 self.analyze_expression(condition, 0);
197 self.context_stack.push(AnalysisContext::Conditional);
198 for stmt in then_body {
199 self.analyze_statement(stmt);
200 }
201 if let Some(else_stmts) = else_body {
202 for stmt in else_stmts {
203 self.analyze_statement(stmt);
204 }
205 }
206 self.context_stack.pop();
207 }
208 HirStmt::While { condition, body } => {
209 self.context_stack.push(AnalysisContext::Loop);
210 self.analyze_expression(condition, 0);
211 for stmt in body {
212 self.analyze_statement(stmt);
213 }
214 self.context_stack.pop();
215 }
216 HirStmt::For {
217 target: _,
218 iter,
219 body,
220 } => {
221 self.context_stack.push(AnalysisContext::Loop);
222 self.analyze_expression(iter, 0);
223 for stmt in body {
224 self.analyze_statement(stmt);
225 }
226 self.context_stack.pop();
227 }
228 HirStmt::Expr(expr) => {
229 self.analyze_expression(expr, 0);
230 }
231 HirStmt::Raise { exception, cause } => {
232 if let Some(exc) = exception {
233 self.analyze_expression(exc, 0);
234 }
235 if let Some(c) = cause {
236 self.analyze_expression(c, 0);
237 }
238 }
239 HirStmt::Break { .. } | HirStmt::Continue { .. } | HirStmt::Pass => {
240 }
242 HirStmt::Block(stmts) => {
244 for s in stmts {
245 self.analyze_statement(s);
246 }
247 }
248 HirStmt::Assert { test, msg } => {
249 self.analyze_expression(test, 0);
251 if let Some(message) = msg {
252 self.analyze_expression(message, 0);
253 }
254 }
255 HirStmt::With {
256 context,
257 target: _,
258 body,
259 ..
260 } => {
261 self.analyze_expression(context, 0);
263
264 for stmt in body {
269 self.analyze_statement(stmt);
270 }
271 }
272 HirStmt::Try {
273 body,
274 handlers,
275 orelse,
276 finalbody,
277 } => {
278 for stmt in body {
280 self.analyze_statement(stmt);
281 }
282
283 for handler in handlers {
285 for stmt in &handler.body {
286 self.analyze_statement(stmt);
287 }
288 }
289
290 if let Some(else_stmts) = orelse {
292 for stmt in else_stmts {
293 self.analyze_statement(stmt);
294 }
295 }
296
297 if let Some(finally_stmts) = finalbody {
299 for stmt in finally_stmts {
300 self.analyze_statement(stmt);
301 }
302 }
303 }
304 HirStmt::FunctionDef { body, .. } => {
307 for stmt in body {
308 self.analyze_statement(stmt);
309 }
310 }
311 }
312 }
313
314 fn check_target_mutation(
317 &mut self,
318 target: &AssignTarget,
319 in_loop: bool,
320 in_conditional: bool,
321 ) {
322 match target {
323 AssignTarget::Symbol(symbol) => {
324 if let Some(usage) = self.param_usage.get_mut(symbol) {
325 usage.is_mutated = true;
326 usage.usage_sites.push(UsageSite {
327 usage_type: UsageType::Write,
328 in_loop,
329 in_conditional,
330 borrow_depth: 0,
331 });
332 }
333 }
334 AssignTarget::Index { base, .. } => {
336 if let HirExpr::Var(var_name) = base.as_ref() {
337 if let Some(usage) = self.param_usage.get_mut(var_name) {
338 usage.is_mutated = true;
339 usage.usage_sites.push(UsageSite {
340 usage_type: UsageType::Write,
341 in_loop,
342 in_conditional,
343 borrow_depth: 0,
344 });
345 }
346 }
347 }
348 AssignTarget::Attribute { value, .. } => {
350 if let HirExpr::Var(var_name) = value.as_ref() {
351 if let Some(usage) = self.param_usage.get_mut(var_name) {
352 usage.is_mutated = true;
353 usage.usage_sites.push(UsageSite {
354 usage_type: UsageType::Write,
355 in_loop,
356 in_conditional,
357 borrow_depth: 0,
358 });
359 }
360 }
361 }
362 AssignTarget::Tuple(targets) => {
365 for t in targets {
366 self.check_target_mutation(t, in_loop, in_conditional);
367 }
368 }
369 }
370 }
371
372 fn analyze_expression(&mut self, expr: &HirExpr, borrow_depth: usize) {
374 match expr {
375 HirExpr::Var(name) => {
376 let in_loop = self.is_in_loop();
377 let in_conditional = self.is_in_conditional();
378 if let Some(usage) = self.param_usage.get_mut(name) {
379 usage.is_read = true;
380 usage.usage_sites.push(UsageSite {
381 usage_type: UsageType::Read,
382 in_loop,
383 in_conditional,
384 borrow_depth,
385 });
386 if in_loop {
387 usage.used_in_loop = true;
388 }
389 }
390 }
391 HirExpr::Attribute { value, attr } => {
392 if let HirExpr::Var(name) = &**value {
393 let in_loop = self.is_in_loop();
394 let in_conditional = self.is_in_conditional();
395 if let Some(usage) = self.param_usage.get_mut(name) {
396 usage.field_accesses.insert(attr.clone());
397 usage.usage_sites.push(UsageSite {
398 usage_type: UsageType::FieldAccess(attr.clone()),
399 in_loop,
400 in_conditional,
401 borrow_depth: borrow_depth + 1,
402 });
403 }
404 }
405 self.analyze_expression(value, borrow_depth + 1);
406 }
407 HirExpr::Call { func, args, .. } => {
408 let in_loop = self.is_in_loop();
410 let in_conditional = self.is_in_conditional();
411 for (i, arg) in args.iter().enumerate() {
412 if let HirExpr::Var(name) = arg {
413 let takes_ownership = self.function_takes_ownership(func, i);
414 if let Some(usage) = self.param_usage.get_mut(name) {
415 if takes_ownership {
417 usage.is_moved = true;
418 self.moved_vars.insert(name.clone());
419 }
420 usage.usage_sites.push(UsageSite {
421 usage_type: UsageType::FunctionArg { takes_ownership },
422 in_loop,
423 in_conditional,
424 borrow_depth,
425 });
426 }
427 }
428 self.analyze_expression(arg, borrow_depth);
429 }
430 }
431 HirExpr::Index { base, index } => {
432 if let HirExpr::Var(name) = &**base {
433 let in_loop = self.is_in_loop();
434 let in_conditional = self.is_in_conditional();
435 if let Some(usage) = self.param_usage.get_mut(name) {
436 usage.usage_sites.push(UsageSite {
437 usage_type: UsageType::IndexAccess,
438 in_loop,
439 in_conditional,
440 borrow_depth: borrow_depth + 1,
441 });
442 }
443 }
444 self.analyze_expression(base, borrow_depth + 1);
445 self.analyze_expression(index, borrow_depth);
446 }
447 HirExpr::Binary { left, right, .. } => {
448 self.analyze_expression(left, borrow_depth);
449 self.analyze_expression(right, borrow_depth);
450 }
451 HirExpr::Unary { operand, .. } => {
452 self.analyze_expression(operand, borrow_depth);
453 }
454 HirExpr::List(elements) | HirExpr::Tuple(elements) => {
455 for elem in elements {
456 self.analyze_expression(elem, borrow_depth);
457 }
458 }
459 HirExpr::Dict(pairs) => {
460 for (k, v) in pairs {
461 self.analyze_expression(k, borrow_depth);
462 self.analyze_expression(v, borrow_depth);
463 }
464 }
465 HirExpr::Borrow { expr, mutable } => {
466 if let HirExpr::Var(name) = &**expr {
467 if *mutable {
468 self.mut_borrowed_vars.insert(name.clone());
469 } else {
470 self.immut_borrowed_vars.insert(name.clone());
471 }
472 }
473 self.analyze_expression(expr, borrow_depth + 1);
474 }
475 HirExpr::MethodCall { object, args, .. } => {
476 self.analyze_expression(object, borrow_depth);
477 for arg in args {
478 self.analyze_expression(arg, borrow_depth);
479 }
480 }
481 HirExpr::Slice {
482 base,
483 start,
484 stop,
485 step,
486 } => {
487 self.analyze_expression(base, borrow_depth);
488 if let Some(s) = start {
489 self.analyze_expression(s, borrow_depth);
490 }
491 if let Some(s) = stop {
492 self.analyze_expression(s, borrow_depth);
493 }
494 if let Some(s) = step {
495 self.analyze_expression(s, borrow_depth);
496 }
497 }
498 HirExpr::Literal(_) => {}
499 HirExpr::ListComp {
500 element,
501 generators,
502 } => {
503 self.context_stack.push(AnalysisContext::Loop);
505
506 for gen in generators {
508 self.analyze_expression(&gen.iter, borrow_depth);
510
511 for cond in &gen.conditions {
513 self.analyze_expression(cond, borrow_depth);
514 }
515 }
516
517 self.analyze_expression(element, borrow_depth);
519
520 self.context_stack.pop();
521 }
522 HirExpr::FString { .. } => {
523 }
525 HirExpr::Lambda { params: _, body } => {
526 self.analyze_expression(body, borrow_depth);
529 }
530 HirExpr::Set(elements) | HirExpr::FrozenSet(elements) => {
531 for elem in elements {
532 self.analyze_expression(elem, borrow_depth);
533 }
534 }
535 HirExpr::SetComp {
536 element,
537 generators,
538 } => {
539 self.context_stack.push(AnalysisContext::Loop);
541
542 for gen in generators {
544 self.analyze_expression(&gen.iter, borrow_depth);
546
547 for cond in &gen.conditions {
549 self.analyze_expression(cond, borrow_depth);
550 }
551 }
552
553 self.analyze_expression(element, borrow_depth);
555
556 self.context_stack.pop();
557 }
558 HirExpr::DictComp {
559 key,
560 value,
561 generators,
562 } => {
563 self.context_stack.push(AnalysisContext::Loop);
565
566 for gen in generators {
568 self.analyze_expression(&gen.iter, borrow_depth);
570
571 for cond in &gen.conditions {
573 self.analyze_expression(cond, borrow_depth);
574 }
575 }
576
577 self.analyze_expression(key, borrow_depth);
579 self.analyze_expression(value, borrow_depth);
580
581 self.context_stack.pop();
582 }
583 HirExpr::Await { value } => {
584 self.analyze_expression(value, borrow_depth);
586 }
587 HirExpr::Yield { value } => {
588 if let Some(v) = value {
590 self.analyze_expression(v, borrow_depth);
591 }
592 }
593 HirExpr::IfExpr { test, body, orelse } => {
594 self.analyze_expression(test, borrow_depth);
596 self.analyze_expression(body, borrow_depth);
597 self.analyze_expression(orelse, borrow_depth);
598 }
599 HirExpr::SortByKey {
600 iterable, key_body, ..
601 } => {
602 self.analyze_expression(iterable, borrow_depth);
604 self.analyze_expression(key_body, borrow_depth);
605 }
606 HirExpr::GeneratorExp {
607 element,
608 generators,
609 } => {
610 self.analyze_expression(element, borrow_depth);
612 for gen in generators {
613 self.analyze_expression(&gen.iter, borrow_depth);
614 for cond in &gen.conditions {
615 self.analyze_expression(cond, borrow_depth);
616 }
617 }
618 }
619 HirExpr::NamedExpr { value, .. } => {
621 self.analyze_expression(value, borrow_depth);
622 }
623 HirExpr::DynamicCall { callee, args, .. } => {
625 self.analyze_expression(callee, borrow_depth);
626 for arg in args {
627 self.analyze_expression(arg, borrow_depth);
628 }
629 }
630 }
631 }
632
633 fn analyze_expression_for_return(&mut self, expr: &HirExpr) {
637 match expr {
638 HirExpr::Var(name) => {
639 if let Some(usage) = self.param_usage.get_mut(name) {
641 usage.escapes_through_return = true;
642 usage.usage_sites.push(UsageSite {
643 usage_type: UsageType::Return,
644 in_loop: false,
645 in_conditional: false,
646 borrow_depth: 0,
647 });
648 }
649 }
650 HirExpr::Binary { left, right, .. } => {
651 self.analyze_expression(left, 0);
654 self.analyze_expression(right, 0);
655 }
656 _ => self.analyze_expression(expr, 0),
657 }
658 }
659
660 fn function_takes_ownership(&self, func_name: &str, _arg_index: usize) -> bool {
662 let borrowing_functions = [
664 "len",
665 "str",
666 "repr",
667 "format",
668 "print",
669 "isinstance",
670 "hasattr",
671 "getattr",
672 "contains",
673 "startswith",
674 "endswith",
675 "find",
676 "index",
677 "count",
678 "int",
680 "float",
681 "bool",
682 "sum",
685 "min",
686 "max",
687 "any",
688 "all",
689 "sorted",
690 "reversed",
691 "enumerate",
692 "zip",
693 "map",
694 "filter",
695 ];
696
697 let ownership_functions = ["append", "extend", "insert", "remove", "pop", "sort"];
699
700 if borrowing_functions.contains(&func_name) {
701 false
702 } else if ownership_functions.contains(&func_name) {
703 true
704 } else {
705 false
710 }
711 }
712
713 fn is_in_loop(&self) -> bool {
715 self.context_stack
716 .iter()
717 .any(|ctx| matches!(ctx, AnalysisContext::Loop))
718 }
719
720 fn is_in_conditional(&self) -> bool {
722 self.context_stack
723 .iter()
724 .any(|ctx| matches!(ctx, AnalysisContext::Conditional))
725 }
726
727 fn determine_strategies(
729 &self,
730 func: &HirFunction,
731 type_mapper: &TypeMapper,
732 ) -> BorrowingAnalysisResult {
733 let mut strategies = IndexMap::new();
734 let mut insights = Vec::new();
735
736 for param in &func.params {
737 let usage = self
738 .param_usage
739 .get(¶m.name)
740 .cloned()
741 .unwrap_or_default();
742 let rust_type = type_mapper.map_type(¶m.ty);
743
744 let strategy = self.determine_parameter_strategy(
745 ¶m.name,
746 &usage,
747 &rust_type,
748 ¶m.ty,
749 &mut insights,
750 );
751
752 strategies.insert(param.name.clone(), strategy);
753 }
754
755 BorrowingAnalysisResult {
756 param_strategies: strategies,
757 insights,
758 }
759 }
760
761 fn determine_parameter_strategy(
763 &self,
764 param_name: &str,
765 usage: &ParameterUsagePattern,
766 rust_type: &RustType,
767 python_type: &PythonType,
768 insights: &mut Vec<BorrowingInsight>,
769 ) -> BorrowingStrategy {
770 trace_decision!(
772 category = DecisionCategory::BorrowStrategy,
773 name = "param_strategy",
774 chosen = param_name,
775 alternatives = ["owned", "borrowed", "mutable_ref", "copy", "arc"],
776 confidence = 0.85
777 );
778
779 if self.is_copy_type(rust_type) {
781 insights.push(BorrowingInsight::SuggestCopyDerive(param_name.to_string()));
782 }
783
784 if usage.is_moved {
786 if !usage.escapes_through_return && !usage.is_stored {
788 insights.push(BorrowingInsight::UnnecessaryMove(param_name.to_string()));
789 }
790 return BorrowingStrategy::TakeOwnership;
791 }
792
793 if usage.escapes_through_return && !matches!(python_type, PythonType::String) {
796 if let Some(ref ret_type) = self.return_type {
797 if python_type == ret_type {
798 return BorrowingStrategy::TakeOwnership;
799 }
800 }
801 }
802
803 if usage.is_stored {
805 return BorrowingStrategy::UseSharedOwnership {
806 is_thread_safe: false,
807 };
808 }
809
810 if usage.used_in_closure {
812 return BorrowingStrategy::TakeOwnership;
814 }
815
816 if self.is_copy_type(rust_type) {
818 return BorrowingStrategy::TakeOwnership; }
820
821 if matches!(python_type, PythonType::String) {
823 return self.determine_string_strategy(param_name, usage);
824 }
825
826 if usage.is_mutated {
828 BorrowingStrategy::BorrowMutable { lifetime: None }
829 } else if usage.is_read {
830 BorrowingStrategy::BorrowImmutable { lifetime: None }
831 } else {
832 BorrowingStrategy::TakeOwnership
834 }
835 }
836
837 fn determine_string_strategy(
839 &self,
840 _param_name: &str,
841 usage: &ParameterUsagePattern,
842 ) -> BorrowingStrategy {
843 trace_decision!(
845 category = DecisionCategory::BorrowStrategy,
846 name = "string_strategy",
847 chosen = "string_analysis",
848 alternatives = ["String", "&str", "Cow", "AsRef", "Into"],
849 confidence = 0.82
850 );
851
852 if usage.is_moved {
858 return BorrowingStrategy::TakeOwnership;
859 }
860
861 if usage.is_mutated {
864 return BorrowingStrategy::TakeOwnership;
865 }
866
867 if usage.escapes_through_return {
873 return BorrowingStrategy::TakeOwnership;
874 }
875
876 if usage.is_read && !usage.is_moved && !usage.is_mutated {
878 return BorrowingStrategy::BorrowImmutable { lifetime: None };
879 }
880
881 BorrowingStrategy::TakeOwnership
883 }
884
885 #[allow(clippy::only_used_in_recursion)]
887 fn is_copy_type(&self, rust_type: &RustType) -> bool {
888 match rust_type {
889 RustType::Primitive(_) => true,
890 RustType::Unit => true,
891 RustType::Tuple(types) => types.iter().all(|t| self.is_copy_type(t)),
892 _ => false,
893 }
894 }
895
896 pub fn analyze_with_escape_analysis(
906 &mut self,
907 func: &depyler_hir::hir::HirFunction,
908 type_mapper: &TypeMapper,
909 ) -> EnhancedBorrowingResult {
910 use crate::escape_analysis::{analyze_ownership, OwnershipFix};
911
912 let basic_result = self.analyze_function(func, type_mapper);
914
915 let ownership_result = analyze_ownership(func);
917
918 let mut ownership_fixes = Vec::new();
920
921 for error in &ownership_result.use_after_move_errors {
922 ownership_fixes.push(OwnershipFixSite {
923 variable: error.var.clone(),
924 site_index: error.move_site.stmt_index,
925 fix: match &error.fix {
926 OwnershipFix::Borrow => OwnershipFixType::UseBorrow,
927 OwnershipFix::MutableBorrow => OwnershipFixType::UseMutableBorrow,
928 OwnershipFix::Clone => OwnershipFixType::InsertClone,
929 OwnershipFix::CloneAtAssignment { var } => {
930 OwnershipFixType::CloneAtAssignment(var.clone())
931 }
932 OwnershipFix::Reject { reason } => {
933 OwnershipFixType::RejectPattern(reason.clone())
934 }
935 },
936 });
937 }
938
939 for pattern in &ownership_result.aliasing_patterns {
940 if pattern.source_used_after && pattern.alias_used_after {
941 ownership_fixes.push(OwnershipFixSite {
942 variable: pattern.alias.clone(),
943 site_index: 0, fix: OwnershipFixType::CloneAtAssignment(pattern.source.clone()),
945 });
946 }
947 }
948
949 EnhancedBorrowingResult {
950 basic_result,
951 use_after_move_count: ownership_result.use_after_move_errors.len(),
952 aliasing_pattern_count: ownership_result.aliasing_patterns.len(),
953 ownership_fixes,
954 }
955 }
956}
957
958#[derive(Debug, Clone)]
960pub struct EnhancedBorrowingResult {
961 pub basic_result: BorrowingAnalysisResult,
963 pub use_after_move_count: usize,
965 pub aliasing_pattern_count: usize,
967 pub ownership_fixes: Vec<OwnershipFixSite>,
969}
970
971#[derive(Debug, Clone)]
973pub struct OwnershipFixSite {
974 pub variable: String,
976 pub site_index: usize,
978 pub fix: OwnershipFixType,
980}
981
982#[derive(Debug, Clone, PartialEq, Eq)]
984pub enum OwnershipFixType {
985 UseBorrow,
987 UseMutableBorrow,
989 InsertClone,
991 CloneAtAssignment(String),
993 RejectPattern(String),
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000 use depyler_hir::hir::{FunctionProperties, HirParam, Literal};
1001 use depyler_annotations::TranspilationAnnotations;
1002 use smallvec::smallvec;
1003
1004 #[test]
1005 fn test_basic_borrowing_analysis() {
1006 let mut ctx = BorrowingContext::new(Some(PythonType::Int));
1007 let type_mapper = TypeMapper::new();
1008
1009 let func = HirFunction {
1010 name: "add_one".to_string(),
1011 params: smallvec![HirParam::new("x".to_string(), PythonType::Int)],
1012 ret_type: PythonType::Int,
1013 body: vec![HirStmt::Return(Some(HirExpr::Binary {
1014 op: depyler_hir::hir::BinOp::Add,
1015 left: Box::new(HirExpr::Var("x".to_string())),
1016 right: Box::new(HirExpr::Literal(Literal::Int(1))),
1017 }))],
1018 properties: FunctionProperties::default(),
1019 annotations: TranspilationAnnotations::default(),
1020 docstring: None,
1021 };
1022
1023 let result = ctx.analyze_function(&func, &type_mapper);
1024
1025 let x_strategy = result.param_strategies.get("x").unwrap();
1027 assert_eq!(*x_strategy, BorrowingStrategy::TakeOwnership);
1028 }
1029
1030 #[test]
1031 fn test_string_borrowing() {
1032 let mut ctx = BorrowingContext::new(Some(PythonType::Int));
1033 let type_mapper = TypeMapper::new();
1034
1035 let func = HirFunction {
1036 name: "string_len".to_string(),
1037 params: smallvec![HirParam::new("s".to_string(), PythonType::String)],
1038 ret_type: PythonType::Int,
1039 body: vec![HirStmt::Return(Some(HirExpr::Call {
1040 func: "len".to_string(),
1041 args: vec![HirExpr::Var("s".to_string())],
1042 kwargs: vec![],
1043 }))],
1044 properties: FunctionProperties::default(),
1045 annotations: TranspilationAnnotations::default(),
1046 docstring: None,
1047 };
1048
1049 let result = ctx.analyze_function(&func, &type_mapper);
1050
1051 let s_strategy = result.param_strategies.get("s").unwrap();
1053 assert!(matches!(
1054 s_strategy,
1055 BorrowingStrategy::BorrowImmutable { .. }
1056 ));
1057 }
1058
1059 #[test]
1060 fn test_mutation_detection() {
1061 let mut ctx = BorrowingContext::new(None);
1062 let type_mapper = TypeMapper::new();
1063
1064 let func = HirFunction {
1065 name: "mutate_list".to_string(),
1066 params: smallvec![HirParam::new(
1067 "lst".to_string(),
1068 PythonType::List(Box::new(PythonType::Int))
1069 )],
1070 ret_type: PythonType::None,
1071 body: vec![HirStmt::Expr(HirExpr::Call {
1072 func: "append".to_string(),
1073 args: vec![
1074 HirExpr::Var("lst".to_string()),
1075 HirExpr::Literal(Literal::Int(42)),
1076 ],
1077 kwargs: vec![],
1078 })],
1079 properties: FunctionProperties::default(),
1080 annotations: TranspilationAnnotations::default(),
1081 docstring: None,
1082 };
1083
1084 let result = ctx.analyze_function(&func, &type_mapper);
1085
1086 let lst_strategy = result.param_strategies.get("lst").unwrap();
1088 assert_eq!(*lst_strategy, BorrowingStrategy::TakeOwnership);
1089 }
1090
1091 #[test]
1093 fn test_borrowing_context_new_with_none() {
1094 let ctx = BorrowingContext::new(None);
1095 assert!(ctx.param_usage.is_empty());
1096 assert!(ctx.moved_vars.is_empty());
1097 assert!(ctx.mut_borrowed_vars.is_empty());
1098 assert!(ctx.immut_borrowed_vars.is_empty());
1099 }
1100
1101 #[test]
1102 fn test_borrowing_context_new_with_return_type() {
1103 let ctx = BorrowingContext::new(Some(PythonType::String));
1104 assert!(ctx.return_type.is_some());
1105 }
1106
1107 #[test]
1109 fn test_parameter_usage_pattern_default() {
1110 let pattern = ParameterUsagePattern::default();
1111 assert!(!pattern.is_read);
1112 assert!(!pattern.is_mutated);
1113 assert!(!pattern.is_moved);
1114 assert!(!pattern.escapes_through_return);
1115 assert!(!pattern.is_stored);
1116 assert!(!pattern.used_in_closure);
1117 assert!(!pattern.used_in_loop);
1118 assert!(pattern.field_accesses.is_empty());
1119 assert!(pattern.method_calls.is_empty());
1120 assert!(pattern.usage_sites.is_empty());
1121 }
1122
1123 #[test]
1124 fn test_parameter_usage_pattern_clone() {
1125 let pattern = ParameterUsagePattern {
1126 is_read: true,
1127 is_mutated: true,
1128 ..Default::default()
1129 };
1130 let cloned = pattern.clone();
1131 assert_eq!(pattern.is_read, cloned.is_read);
1132 assert_eq!(pattern.is_mutated, cloned.is_mutated);
1133 }
1134
1135 #[test]
1137 fn test_usage_site_creation() {
1138 let site = UsageSite {
1139 usage_type: UsageType::Read,
1140 in_loop: true,
1141 in_conditional: false,
1142 borrow_depth: 1,
1143 };
1144 assert!(site.in_loop);
1145 assert!(!site.in_conditional);
1146 assert_eq!(site.borrow_depth, 1);
1147 }
1148
1149 #[test]
1150 fn test_usage_site_clone() {
1151 let site = UsageSite {
1152 usage_type: UsageType::Write,
1153 in_loop: false,
1154 in_conditional: true,
1155 borrow_depth: 2,
1156 };
1157 let cloned = site.clone();
1158 assert_eq!(site.in_loop, cloned.in_loop);
1159 assert_eq!(site.borrow_depth, cloned.borrow_depth);
1160 }
1161
1162 #[test]
1164 fn test_usage_type_read() {
1165 let usage = UsageType::Read;
1166 assert_eq!(usage, UsageType::Read);
1167 }
1168
1169 #[test]
1170 fn test_usage_type_write() {
1171 let usage = UsageType::Write;
1172 assert_eq!(usage, UsageType::Write);
1173 }
1174
1175 #[test]
1176 fn test_usage_type_method_call() {
1177 let usage = UsageType::MethodCall("append".to_string());
1178 assert!(matches!(usage, UsageType::MethodCall(_)));
1179 }
1180
1181 #[test]
1182 fn test_usage_type_function_arg_owned() {
1183 let usage = UsageType::FunctionArg {
1184 takes_ownership: true,
1185 };
1186 assert!(matches!(
1187 usage,
1188 UsageType::FunctionArg {
1189 takes_ownership: true
1190 }
1191 ));
1192 }
1193
1194 #[test]
1195 fn test_usage_type_function_arg_borrowed() {
1196 let usage = UsageType::FunctionArg {
1197 takes_ownership: false,
1198 };
1199 assert!(matches!(
1200 usage,
1201 UsageType::FunctionArg {
1202 takes_ownership: false
1203 }
1204 ));
1205 }
1206
1207 #[test]
1208 fn test_usage_type_return() {
1209 let usage = UsageType::Return;
1210 assert_eq!(usage, UsageType::Return);
1211 }
1212
1213 #[test]
1214 fn test_usage_type_store() {
1215 let usage = UsageType::Store;
1216 assert_eq!(usage, UsageType::Store);
1217 }
1218
1219 #[test]
1220 fn test_usage_type_closure() {
1221 let usage = UsageType::Closure {
1222 captures_by_value: true,
1223 };
1224 assert!(matches!(
1225 usage,
1226 UsageType::Closure {
1227 captures_by_value: true
1228 }
1229 ));
1230 }
1231
1232 #[test]
1233 fn test_usage_type_field_access() {
1234 let usage = UsageType::FieldAccess("name".to_string());
1235 assert!(matches!(usage, UsageType::FieldAccess(_)));
1236 }
1237
1238 #[test]
1239 fn test_usage_type_index_access() {
1240 let usage = UsageType::IndexAccess;
1241 assert_eq!(usage, UsageType::IndexAccess);
1242 }
1243
1244 #[test]
1245 fn test_usage_type_clone() {
1246 let usage = UsageType::MethodCall("push".to_string());
1247 let cloned = usage.clone();
1248 assert_eq!(usage, cloned);
1249 }
1250
1251 #[test]
1253 fn test_borrowing_strategy_take_ownership() {
1254 let strategy = BorrowingStrategy::TakeOwnership;
1255 assert_eq!(strategy, BorrowingStrategy::TakeOwnership);
1256 }
1257
1258 #[test]
1259 fn test_borrowing_strategy_borrow_immutable() {
1260 let strategy = BorrowingStrategy::BorrowImmutable { lifetime: None };
1261 assert!(matches!(
1262 strategy,
1263 BorrowingStrategy::BorrowImmutable { .. }
1264 ));
1265 }
1266
1267 #[test]
1268 fn test_borrowing_strategy_borrow_immutable_with_lifetime() {
1269 let strategy = BorrowingStrategy::BorrowImmutable {
1270 lifetime: Some("'a".to_string()),
1271 };
1272 if let BorrowingStrategy::BorrowImmutable { lifetime } = &strategy {
1273 assert_eq!(lifetime, &Some("'a".to_string()));
1274 }
1275 }
1276
1277 #[test]
1278 fn test_borrowing_strategy_borrow_mutable() {
1279 let strategy = BorrowingStrategy::BorrowMutable { lifetime: None };
1280 assert!(matches!(strategy, BorrowingStrategy::BorrowMutable { .. }));
1281 }
1282
1283 #[test]
1284 fn test_borrowing_strategy_use_cow() {
1285 let strategy = BorrowingStrategy::UseCow {
1286 lifetime: "'a".to_string(),
1287 };
1288 if let BorrowingStrategy::UseCow { lifetime } = &strategy {
1289 assert_eq!(lifetime, "'a");
1290 }
1291 }
1292
1293 #[test]
1294 fn test_borrowing_strategy_use_shared_ownership_arc() {
1295 let strategy = BorrowingStrategy::UseSharedOwnership {
1296 is_thread_safe: true,
1297 };
1298 if let BorrowingStrategy::UseSharedOwnership { is_thread_safe } = strategy {
1299 assert!(is_thread_safe);
1300 }
1301 }
1302
1303 #[test]
1304 fn test_borrowing_strategy_use_shared_ownership_rc() {
1305 let strategy = BorrowingStrategy::UseSharedOwnership {
1306 is_thread_safe: false,
1307 };
1308 if let BorrowingStrategy::UseSharedOwnership { is_thread_safe } = strategy {
1309 assert!(!is_thread_safe);
1310 }
1311 }
1312
1313 #[test]
1314 fn test_borrowing_strategy_clone() {
1315 let strategy = BorrowingStrategy::TakeOwnership;
1316 let cloned = strategy.clone();
1317 assert_eq!(strategy, cloned);
1318 }
1319
1320 #[test]
1322 fn test_borrowing_insight_unnecessary_move() {
1323 let insight = BorrowingInsight::UnnecessaryMove("x".to_string());
1324 assert!(matches!(insight, BorrowingInsight::UnnecessaryMove(_)));
1325 }
1326
1327 #[test]
1328 fn test_borrowing_insight_lifetime_optimization() {
1329 let insight = BorrowingInsight::LifetimeOptimization {
1330 param: "s".to_string(),
1331 suggestion: "Use 'a".to_string(),
1332 };
1333 assert!(matches!(
1334 insight,
1335 BorrowingInsight::LifetimeOptimization { .. }
1336 ));
1337 }
1338
1339 #[test]
1340 fn test_borrowing_insight_suggest_copy() {
1341 let insight = BorrowingInsight::SuggestCopyDerive("Point".to_string());
1342 assert!(matches!(insight, BorrowingInsight::SuggestCopyDerive(_)));
1343 }
1344
1345 #[test]
1346 fn test_borrowing_insight_borrow_conflict() {
1347 let insight = BorrowingInsight::PotentialBorrowConflict {
1348 param: "data".to_string(),
1349 locations: vec!["line 10".to_string(), "line 20".to_string()],
1350 };
1351 assert!(matches!(
1352 insight,
1353 BorrowingInsight::PotentialBorrowConflict { .. }
1354 ));
1355 }
1356
1357 #[test]
1358 fn test_borrowing_insight_clone() {
1359 let insight = BorrowingInsight::UnnecessaryMove("y".to_string());
1360 let cloned = insight.clone();
1361 assert!(matches!(cloned, BorrowingInsight::UnnecessaryMove(_)));
1362 }
1363
1364 #[test]
1366 fn test_borrowing_analysis_result_creation() {
1367 let result = BorrowingAnalysisResult {
1368 param_strategies: IndexMap::new(),
1369 insights: Vec::new(),
1370 };
1371 assert!(result.param_strategies.is_empty());
1372 assert!(result.insights.is_empty());
1373 }
1374
1375 #[test]
1376 fn test_borrowing_analysis_result_with_strategies() {
1377 let mut strategies = IndexMap::new();
1378 strategies.insert("x".to_string(), BorrowingStrategy::TakeOwnership);
1379 let result = BorrowingAnalysisResult {
1380 param_strategies: strategies,
1381 insights: Vec::new(),
1382 };
1383 assert_eq!(result.param_strategies.len(), 1);
1384 assert!(result.param_strategies.contains_key("x"));
1385 }
1386
1387 #[test]
1388 fn test_borrowing_analysis_result_clone() {
1389 let result = BorrowingAnalysisResult {
1390 param_strategies: IndexMap::new(),
1391 insights: vec![BorrowingInsight::SuggestCopyDerive("T".to_string())],
1392 };
1393 let cloned = result.clone();
1394 assert_eq!(result.insights.len(), cloned.insights.len());
1395 }
1396
1397 #[test]
1399 fn test_analyze_function_with_float_param() {
1400 let mut ctx = BorrowingContext::new(Some(PythonType::Float));
1401 let type_mapper = TypeMapper::new();
1402
1403 let func = HirFunction {
1404 name: "double".to_string(),
1405 params: smallvec![HirParam::new("x".to_string(), PythonType::Float)],
1406 ret_type: PythonType::Float,
1407 body: vec![HirStmt::Return(Some(HirExpr::Binary {
1408 op: depyler_hir::hir::BinOp::Mul,
1409 left: Box::new(HirExpr::Var("x".to_string())),
1410 right: Box::new(HirExpr::Literal(Literal::Float(2.0))),
1411 }))],
1412 properties: FunctionProperties::default(),
1413 annotations: TranspilationAnnotations::default(),
1414 docstring: None,
1415 };
1416
1417 let result = ctx.analyze_function(&func, &type_mapper);
1418 let x_strategy = result.param_strategies.get("x").unwrap();
1420 assert_eq!(*x_strategy, BorrowingStrategy::TakeOwnership);
1421 }
1422
1423 #[test]
1424 fn test_analyze_function_with_bool_param() {
1425 let mut ctx = BorrowingContext::new(Some(PythonType::Bool));
1426 let type_mapper = TypeMapper::new();
1427
1428 let func = HirFunction {
1429 name: "negate".to_string(),
1430 params: smallvec![HirParam::new("b".to_string(), PythonType::Bool)],
1431 ret_type: PythonType::Bool,
1432 body: vec![HirStmt::Return(Some(HirExpr::Unary {
1433 op: depyler_hir::hir::UnaryOp::Not,
1434 operand: Box::new(HirExpr::Var("b".to_string())),
1435 }))],
1436 properties: FunctionProperties::default(),
1437 annotations: TranspilationAnnotations::default(),
1438 docstring: None,
1439 };
1440
1441 let result = ctx.analyze_function(&func, &type_mapper);
1442 let b_strategy = result.param_strategies.get("b").unwrap();
1444 assert_eq!(*b_strategy, BorrowingStrategy::TakeOwnership);
1445 }
1446
1447 #[test]
1448 fn test_analyze_function_empty_body() {
1449 let mut ctx = BorrowingContext::new(None);
1450 let type_mapper = TypeMapper::new();
1451
1452 let func = HirFunction {
1453 name: "noop".to_string(),
1454 params: smallvec![HirParam::new("x".to_string(), PythonType::Int)],
1455 ret_type: PythonType::None,
1456 body: vec![],
1457 properties: FunctionProperties::default(),
1458 annotations: TranspilationAnnotations::default(),
1459 docstring: None,
1460 };
1461
1462 let result = ctx.analyze_function(&func, &type_mapper);
1463 assert!(result.param_strategies.contains_key("x"));
1465 }
1466
1467 #[test]
1468 fn test_analyze_function_no_params() {
1469 let mut ctx = BorrowingContext::new(Some(PythonType::Int));
1470 let type_mapper = TypeMapper::new();
1471
1472 let func = HirFunction {
1473 name: "get_zero".to_string(),
1474 params: smallvec![],
1475 ret_type: PythonType::Int,
1476 body: vec![HirStmt::Return(Some(HirExpr::Literal(Literal::Int(0))))],
1477 properties: FunctionProperties::default(),
1478 annotations: TranspilationAnnotations::default(),
1479 docstring: None,
1480 };
1481
1482 let result = ctx.analyze_function(&func, &type_mapper);
1483 assert!(result.param_strategies.is_empty());
1484 }
1485
1486 #[test]
1487 fn test_analyze_function_multiple_params() {
1488 let mut ctx = BorrowingContext::new(Some(PythonType::Int));
1489 let type_mapper = TypeMapper::new();
1490
1491 let func = HirFunction {
1492 name: "add".to_string(),
1493 params: smallvec![
1494 HirParam::new("a".to_string(), PythonType::Int),
1495 HirParam::new("b".to_string(), PythonType::Int)
1496 ],
1497 ret_type: PythonType::Int,
1498 body: vec![HirStmt::Return(Some(HirExpr::Binary {
1499 op: depyler_hir::hir::BinOp::Add,
1500 left: Box::new(HirExpr::Var("a".to_string())),
1501 right: Box::new(HirExpr::Var("b".to_string())),
1502 }))],
1503 properties: FunctionProperties::default(),
1504 annotations: TranspilationAnnotations::default(),
1505 docstring: None,
1506 };
1507
1508 let result = ctx.analyze_function(&func, &type_mapper);
1509 assert_eq!(result.param_strategies.len(), 2);
1510 assert!(result.param_strategies.contains_key("a"));
1511 assert!(result.param_strategies.contains_key("b"));
1512 }
1513
1514 #[test]
1517 fn test_analysis_context_loop_debug() {
1518 let ctx = AnalysisContext::Loop;
1519 let debug = format!("{:?}", ctx);
1520 assert!(debug.contains("Loop"));
1521 }
1522
1523 #[test]
1524 fn test_analysis_context_conditional_debug() {
1525 let ctx = AnalysisContext::Conditional;
1526 let debug = format!("{:?}", ctx);
1527 assert!(debug.contains("Conditional"));
1528 }
1529
1530 #[test]
1531 fn test_analysis_context_closure_debug() {
1532 let ctx = AnalysisContext::Closure {
1533 captures: HashSet::from(["x".to_string(), "y".to_string()]),
1534 };
1535 let debug = format!("{:?}", ctx);
1536 assert!(debug.contains("Closure"));
1537 assert!(debug.contains("captures"));
1538 }
1539
1540 #[test]
1541 fn test_analysis_context_function_debug() {
1542 let ctx = AnalysisContext::Function;
1543 let debug = format!("{:?}", ctx);
1544 assert!(debug.contains("Function"));
1545 }
1546
1547 #[test]
1548 fn test_analysis_context_clone() {
1549 let ctx = AnalysisContext::Closure {
1550 captures: HashSet::from(["var".to_string()]),
1551 };
1552 let cloned = ctx.clone();
1553 if let AnalysisContext::Closure { captures } = cloned {
1554 assert!(captures.contains("var"));
1555 } else {
1556 panic!("Expected Closure variant");
1557 }
1558 }
1559
1560 #[test]
1561 fn test_borrowing_context_debug() {
1562 let ctx = BorrowingContext::new(None);
1563 let debug = format!("{:?}", ctx);
1564 assert!(debug.contains("BorrowingContext"));
1565 }
1566
1567 #[test]
1568 fn test_borrowing_context_initial_state() {
1569 let ctx = BorrowingContext::new(Some(PythonType::String));
1570 assert!(ctx.param_usage.is_empty());
1571 assert!(ctx.moved_vars.is_empty());
1572 assert!(ctx.mut_borrowed_vars.is_empty());
1573 assert!(ctx.immut_borrowed_vars.is_empty());
1574 assert!(!ctx.context_stack.is_empty()); }
1576
1577 #[test]
1578 fn test_parameter_usage_pattern_all_fields() {
1579 let mut pattern = ParameterUsagePattern {
1580 is_read: true,
1581 is_mutated: true,
1582 is_moved: false,
1583 escapes_through_return: true,
1584 is_stored: false,
1585 used_in_closure: true,
1586 used_in_loop: true,
1587 ..Default::default()
1588 };
1589 pattern.field_accesses.insert("name".to_string());
1590 pattern.method_calls.insert("len".to_string());
1591
1592 assert!(pattern.is_read);
1593 assert!(pattern.is_mutated);
1594 assert!(!pattern.is_moved);
1595 assert!(pattern.escapes_through_return);
1596 assert!(!pattern.is_stored);
1597 assert!(pattern.used_in_closure);
1598 assert!(pattern.used_in_loop);
1599 assert!(pattern.field_accesses.contains("name"));
1600 assert!(pattern.method_calls.contains("len"));
1601 }
1602
1603 #[test]
1604 fn test_parameter_usage_pattern_debug() {
1605 let pattern = ParameterUsagePattern::default();
1606 let debug = format!("{:?}", pattern);
1607 assert!(debug.contains("ParameterUsagePattern"));
1608 assert!(debug.contains("is_read"));
1609 }
1610
1611 #[test]
1612 fn test_usage_site_all_fields() {
1613 let site = UsageSite {
1614 usage_type: UsageType::Write,
1615 in_loop: true,
1616 in_conditional: false,
1617 borrow_depth: 2,
1618 };
1619 assert!(matches!(site.usage_type, UsageType::Write));
1620 assert!(site.in_loop);
1621 assert!(!site.in_conditional);
1622 assert_eq!(site.borrow_depth, 2);
1623 }
1624
1625 #[test]
1626 fn test_usage_site_debug() {
1627 let site = UsageSite {
1628 usage_type: UsageType::Read,
1629 in_loop: false,
1630 in_conditional: true,
1631 borrow_depth: 0,
1632 };
1633 let debug = format!("{:?}", site);
1634 assert!(debug.contains("UsageSite"));
1635 assert!(debug.contains("Read"));
1636 }
1637
1638 #[test]
1639 fn test_usage_type_read_eq() {
1640 assert_eq!(UsageType::Read, UsageType::Read);
1641 assert_ne!(UsageType::Read, UsageType::Write);
1642 }
1643
1644 #[test]
1645 fn test_usage_type_closure_variants() {
1646 let by_value = UsageType::Closure {
1647 captures_by_value: true,
1648 };
1649 let by_ref = UsageType::Closure {
1650 captures_by_value: false,
1651 };
1652 assert_ne!(by_value, by_ref);
1653 }
1654
1655 #[test]
1656 fn test_borrowing_strategy_eq() {
1657 assert_eq!(
1658 BorrowingStrategy::TakeOwnership,
1659 BorrowingStrategy::TakeOwnership
1660 );
1661 assert_ne!(
1662 BorrowingStrategy::TakeOwnership,
1663 BorrowingStrategy::BorrowMutable { lifetime: None }
1664 );
1665 }
1666
1667 #[test]
1668 fn test_borrowing_strategy_debug_all() {
1669 let strategies = [
1670 BorrowingStrategy::TakeOwnership,
1671 BorrowingStrategy::BorrowImmutable { lifetime: None },
1672 BorrowingStrategy::BorrowImmutable {
1673 lifetime: Some("a".to_string()),
1674 },
1675 BorrowingStrategy::BorrowMutable { lifetime: None },
1676 BorrowingStrategy::UseCow {
1677 lifetime: "b".to_string(),
1678 },
1679 BorrowingStrategy::UseSharedOwnership {
1680 is_thread_safe: true,
1681 },
1682 ];
1683 for strategy in strategies {
1684 let debug = format!("{:?}", strategy);
1685 assert!(!debug.is_empty());
1686 }
1687 }
1688
1689 #[test]
1690 fn test_borrowing_insight_debug_all() {
1691 let insights = [
1692 BorrowingInsight::UnnecessaryMove("x".to_string()),
1693 BorrowingInsight::LifetimeOptimization {
1694 param: "p".to_string(),
1695 suggestion: "Consider borrow".to_string(),
1696 },
1697 BorrowingInsight::SuggestCopyDerive("y".to_string()),
1698 BorrowingInsight::PotentialBorrowConflict {
1699 param: "z".to_string(),
1700 locations: vec!["line 1".to_string(), "line 2".to_string()],
1701 },
1702 ];
1703 for insight in insights {
1704 let debug = format!("{:?}", insight);
1705 assert!(!debug.is_empty());
1706 }
1707 }
1708
1709 #[test]
1710 fn test_borrowing_analysis_result_debug() {
1711 let result = BorrowingAnalysisResult {
1712 param_strategies: IndexMap::new(),
1713 insights: vec![],
1714 };
1715 let debug = format!("{:?}", result);
1716 assert!(debug.contains("BorrowingAnalysisResult"));
1717 }
1718
1719 #[test]
1720 fn test_borrowing_analysis_result_with_insights() {
1721 let result = BorrowingAnalysisResult {
1722 param_strategies: IndexMap::new(),
1723 insights: vec![
1724 BorrowingInsight::SuggestCopyDerive("val".to_string()),
1725 BorrowingInsight::LifetimeOptimization {
1726 param: "p".to_string(),
1727 suggestion: "Borrow instead".to_string(),
1728 },
1729 ],
1730 };
1731 assert_eq!(result.insights.len(), 2);
1732 }
1733}