1use super::dataflow_v2::{
10 DataFlowGraphV2, FlowData, FlowKind, Guard, GuardKind, ScopeData, ScopeId, ScopeKind, VarData,
11 VarKind,
12};
13use super::lock_v2::{AccessKind, LockAcquisitionV2, LockType};
14use super::var_id::VarId;
15use crate::ast::ASTRegistry;
16use crate::symbol::{SymbolId, SymbolPath, SymbolRegistry, VarScope};
17use im::HashMap as ImHashMap;
18use ryo_source::pure::{
19 PureBlock, PureExpr, PureFile, PureImplItem, PureItem, PureParam, PurePattern, PureStmt,
20};
21use ryo_symbol::{SymbolPathResolver, WorkspaceFilePath};
22use smallvec::smallvec;
23use std::collections::{HashMap, HashSet};
24use std::sync::Arc;
25
26pub struct DataFlowBuilderWorkspace<'a> {
28 registry: &'a SymbolRegistry,
29 files: &'a ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
30 crate_name: &'a str,
31}
32
33impl<'a> DataFlowBuilderWorkspace<'a> {
34 pub fn new(
36 registry: &'a SymbolRegistry,
37 files: &'a ImHashMap<WorkspaceFilePath, Arc<PureFile>>,
38 crate_name: &'a str,
39 ) -> Self {
40 Self {
41 registry,
42 files,
43 crate_name,
44 }
45 }
46
47 pub fn build(self) -> DataFlowGraphV2 {
49 let estimated_vars = self.files.len() * 10;
50 let estimated_flows = self.files.len() * 5;
51 let mut graph = DataFlowGraphV2::with_capacity(estimated_vars, estimated_flows);
52
53 let resolver = SymbolPathResolver::new(self.crate_name);
54 for (path, file) in self.files {
55 let module_path_str = resolver.module_path_str(path);
58 let module_path = SymbolPath::parse(&module_path_str).ok();
59
60 let mut collector = FlowCollectorV2::new(module_path, self.registry, self.crate_name);
61 collector.visit_file(file);
62 collector.apply_to(&mut graph);
63 }
64
65 graph
66 }
67
68 #[deprecated(
80 since = "0.1.0",
81 note = "Use build_incremental_by_symbols() for symbol-based updates without file I/O."
82 )]
83 pub fn build_incremental(
84 self,
85 graph: &mut DataFlowGraphV2,
86 modified_files: &[WorkspaceFilePath],
87 ) {
88 let resolver = SymbolPathResolver::new(self.crate_name);
89
90 for path in modified_files {
91 if let Some(file) = self.files.get(path) {
92 let module_path_str = resolver.module_path_str(path);
93 let module_path = SymbolPath::parse(&module_path_str).ok();
94
95 let mut collector =
96 FlowCollectorV2::new(module_path, self.registry, self.crate_name);
97 collector.visit_file(file);
98 collector.apply_to(graph);
99 }
100 }
101 }
102
103 pub fn build_incremental_by_symbols(
114 &self,
115 graph: &mut DataFlowGraphV2,
116 ast_registry: &ASTRegistry,
117 affected_ids: &[SymbolId],
118 ) {
119 for &id in affected_ids {
120 let module_path = self.registry.resolve(id).and_then(|path| {
122 let path_str = path.to_string();
124 path_str
125 .rsplit_once("::")
126 .and_then(|(parent, _)| SymbolPath::parse(parent).ok())
127 });
128
129 if let Some(item) = ast_registry.get(id) {
131 let mut collector =
132 FlowCollectorV2::new(module_path, self.registry, self.crate_name);
133 collector.visit_item(item);
134 collector.apply_to(graph);
135 }
136 }
137 }
138}
139
140struct CollectedVarV2 {
142 data: VarData,
143 name: String,
144 temp_id: usize,
145 var_symbol_id: Option<SymbolId>,
147}
148
149struct CollectedFlowV2 {
151 from_id: usize,
152 to_id: usize,
153 data: FlowData,
154}
155
156struct FlowCollectorV2<'a> {
158 module_path: Option<SymbolPath>,
160 registry: &'a SymbolRegistry,
161 crate_name: String,
163 current_scope_path: Option<SymbolPath>,
165 current_symbol: Option<SymbolId>,
167 vars: Vec<CollectedVarV2>,
169 var_name_map: HashMap<String, usize>,
171 flows: Vec<CollectedFlowV2>,
173 collected_scopes: Vec<ScopeData>,
175 scope_stack: Vec<ScopeId>,
177 next_id: usize,
179 current_line: u32,
181 collected_locks: Vec<CollectedLockV2>,
183 guard_temp_ids: HashSet<usize>,
185 collected_field_accesses: Vec<CollectedFieldAccessV2>,
187 in_local_init: bool,
189}
190
191struct CollectedLockV2 {
193 lock_temp_id: usize,
195 guard_temp_id: usize,
197 lock_type: LockType,
199 line: u32,
201 is_try: bool,
203 lock_name: String,
205 guard_name: String,
207 owner_fn: Option<SymbolId>,
209}
210
211struct CollectedFieldAccessV2 {
213 guard_temp_id: usize,
215 field_name: String,
217 access_kind: AccessKind,
219}
220
221impl<'a> FlowCollectorV2<'a> {
222 fn new(
223 module_path: Option<SymbolPath>,
224 registry: &'a SymbolRegistry,
225 crate_name: &str,
226 ) -> Self {
227 Self {
228 module_path,
229 registry,
230 crate_name: crate_name.to_string(),
231 current_scope_path: None,
232 current_symbol: None,
233 vars: Vec::new(),
234 var_name_map: HashMap::new(),
235 flows: Vec::new(),
236 collected_scopes: Vec::new(),
237 scope_stack: Vec::new(),
238 next_id: 0,
239 current_line: 0,
240 collected_locks: Vec::new(),
241 guard_temp_ids: HashSet::new(),
242 collected_field_accesses: Vec::new(),
243 in_local_init: false,
244 }
245 }
246
247 fn apply_to(self, graph: &mut DataFlowGraphV2) {
249 for scope_data in self.collected_scopes {
251 graph.add_scope(scope_data);
252 }
253
254 let mut id_to_var: HashMap<usize, VarId> = HashMap::new();
255
256 for collected in self.vars {
258 let var_id = graph.add_var(collected.data, collected.name, collected.var_symbol_id);
259 id_to_var.insert(collected.temp_id, var_id);
260 }
261
262 for flow in self.flows {
264 if let (Some(&from_var), Some(&to_var)) =
265 (id_to_var.get(&flow.from_id), id_to_var.get(&flow.to_id))
266 {
267 graph.add_flow(from_var, to_var, flow.data);
268 }
269 }
270
271 for lock in self.collected_locks {
273 if let (Some(&lock_var), Some(&guard_var)) = (
274 id_to_var.get(&lock.lock_temp_id),
275 id_to_var.get(&lock.guard_temp_id),
276 ) {
277 let mut acq = LockAcquisitionV2::new(
278 lock_var,
279 guard_var,
280 lock.lock_type,
281 lock.line,
282 lock.lock_name,
283 lock.guard_name,
284 );
285 if lock.is_try {
286 acq = acq.with_try();
287 }
288 if let Some(owner) = lock.owner_fn {
289 acq = acq.with_owner_fn(owner);
290 }
291 graph.lock_tracker_mut().acquire(acq);
292 }
293 }
294
295 for fa in self.collected_field_accesses {
297 if let Some(&guard_var) = id_to_var.get(&fa.guard_temp_id) {
298 graph.lock_tracker_mut().record_field_access(
299 guard_var,
300 &fa.field_name,
301 fa.access_kind,
302 0, );
304 }
305 }
306
307 graph.lock_tracker_mut().flush_active_sections();
311 }
312
313 fn lookup_existing_var(&self, name: &str) -> Option<usize> {
317 self.var_name_map.get(name).copied()
318 }
319
320 fn lookup_or_create_var(&mut self, name: &str, default_kind: VarKind) -> Option<usize> {
326 if let Some(id) = self.lookup_existing_var(name) {
328 return Some(id);
329 }
330
331 self.get_or_create_var(name, default_kind)
333 }
334
335 fn get_or_create_var(&mut self, name: &str, kind: VarKind) -> Option<usize> {
340 let parent = self.current_symbol?;
342
343 if let Some(&id) = self.var_name_map.get(name) {
345 return Some(id);
346 }
347
348 let id = self.next_id;
350 self.next_id += 1;
351
352 let var_scope = match kind {
354 VarKind::Parameter => VarScope::Param,
355 VarKind::Field => VarScope::Field,
356 VarKind::Local | VarKind::Temp | VarKind::Return | VarKind::Static => VarScope::Local,
357 };
358
359 let var_symbol_id = self.lookup_var_symbol(parent, var_scope, name);
362
363 let data = VarData {
364 parent,
365 kind,
366 line: self.current_line,
367 is_mut: false,
368 };
369
370 self.vars.push(CollectedVarV2 {
371 data,
372 name: name.to_string(),
373 temp_id: id,
374 var_symbol_id,
375 });
376 self.var_name_map.insert(name.to_string(), id);
377 Some(id)
378 }
379
380 fn lookup_var_symbol(&self, parent: SymbolId, scope: VarScope, name: &str) -> Option<SymbolId> {
384 let parent_path = self.registry.resolve(parent)?;
386
387 let var_path = parent_path.with_var_scope(scope, name).ok()?;
389
390 self.registry.lookup(&var_path)
392 }
393
394 fn set_var_mut(&mut self, temp_id: usize) {
396 if let Some(var) = self.vars.iter_mut().find(|v| v.temp_id == temp_id) {
397 var.data.is_mut = true;
398 }
399 }
400
401 fn usage_sink(&mut self) -> Option<usize> {
407 self.get_or_create_var("$usage", VarKind::Temp)
408 }
409
410 fn track_call_args(&mut self, args: &[PureExpr]) {
420 let Some(sink) = self.usage_sink() else {
421 return;
422 };
423 for arg in args {
424 let (inner, kind) = match arg {
429 PureExpr::Ref {
430 is_mut: false,
431 expr: inner,
432 } => (inner.as_ref(), FlowKind::SharedBorrow),
433 PureExpr::Ref {
434 is_mut: true,
435 expr: inner,
436 } => (inner.as_ref(), FlowKind::MutBorrow),
437 other => (other, FlowKind::Argument),
438 };
439 let sources = self.collect_expr_sources(inner);
440 for source_id in sources {
441 self.add_flow(source_id, sink, kind);
442 }
443 }
444 }
445
446 fn track_receiver_read(&mut self, receiver: &PureExpr) {
448 let Some(sink) = self.usage_sink() else {
449 return;
450 };
451 let sources = self.collect_expr_sources(receiver);
452 for source_id in sources {
453 self.add_flow(source_id, sink, FlowKind::Read);
454 }
455 }
456
457 fn track_receiver_mut_borrow(&mut self, receiver: &PureExpr) {
459 let Some(sink) = self.usage_sink() else {
460 return;
461 };
462 let sources = self.collect_expr_sources(receiver);
463 for source_id in sources {
464 self.add_flow(source_id, sink, FlowKind::MutBorrow);
465 }
466 }
467
468 fn track_closure_captures(&mut self, body: &PureExpr, is_move: bool) {
475 let Some(sink) = self.usage_sink() else {
476 return;
477 };
478 let existing_vars: Vec<String> = self.var_name_map.keys().cloned().collect();
479 let mut ref_names = Vec::new();
481 Self::collect_all_referenced_names(body, &mut ref_names);
482 let kind = if is_move {
483 FlowKind::Move
484 } else {
485 FlowKind::Read
486 };
487 for name in ref_names {
488 if existing_vars.contains(&name) {
489 if let Some(&var_id) = self.var_name_map.get(&name) {
490 self.add_flow(var_id, sink, kind);
491 }
492 }
493 }
494 }
495
496 fn collect_all_referenced_names(expr: &PureExpr, names: &mut Vec<String>) {
501 match expr {
502 PureExpr::Path(name) => {
503 let simple = name.split("::").last().unwrap_or(name);
504 names.push(simple.to_string());
505 }
506 PureExpr::Field { expr, .. } => {
507 Self::collect_all_referenced_names(expr, names);
508 }
509 PureExpr::Binary { left, right, .. } => {
510 Self::collect_all_referenced_names(left, names);
511 Self::collect_all_referenced_names(right, names);
512 }
513 PureExpr::Unary { expr, .. }
514 | PureExpr::Ref { expr, .. }
515 | PureExpr::Await(expr)
516 | PureExpr::Try(expr)
517 | PureExpr::Cast { expr, .. } => {
518 Self::collect_all_referenced_names(expr, names);
519 }
520 PureExpr::Call { func, args } => {
521 Self::collect_all_referenced_names(func, names);
522 for arg in args {
523 Self::collect_all_referenced_names(arg, names);
524 }
525 }
526 PureExpr::MethodCall { receiver, args, .. } => {
527 Self::collect_all_referenced_names(receiver, names);
528 for arg in args {
529 Self::collect_all_referenced_names(arg, names);
530 }
531 }
532 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
533 for e in exprs {
534 Self::collect_all_referenced_names(e, names);
535 }
536 }
537 PureExpr::Struct { fields, .. } => {
538 for (_, e) in fields {
539 Self::collect_all_referenced_names(e, names);
540 }
541 }
542 PureExpr::Index { expr, index } => {
543 Self::collect_all_referenced_names(expr, names);
544 Self::collect_all_referenced_names(index, names);
545 }
546 PureExpr::Block { block, .. } | PureExpr::Unsafe(block) => {
547 Self::collect_all_referenced_names_block(block, names);
548 }
549 PureExpr::Async { body, .. } => {
550 Self::collect_all_referenced_names_block(body, names);
551 }
552 PureExpr::If {
553 cond,
554 then_branch,
555 else_branch,
556 } => {
557 Self::collect_all_referenced_names(cond, names);
558 Self::collect_all_referenced_names_block(then_branch, names);
559 if let Some(else_expr) = else_branch {
560 Self::collect_all_referenced_names(else_expr, names);
561 }
562 }
563 PureExpr::Match { expr, arms } => {
564 Self::collect_all_referenced_names(expr, names);
565 for arm in arms {
566 if let Some(guard) = &arm.guard {
567 Self::collect_all_referenced_names(guard, names);
568 }
569 Self::collect_all_referenced_names(&arm.body, names);
570 }
571 }
572 PureExpr::Loop { body, .. } => {
573 Self::collect_all_referenced_names_block(body, names);
574 }
575 PureExpr::While { cond, body, .. } => {
576 Self::collect_all_referenced_names(cond, names);
577 Self::collect_all_referenced_names_block(body, names);
578 }
579 PureExpr::For { expr, body, .. } => {
580 Self::collect_all_referenced_names(expr, names);
581 Self::collect_all_referenced_names_block(body, names);
582 }
583 PureExpr::Closure { body, .. } => {
584 Self::collect_all_referenced_names(body, names);
585 }
586 PureExpr::Return(Some(expr))
587 | PureExpr::Break {
588 expr: Some(expr), ..
589 } => {
590 Self::collect_all_referenced_names(expr, names);
591 }
592 PureExpr::Let { expr, .. } => {
593 Self::collect_all_referenced_names(expr, names);
594 }
595 _ => {} }
597 }
598
599 fn collect_all_referenced_names_block(block: &PureBlock, names: &mut Vec<String>) {
601 for stmt in &block.stmts {
602 match stmt {
603 PureStmt::Expr(e) | PureStmt::Semi(e) => {
604 Self::collect_all_referenced_names(e, names);
605 }
606 PureStmt::Local { init, .. } => {
607 if let Some(expr) = init {
608 Self::collect_all_referenced_names(expr, names);
609 }
610 }
611 PureStmt::Item(_) => {}
612 }
613 }
614 }
615
616 fn track_macro_var_refs(&mut self, tokens: &str) {
621 let Some(sink) = self.usage_sink() else {
622 return;
623 };
624 for token in tokens.split(|c: char| !c.is_alphanumeric() && c != '_') {
626 if token.is_empty() || token.starts_with(|c: char| c.is_ascii_digit()) {
627 continue;
628 }
629 if let Some(&var_id) = self.var_name_map.get(token) {
631 self.add_flow(var_id, sink, FlowKind::Read);
632 }
633 }
634 }
635
636 fn bind_pattern_vars(&mut self, pattern: &PurePattern, scrutinee_sources: &[usize]) {
641 match pattern {
642 PurePattern::Ident { name, .. } => {
643 if let Some(target_id) = self.get_or_create_var(name, VarKind::Local) {
644 for &source_id in scrutinee_sources {
645 self.add_flow(source_id, target_id, FlowKind::Assign);
646 }
647 }
648 }
649 PurePattern::Tuple(pats) | PurePattern::Slice(pats) | PurePattern::Or(pats) => {
650 for pat in pats {
651 self.bind_pattern_vars(pat, scrutinee_sources);
652 }
653 }
654 PurePattern::Struct { fields, .. } => {
655 for (_, pat) in fields {
656 self.bind_pattern_vars(pat, scrutinee_sources);
657 }
658 }
659 PurePattern::Ref { pattern, .. } => {
660 self.bind_pattern_vars(pattern, scrutinee_sources);
661 }
662 _ => {} }
664 }
665
666 fn track_return(&mut self, expr: &PureExpr) {
668 let Some(sink) = self.usage_sink() else {
669 return;
670 };
671 let sources = self.collect_expr_sources(expr);
672 for source_id in sources {
673 self.add_flow(source_id, sink, FlowKind::Return);
674 }
675 }
676
677 fn add_flow(&mut self, from_id: usize, to_id: usize, kind: FlowKind) {
679 let scope = self.current_scope();
680 self.flows.push(CollectedFlowV2 {
681 from_id,
682 to_id,
683 data: FlowData {
684 kind,
685 line: self.current_line,
686 scope,
687 },
688 });
689 }
690
691 fn current_scope(&self) -> ScopeId {
695 self.scope_stack
696 .last()
697 .copied()
698 .unwrap_or(ScopeId::from_raw(0))
699 }
700
701 fn push_scope(&mut self, kind: ScopeKind, guard: Option<Guard>) {
703 let parent = self.scope_stack.last().copied();
704 let id = ScopeId::from_raw(self.collected_scopes.len() as u32);
705 self.collected_scopes.push(ScopeData {
706 parent,
707 kind,
708 guard,
709 });
710 self.scope_stack.push(id);
711 }
712
713 fn pop_scope(&mut self) {
715 self.scope_stack.pop();
716 }
717
718 fn parse_guard(&mut self, cond: &PureExpr) -> Option<Guard> {
725 match cond {
726 PureExpr::Binary { op, left, right } => {
728 let (kind, left_expr, _right_expr) = match op.as_str() {
729 "<" => (GuardKind::LessThan, left, right),
730 "<=" => (GuardKind::LessEqual, left, right),
731 ">" => (GuardKind::LessThan, right, left),
733 ">=" => (GuardKind::LessEqual, right, left),
734 "&&" => {
736 return self.parse_guard(left).or_else(|| self.parse_guard(right));
738 }
739 _ => return None,
740 };
741 let var_name = extract_expr_name(left_expr)?;
743 Some(Guard {
744 kind,
745 var_names: smallvec![var_name],
746 })
747 }
748 PureExpr::MethodCall {
750 receiver, method, ..
751 } => {
752 let kind = match method.as_str() {
753 "is_some" => GuardKind::IsSome,
754 "is_ok" => GuardKind::IsOk,
755 _ => return None,
756 };
757 let var_name = extract_expr_name(receiver)?;
758 Some(Guard {
759 kind,
760 var_names: smallvec![var_name],
761 })
762 }
763 PureExpr::Let { pattern, expr } => {
765 let kind = match pattern {
766 PurePattern::Struct { path, .. } | PurePattern::Path(path) => {
767 if path == "Some" || path.ends_with("::Some") {
768 GuardKind::LetSome
769 } else if path == "Ok" || path.ends_with("::Ok") {
770 GuardKind::LetOk
771 } else {
772 return None;
773 }
774 }
775 _ => return None,
776 };
777 let var_name = extract_expr_name(expr)?;
778 Some(Guard {
779 kind,
780 var_names: smallvec![var_name],
781 })
782 }
783 _ => None,
784 }
785 }
786
787 fn parse_match_arm_guard(&self, _pattern: &PurePattern) -> Option<Guard> {
789 None
791 }
792
793 fn emit_conditional_flow(&mut self, cond: &PureExpr) {
795 let mut names = Vec::new();
796 FlowCollectorV2::collect_all_referenced_names(cond, &mut names);
797 let var_ids: Vec<usize> = names
799 .iter()
800 .filter_map(|name| self.lookup_existing_var(name))
801 .collect();
802 for &var_id in &var_ids {
805 self.add_flow(var_id, var_id, FlowKind::Conditional);
806 }
807 }
808
809 fn visit_file(&mut self, file: &PureFile) {
810 for item in &file.items {
811 self.visit_item(item);
812 }
813 }
814
815 fn visit_item(&mut self, item: &PureItem) {
816 match item {
817 PureItem::Fn(f) => {
818 let scope_path = self
820 .module_path
821 .as_ref()
822 .and_then(|mp| mp.child(&f.name).ok())
823 .unwrap_or_else(|| {
824 SymbolPath::parse(&format!("{}::unknown::{}", self.crate_name, f.name))
825 .unwrap_or_else(|_| {
826 SymbolPath::parse(&format!("{}::unknown", self.crate_name)).unwrap()
827 })
828 });
829
830 self.current_scope_path = Some(scope_path.clone());
831 self.current_line = 0;
832 self.var_name_map.clear();
834 self.guard_temp_ids.clear();
835
836 self.current_symbol = self.registry.lookup(&scope_path);
838
839 self.push_scope(ScopeKind::Function, None);
841
842 for param in &f.params {
844 if let Some(name) = extract_param_name(param) {
845 self.get_or_create_var(&name, VarKind::Parameter);
846 }
847 }
848
849 self.visit_block(&f.body);
850 self.pop_scope();
851 self.current_scope_path = None;
852 self.current_symbol = None;
853 }
854 PureItem::Impl(i) => {
855 for impl_item in &i.items {
856 self.visit_impl_item(impl_item, &i.self_ty, i.trait_.as_deref());
857 }
858 }
859 PureItem::Mod(m) => {
860 for item in &m.items {
861 self.visit_item(item);
862 }
863 }
864 PureItem::Static(_) | PureItem::Const(_) => {}
866 _ => {}
867 }
868 }
869
870 fn visit_impl_item(&mut self, item: &PureImplItem, self_ty: &str, trait_: Option<&str>) {
871 if let PureImplItem::Fn(f) = item {
872 let scope_path = self
876 .module_path
877 .as_ref()
878 .and_then(|mp| {
879 let method_base = if let Some(trait_name) = trait_ {
880 mp.child_trait_impl(trait_name, self_ty)
881 } else {
882 let base_type = self_ty.split('<').next().unwrap_or(self_ty).trim();
884 mp.child(base_type).ok()?
885 };
886 method_base.child(&f.name).ok()
887 })
888 .unwrap_or_else(|| {
889 SymbolPath::parse(&format!(
890 "{}::unknown::{}::{}",
891 self.crate_name, self_ty, f.name
892 ))
893 .unwrap_or_else(|_| {
894 SymbolPath::parse(&format!("{}::unknown", self.crate_name)).unwrap()
895 })
896 });
897
898 self.current_scope_path = Some(scope_path.clone());
899 self.current_line = 0;
900 self.var_name_map.clear();
902
903 self.current_symbol = self.registry.lookup(&scope_path);
905
906 self.push_scope(ScopeKind::Function, None);
908
909 for param in &f.params {
911 if let Some(name) = extract_param_name(param) {
912 if name != "self" {
913 self.get_or_create_var(&name, VarKind::Parameter);
914 }
915 }
916 }
917
918 self.visit_block(&f.body);
919 self.pop_scope();
920 self.current_scope_path = None;
921 self.current_symbol = None;
922 }
923 }
924
925 fn visit_block(&mut self, block: &PureBlock) {
926 for stmt in &block.stmts {
927 self.visit_stmt(stmt);
928 }
929 if let Some(PureStmt::Expr(expr)) = block.stmts.last() {
931 self.track_return(expr);
932 }
933 }
934
935 fn visit_stmt(&mut self, stmt: &PureStmt) {
936 match stmt {
937 PureStmt::Local { pattern, init, .. } => {
938 if let Some(name) = extract_pattern_name(pattern) {
939 if let Some(target_id) = self.get_or_create_var(&name, VarKind::Local) {
940 if matches!(pattern, PurePattern::Ident { is_mut: true, .. }) {
942 self.set_var_mut(target_id);
943 }
944 if let Some(expr) = init {
945 if let Some((lock_receiver, lock_type, is_try)) =
948 extract_lock_call(expr)
949 {
950 let lock_sources = self.collect_expr_sources(lock_receiver);
951 if let Some(&lock_source_id) = lock_sources.first() {
952 let lock_name = extract_expr_name(lock_receiver)
953 .unwrap_or_else(|| "?".to_string());
954 self.collected_locks.push(CollectedLockV2 {
955 lock_temp_id: lock_source_id,
956 guard_temp_id: target_id,
957 lock_type,
958 line: self.current_line,
959 is_try,
960 lock_name,
961 guard_name: name.clone(),
962 owner_fn: self.current_symbol,
963 });
964 self.guard_temp_ids.insert(target_id);
965 }
966 }
967 if let Some(clone_receiver) = is_clone_call(expr) {
969 let sources = self.collect_expr_sources(clone_receiver);
971 for source_id in sources {
972 self.add_flow(source_id, target_id, FlowKind::Clone);
973 }
974 } else if let Some((is_mut, inner)) = is_borrow_expr(expr) {
975 let kind = if is_mut {
977 FlowKind::MutBorrow
978 } else {
979 FlowKind::SharedBorrow
980 };
981 let sources = self.collect_expr_sources(inner);
982 for source_id in sources {
983 self.add_flow(source_id, target_id, kind);
984 }
985 } else {
986 let sources = self.collect_expr_sources(expr);
988 for source_id in sources {
989 self.add_flow(source_id, target_id, FlowKind::Assign);
990 }
991 }
992 self.in_local_init = true;
996 self.visit_expr_for_assignments(expr);
997 self.in_local_init = false;
998 }
999 }
1000 } else if let Some(expr) = init {
1001 self.in_local_init = true;
1005 self.visit_expr_for_assignments(expr);
1006 self.in_local_init = false;
1007 }
1008 }
1009 PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
1010 self.visit_expr_for_assignments(expr);
1011 }
1012 PureStmt::Item(item) => {
1013 self.visit_item(item);
1014 }
1015 }
1016 }
1017
1018 fn visit_expr_for_assignments(&mut self, expr: &PureExpr) {
1019 match expr {
1020 PureExpr::Binary { op, left, right } if op == "=" => {
1021 if let Some((name, kind)) = classify_lvalue(left) {
1022 if let Some(target_id) = self.get_or_create_var(&name, kind) {
1023 let sources = self.collect_expr_sources(right);
1024
1025 for source_id in sources {
1026 self.add_flow(source_id, target_id, FlowKind::Assign);
1027 }
1028 }
1029 }
1030 self.track_guard_field_access(left, AccessKind::Write);
1032 self.visit_expr_for_assignments(right);
1033 }
1034 PureExpr::Binary { left, right, .. } => {
1035 self.visit_expr_for_assignments(left);
1036 self.visit_expr_for_assignments(right);
1037 }
1038 PureExpr::Block { block, .. } => {
1039 self.push_scope(ScopeKind::Block, None);
1040 self.visit_block(block);
1041 self.pop_scope();
1042 }
1043 PureExpr::If {
1044 cond,
1045 then_branch,
1046 else_branch,
1047 } => {
1048 self.visit_expr_for_assignments(cond);
1049 self.emit_conditional_flow(cond);
1051 let guard = self.parse_guard(cond);
1053 self.push_scope(ScopeKind::IfThen, guard);
1054 self.visit_block(then_branch);
1055 self.pop_scope();
1056 if let Some(else_expr) = else_branch {
1057 self.push_scope(ScopeKind::IfElse, None);
1059 self.visit_expr_for_assignments(else_expr);
1060 self.pop_scope();
1061 }
1062 }
1063 PureExpr::Match { expr, arms } => {
1064 self.visit_expr_for_assignments(expr);
1065 self.track_receiver_read(expr);
1067 let scrutinee_sources = self.collect_expr_sources(expr);
1069 for arm in arms {
1070 let arm_guard = self.parse_match_arm_guard(&arm.pattern);
1072 self.push_scope(ScopeKind::MatchArm, arm_guard);
1073 self.bind_pattern_vars(&arm.pattern, &scrutinee_sources);
1075 if let Some(guard) = &arm.guard {
1076 self.visit_expr_for_assignments(guard);
1077 }
1078 self.visit_expr_for_assignments(&arm.body);
1079 self.pop_scope();
1080 }
1081 }
1082 PureExpr::Loop { body: block, .. } => {
1083 self.push_scope(ScopeKind::LoopBody, None);
1084 self.visit_block(block);
1085 self.pop_scope();
1086 }
1087 PureExpr::While { cond, body, .. } => {
1088 self.visit_expr_for_assignments(cond);
1089 self.track_receiver_read(cond);
1090 self.emit_conditional_flow(cond);
1091 let guard = self.parse_guard(cond);
1092 self.push_scope(ScopeKind::WhileBody, guard);
1093 self.visit_block(body);
1094 self.pop_scope();
1095 }
1096 PureExpr::For {
1097 pat, expr, body, ..
1098 } => {
1099 self.visit_expr_for_assignments(expr);
1100 if let Some(name) = extract_pattern_name(pat) {
1102 if let Some(target_id) = self.get_or_create_var(&name, VarKind::Local) {
1103 let sources = self.collect_expr_sources(expr);
1104 for source_id in sources {
1105 self.add_flow(source_id, target_id, FlowKind::Assign);
1106 }
1107 }
1108 }
1109 self.track_receiver_read(expr);
1111 self.push_scope(ScopeKind::ForBody, None);
1113 self.visit_block(body);
1114 self.pop_scope();
1115 }
1116 PureExpr::Closure { body, is_move, .. } => {
1117 self.track_closure_captures(body, *is_move);
1119 self.push_scope(ScopeKind::Closure, None);
1120 self.visit_expr_for_assignments(body);
1121 self.pop_scope();
1122 }
1123 PureExpr::Call { func, args } => {
1124 self.visit_expr_for_assignments(func);
1125 for arg in args {
1126 self.visit_expr_for_assignments(arg);
1127 }
1128 self.track_call_args(args);
1130 }
1131 PureExpr::MethodCall {
1132 receiver,
1133 method,
1134 args,
1135 ..
1136 } => {
1137 self.visit_expr_for_assignments(receiver);
1138 for arg in args {
1139 self.visit_expr_for_assignments(arg);
1140 }
1141 if !self.in_local_init {
1145 if let Some((lock_receiver, lock_type, is_try)) = extract_lock_call(expr) {
1146 let lock_sources = self.collect_expr_sources(lock_receiver);
1147 if let Some(&lock_source_id) = lock_sources.first() {
1148 let lock_name =
1149 extract_expr_name(lock_receiver).unwrap_or_else(|| "?".to_string());
1150 let guard_name = format!("_anon_guard_{}", self.current_line);
1151 if let Some(guard_id) =
1152 self.get_or_create_var(&guard_name, VarKind::Temp)
1153 {
1154 self.collected_locks.push(CollectedLockV2 {
1155 lock_temp_id: lock_source_id,
1156 guard_temp_id: guard_id,
1157 lock_type,
1158 line: self.current_line,
1159 is_try,
1160 lock_name,
1161 guard_name,
1162 owner_fn: self.current_symbol,
1163 });
1164 self.guard_temp_ids.insert(guard_id);
1165 }
1166 }
1167 }
1168 }
1169 if method.ends_with("_mut") {
1181 self.track_receiver_mut_borrow(receiver);
1182 } else {
1183 self.track_receiver_read(receiver);
1184 }
1185 self.track_call_args(args);
1186 }
1187 PureExpr::Return(Some(expr)) => {
1188 self.visit_expr_for_assignments(expr);
1189 self.track_return(expr);
1190 }
1191 PureExpr::Return(None) => {}
1192 PureExpr::Break {
1193 expr: Some(expr), ..
1194 } => {
1195 self.visit_expr_for_assignments(expr);
1196 self.track_return(expr); }
1198 PureExpr::Struct { fields, .. } => {
1199 for (_, value_expr) in fields {
1201 self.visit_expr_for_assignments(value_expr);
1202 self.track_receiver_read(value_expr);
1203 }
1204 }
1205 PureExpr::Async { is_move, body } => {
1206 if *is_move {
1207 let body_expr = PureExpr::Block {
1208 label: None,
1209 block: body.clone(),
1210 };
1211 self.track_closure_captures(&body_expr, true);
1212 }
1213 self.push_scope(ScopeKind::Block, None);
1214 self.visit_block(body);
1215 self.pop_scope();
1216 }
1217 PureExpr::Macro { tokens, .. } => {
1218 self.track_macro_var_refs(tokens);
1220 }
1221 PureExpr::Index { expr, index } => {
1222 self.visit_expr_for_assignments(expr);
1224 self.visit_expr_for_assignments(index);
1225 if let Some(index_name) = extract_expr_name(index) {
1227 if let Some(index_id) = self.lookup_existing_var(&index_name) {
1228 if let Some(coll_name) = extract_expr_name(expr) {
1229 if let Some(coll_id) = self.lookup_existing_var(&coll_name) {
1230 self.add_flow(index_id, coll_id, FlowKind::Index);
1231 }
1232 }
1233 }
1234 }
1235 }
1236 PureExpr::Let { expr, .. } => {
1237 self.visit_expr_for_assignments(expr);
1239 self.track_receiver_read(expr);
1240 }
1241 PureExpr::Field { .. } => {
1242 self.track_guard_field_access(expr, AccessKind::Read);
1244 }
1245 _ => {}
1246 }
1247 }
1248
1249 fn track_guard_field_access(&mut self, expr: &PureExpr, access_kind: AccessKind) {
1254 if let PureExpr::Field {
1255 expr: receiver,
1256 field,
1257 } = expr
1258 {
1259 if let PureExpr::Path(name) = &**receiver {
1260 if let Some(&guard_id) = self.var_name_map.get(name.as_str()) {
1261 if self.guard_temp_ids.contains(&guard_id) {
1262 self.collected_field_accesses.push(CollectedFieldAccessV2 {
1263 guard_temp_id: guard_id,
1264 field_name: field.clone(),
1265 access_kind,
1266 });
1267 }
1268 }
1269 }
1270 }
1271 }
1272
1273 fn collect_expr_sources(&mut self, expr: &PureExpr) -> Vec<usize> {
1274 let mut sources = Vec::new();
1275 self.collect_sources_recursive(expr, &mut sources);
1276 sources
1277 }
1278
1279 fn collect_sources_recursive(&mut self, expr: &PureExpr, sources: &mut Vec<usize>) {
1280 match expr {
1281 PureExpr::Path(name) => {
1282 let simple_name = name.split("::").last().unwrap_or(name);
1283 if let Some(id) = self.lookup_or_create_var(simple_name, VarKind::Local) {
1285 sources.push(id);
1286 }
1287 }
1288 PureExpr::Field { expr, field } => {
1289 if is_self_expr(expr) {
1290 if let Some(id) = self.get_or_create_var(field, VarKind::Field) {
1292 sources.push(id);
1293 }
1294 } else {
1295 self.collect_sources_recursive(expr, sources);
1296 }
1297 }
1298 PureExpr::Binary { left, right, .. } => {
1299 self.collect_sources_recursive(left, sources);
1300 self.collect_sources_recursive(right, sources);
1301 }
1302 PureExpr::Unary { expr, .. } => {
1303 self.collect_sources_recursive(expr, sources);
1304 }
1305 PureExpr::Call { func, args } => {
1306 for arg in args {
1307 self.collect_sources_recursive(arg, sources);
1308 }
1309 self.collect_sources_recursive(func, sources);
1310 }
1311 PureExpr::MethodCall { receiver, args, .. } => {
1312 self.collect_sources_recursive(receiver, sources);
1313 for arg in args {
1314 self.collect_sources_recursive(arg, sources);
1315 }
1316 }
1317 PureExpr::If {
1318 cond,
1319 then_branch,
1320 else_branch,
1321 } => {
1322 self.collect_sources_recursive(cond, sources);
1323 if let Some(PureStmt::Expr(e)) = then_branch.stmts.last() {
1324 self.collect_sources_recursive(e, sources);
1325 }
1326 if let Some(else_expr) = else_branch {
1327 self.collect_sources_recursive(else_expr, sources);
1328 }
1329 }
1330 PureExpr::Block { block, .. } => {
1331 if let Some(PureStmt::Expr(e)) = block.stmts.last() {
1332 self.collect_sources_recursive(e, sources);
1333 }
1334 }
1335 PureExpr::Match { expr, arms } => {
1336 self.collect_sources_recursive(expr, sources);
1337 for arm in arms {
1338 self.collect_sources_recursive(&arm.body, sources);
1339 }
1340 }
1341 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1342 for e in exprs {
1343 self.collect_sources_recursive(e, sources);
1344 }
1345 }
1346 PureExpr::Index { expr, index } => {
1347 self.collect_sources_recursive(expr, sources);
1348 self.collect_sources_recursive(index, sources);
1349 }
1350 PureExpr::Ref { expr, .. } | PureExpr::Await(expr) | PureExpr::Try(expr) => {
1351 self.collect_sources_recursive(expr, sources);
1352 }
1353 PureExpr::Struct { fields, .. } => {
1354 for (_, e) in fields {
1355 self.collect_sources_recursive(e, sources);
1356 }
1357 }
1358 _ => {}
1359 }
1360 }
1361}
1362
1363fn extract_param_name(param: &PureParam) -> Option<String> {
1364 match param {
1365 PureParam::SelfValue { .. } => Some("self".to_string()),
1366 PureParam::Typed { name, .. } => Some(name.clone()),
1367 }
1368}
1369
1370fn extract_pattern_name(pat: &PurePattern) -> Option<String> {
1371 match pat {
1372 PurePattern::Ident { name, .. } => Some(name.clone()),
1373 PurePattern::Ref { pattern, .. } => extract_pattern_name(pattern),
1374 _ => None,
1375 }
1376}
1377
1378fn extract_expr_name(expr: &PureExpr) -> Option<String> {
1382 match expr {
1383 PureExpr::Path(name) => Some(name.clone()),
1384 PureExpr::Field { expr, field } if is_self_expr(expr) => Some(field.clone()),
1385 PureExpr::Field { expr, field } => {
1386 if let Some(receiver_name) = extract_expr_name(expr) {
1388 Some(format!("{}.{}", receiver_name, field))
1389 } else {
1390 Some(field.clone())
1391 }
1392 }
1393 _ => None,
1394 }
1395}
1396
1397fn classify_lvalue(expr: &PureExpr) -> Option<(String, VarKind)> {
1398 match expr {
1399 PureExpr::Path(name) => Some((name.clone(), VarKind::Local)),
1400 PureExpr::Field { expr, field } => {
1401 if is_self_expr(expr) {
1402 Some((field.clone(), VarKind::Field))
1404 } else {
1405 None
1406 }
1407 }
1408 _ => None,
1409 }
1410}
1411
1412fn is_self_expr(expr: &PureExpr) -> bool {
1413 matches!(expr, PureExpr::Path(name) if name == "self")
1414}
1415
1416fn is_borrow_expr(expr: &PureExpr) -> Option<(bool, &PureExpr)> {
1420 match expr {
1421 PureExpr::Ref { is_mut, expr } => Some((*is_mut, expr)),
1422 _ => None,
1423 }
1424}
1425
1426fn is_clone_call(expr: &PureExpr) -> Option<&PureExpr> {
1430 match expr {
1431 PureExpr::MethodCall {
1432 receiver,
1433 method,
1434 args,
1435 ..
1436 } if method == "clone" && args.is_empty() => Some(receiver),
1437 _ => None,
1438 }
1439}
1440
1441fn classify_lock_method(method: &str) -> Option<(LockType, bool)> {
1445 match method {
1446 "lock" => Some((LockType::Mutex, false)),
1447 "try_lock" => Some((LockType::Mutex, true)),
1448 "read" => Some((LockType::RwLockRead, false)),
1449 "try_read" => Some((LockType::RwLockRead, true)),
1450 "write" => Some((LockType::RwLockWrite, false)),
1451 "try_write" => Some((LockType::RwLockWrite, true)),
1452 "borrow" => Some((LockType::RefCell, false)),
1453 "borrow_mut" => Some((LockType::RefCellMut, false)),
1454 _ => None,
1455 }
1456}
1457
1458fn extract_lock_call(expr: &PureExpr) -> Option<(&PureExpr, LockType, bool)> {
1468 match expr {
1469 PureExpr::MethodCall {
1470 receiver,
1471 method,
1472 args,
1473 ..
1474 } => {
1475 if args.is_empty() {
1477 if let Some((lock_type, is_try)) = classify_lock_method(method) {
1478 return Some((receiver, lock_type, is_try));
1479 }
1480 }
1481 if args.is_empty() {
1484 return extract_lock_call(receiver);
1485 }
1486 None
1487 }
1488 PureExpr::Await(inner) => extract_lock_call(inner),
1490 PureExpr::Try(inner) => extract_lock_call(inner),
1492 _ => None,
1493 }
1494}
1495
1496#[cfg(test)]
1497mod tests {
1498 use super::*;
1499 use crate::symbol::SymbolKind;
1500 use ryo_source::pure::{
1501 MacroDelimiter, PureFn, PureGenerics, PureImpl, PureImplItem, PureMatchArm, PureVis,
1502 };
1503
1504 fn build_graph_for_fn(fn_def: PureFn) -> DataFlowGraphV2 {
1509 let mut registry = SymbolRegistry::new();
1510 let module_path = SymbolPath::parse("test_crate").unwrap();
1511 let fn_path = module_path.child(&fn_def.name).unwrap();
1512 registry.register(fn_path, SymbolKind::Function).unwrap();
1513
1514 let file = PureFile {
1515 attrs: vec![],
1516 items: vec![PureItem::Fn(fn_def)],
1517 };
1518
1519 let mut graph = DataFlowGraphV2::new();
1520 let mut collector = FlowCollectorV2::new(Some(module_path), ®istry, "test_crate");
1521 collector.visit_file(&file);
1522 collector.apply_to(&mut graph);
1523 graph
1524 }
1525
1526 fn make_fn(name: &str, stmts: Vec<PureStmt>) -> PureFn {
1527 PureFn {
1528 attrs: vec![],
1529 vis: PureVis::Public,
1530 is_async: false,
1531 is_async_inferred: false,
1532 is_const: false,
1533 is_unsafe: false,
1534 abi: None,
1535 name: name.to_string(),
1536 generics: PureGenerics::default(),
1537 params: vec![],
1538 ret: None,
1539 body: PureBlock { stmts },
1540 }
1541 }
1542
1543 fn find_var(graph: &DataFlowGraphV2, name: &str) -> Option<VarId> {
1544 graph
1545 .iter_vars()
1546 .find(|(vid, _)| graph.var_name(*vid) == Some(name))
1547 .map(|(vid, _)| vid)
1548 }
1549
1550 fn outgoing_kinds(graph: &DataFlowGraphV2, var: VarId) -> Vec<FlowKind> {
1551 graph
1552 .outgoing(var)
1553 .iter()
1554 .filter_map(|&fid| graph.flow(fid).map(|f| f.kind))
1555 .collect()
1556 }
1557
1558 #[test]
1561 fn test_call_arg_generates_argument_flow() {
1562 let stmts = vec![
1564 PureStmt::Local {
1565 pattern: PurePattern::Ident {
1566 name: "x".into(),
1567 is_mut: false,
1568 },
1569 ty: None,
1570 init: Some(PureExpr::Lit("1".into())),
1571 },
1572 PureStmt::Semi(PureExpr::Call {
1573 func: Box::new(PureExpr::Path("foo".into())),
1574 args: vec![PureExpr::Path("x".into())],
1575 }),
1576 ];
1577
1578 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1579 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1580 let kinds = outgoing_kinds(&graph, x);
1581
1582 assert!(
1583 kinds.contains(&FlowKind::Argument),
1584 "x passed to foo() should have Argument flow, got: {:?}",
1585 kinds
1586 );
1587 }
1588
1589 #[test]
1592 fn test_method_receiver_generates_read_flow() {
1593 let stmts = vec![
1595 PureStmt::Local {
1596 pattern: PurePattern::Ident {
1597 name: "x".into(),
1598 is_mut: false,
1599 },
1600 ty: None,
1601 init: Some(PureExpr::Lit("1".into())),
1602 },
1603 PureStmt::Semi(PureExpr::MethodCall {
1604 receiver: Box::new(PureExpr::Path("x".into())),
1605 method: "push".into(),
1606 turbofish: None,
1607 args: vec![PureExpr::Lit("1".into())],
1608 }),
1609 ];
1610
1611 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1612 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1613 let kinds = outgoing_kinds(&graph, x);
1614
1615 assert!(
1616 kinds.contains(&FlowKind::Read),
1617 "x used as method receiver should have Read flow, got: {:?}",
1618 kinds
1619 );
1620 }
1621
1622 #[test]
1625 fn test_return_generates_return_flow() {
1626 let stmts = vec![
1628 PureStmt::Local {
1629 pattern: PurePattern::Ident {
1630 name: "x".into(),
1631 is_mut: false,
1632 },
1633 ty: None,
1634 init: Some(PureExpr::Lit("1".into())),
1635 },
1636 PureStmt::Semi(PureExpr::Return(Some(Box::new(PureExpr::Path("x".into()))))),
1637 ];
1638
1639 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1640 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1641 let kinds = outgoing_kinds(&graph, x);
1642
1643 assert!(
1644 kinds.contains(&FlowKind::Return),
1645 "x in return expression should have Return flow, got: {:?}",
1646 kinds
1647 );
1648 }
1649
1650 #[test]
1653 fn test_shared_borrow_generates_borrow_flow() {
1654 let stmts = vec![
1656 PureStmt::Local {
1657 pattern: PurePattern::Ident {
1658 name: "x".into(),
1659 is_mut: false,
1660 },
1661 ty: None,
1662 init: Some(PureExpr::Lit("1".into())),
1663 },
1664 PureStmt::Local {
1665 pattern: PurePattern::Ident {
1666 name: "y".into(),
1667 is_mut: false,
1668 },
1669 ty: None,
1670 init: Some(PureExpr::Ref {
1671 is_mut: false,
1672 expr: Box::new(PureExpr::Path("x".into())),
1673 }),
1674 },
1675 ];
1676
1677 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1678 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1679 let kinds = outgoing_kinds(&graph, x);
1680
1681 assert!(
1682 kinds.contains(&FlowKind::SharedBorrow),
1683 "x in &x should have SharedBorrow flow, got: {:?}",
1684 kinds
1685 );
1686 }
1687
1688 #[test]
1689 fn test_mut_borrow_generates_mut_borrow_flow() {
1690 let stmts = vec![
1692 PureStmt::Local {
1693 pattern: PurePattern::Ident {
1694 name: "x".into(),
1695 is_mut: true,
1696 },
1697 ty: None,
1698 init: Some(PureExpr::Lit("1".into())),
1699 },
1700 PureStmt::Local {
1701 pattern: PurePattern::Ident {
1702 name: "y".into(),
1703 is_mut: false,
1704 },
1705 ty: None,
1706 init: Some(PureExpr::Ref {
1707 is_mut: true,
1708 expr: Box::new(PureExpr::Path("x".into())),
1709 }),
1710 },
1711 ];
1712
1713 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1714 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1715 let kinds = outgoing_kinds(&graph, x);
1716
1717 assert!(
1718 kinds.contains(&FlowKind::MutBorrow),
1719 "x in &mut x should have MutBorrow flow, got: {:?}",
1720 kinds
1721 );
1722 }
1723
1724 #[test]
1727 fn test_method_call_arg_generates_argument_flow() {
1728 let stmts = vec![
1730 PureStmt::Local {
1731 pattern: PurePattern::Ident {
1732 name: "x".into(),
1733 is_mut: false,
1734 },
1735 ty: None,
1736 init: Some(PureExpr::Lit("1".into())),
1737 },
1738 PureStmt::Local {
1739 pattern: PurePattern::Ident {
1740 name: "y".into(),
1741 is_mut: false,
1742 },
1743 ty: None,
1744 init: Some(PureExpr::Lit("2".into())),
1745 },
1746 PureStmt::Semi(PureExpr::MethodCall {
1747 receiver: Box::new(PureExpr::Path("y".into())),
1748 method: "foo".into(),
1749 turbofish: None,
1750 args: vec![PureExpr::Path("x".into())],
1751 }),
1752 ];
1753
1754 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1755 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1756 let kinds = outgoing_kinds(&graph, x);
1757
1758 assert!(
1759 kinds.contains(&FlowKind::Argument),
1760 "x passed as method arg should have Argument flow, got: {:?}",
1761 kinds
1762 );
1763 }
1764
1765 #[test]
1768 fn test_for_loop_creates_assign_to_pattern_var() {
1769 let stmts = vec![
1771 PureStmt::Local {
1772 pattern: PurePattern::Ident {
1773 name: "items".into(),
1774 is_mut: false,
1775 },
1776 ty: None,
1777 init: Some(PureExpr::Lit("1".into())),
1778 },
1779 PureStmt::Semi(PureExpr::For {
1780 label: None,
1781 pat: PurePattern::Ident {
1782 name: "item".into(),
1783 is_mut: false,
1784 },
1785 expr: Box::new(PureExpr::Path("items".into())),
1786 body: PureBlock { stmts: vec![] },
1787 }),
1788 ];
1789
1790 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1791 let items = find_var(&graph, "items").expect("variable 'items' should exist");
1792 let kinds = outgoing_kinds(&graph, items);
1793
1794 assert!(
1795 kinds.contains(&FlowKind::Read),
1796 "items used as for-loop iterator should have Read flow, got: {:?}",
1797 kinds
1798 );
1799
1800 let item = find_var(&graph, "item").expect("loop variable 'item' should exist");
1801 let incoming: Vec<FlowKind> = graph
1802 .incoming(item)
1803 .iter()
1804 .filter_map(|&fid| graph.flow(fid).map(|f| f.kind))
1805 .collect();
1806 assert!(
1807 incoming.contains(&FlowKind::Assign),
1808 "item should have incoming Assign from iterator, got: {:?}",
1809 incoming
1810 );
1811 }
1812
1813 #[test]
1816 fn test_break_with_value_generates_read_flow() {
1817 let stmts = vec![
1819 PureStmt::Local {
1820 pattern: PurePattern::Ident {
1821 name: "x".into(),
1822 is_mut: false,
1823 },
1824 ty: None,
1825 init: Some(PureExpr::Lit("1".into())),
1826 },
1827 PureStmt::Semi(PureExpr::Loop {
1828 label: None,
1829 body: PureBlock {
1830 stmts: vec![PureStmt::Semi(PureExpr::Break {
1831 label: None,
1832 expr: Some(Box::new(PureExpr::Path("x".into()))),
1833 })],
1834 },
1835 }),
1836 ];
1837
1838 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1839 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1840 let kinds = outgoing_kinds(&graph, x);
1841
1842 assert!(
1843 !kinds.is_empty(),
1844 "x in break expression should have outgoing flow, got: {:?}",
1845 kinds
1846 );
1847 }
1848
1849 #[test]
1852 fn test_closure_capture_generates_flow() {
1853 let stmts = vec![
1855 PureStmt::Local {
1856 pattern: PurePattern::Ident {
1857 name: "x".into(),
1858 is_mut: false,
1859 },
1860 ty: None,
1861 init: Some(PureExpr::Lit("1".into())),
1862 },
1863 PureStmt::Local {
1864 pattern: PurePattern::Ident {
1865 name: "f".into(),
1866 is_mut: false,
1867 },
1868 ty: None,
1869 init: Some(PureExpr::Closure {
1870 is_async: false,
1871 is_move: false,
1872 params: vec![],
1873 ret: None,
1874 body: Box::new(PureExpr::Binary {
1875 op: "+".into(),
1876 left: Box::new(PureExpr::Path("x".into())),
1877 right: Box::new(PureExpr::Lit("1".into())),
1878 }),
1879 }),
1880 },
1881 ];
1882
1883 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1884 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1885 let kinds = outgoing_kinds(&graph, x);
1886
1887 assert!(
1888 !kinds.is_empty(),
1889 "x captured by closure should have outgoing flow, got: {:?}",
1890 kinds
1891 );
1892 }
1893
1894 #[test]
1895 fn test_move_closure_capture_generates_move_flow() {
1896 let stmts = vec![
1898 PureStmt::Local {
1899 pattern: PurePattern::Ident {
1900 name: "x".into(),
1901 is_mut: false,
1902 },
1903 ty: None,
1904 init: Some(PureExpr::Lit("1".into())),
1905 },
1906 PureStmt::Local {
1907 pattern: PurePattern::Ident {
1908 name: "f".into(),
1909 is_mut: false,
1910 },
1911 ty: None,
1912 init: Some(PureExpr::Closure {
1913 is_async: false,
1914 is_move: true,
1915 params: vec![],
1916 ret: None,
1917 body: Box::new(PureExpr::Binary {
1918 op: "+".into(),
1919 left: Box::new(PureExpr::Path("x".into())),
1920 right: Box::new(PureExpr::Lit("1".into())),
1921 }),
1922 }),
1923 },
1924 ];
1925
1926 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1927 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1928 let kinds = outgoing_kinds(&graph, x);
1929
1930 assert!(
1931 kinds.contains(&FlowKind::Move),
1932 "x captured by move closure should have Move flow, got: {:?}",
1933 kinds
1934 );
1935 }
1936
1937 #[test]
1940 fn test_match_arm_pattern_creates_variable_with_assign() {
1941 let stmts = vec![
1944 PureStmt::Local {
1945 pattern: PurePattern::Ident {
1946 name: "x".into(),
1947 is_mut: false,
1948 },
1949 ty: None,
1950 init: Some(PureExpr::Lit("1".into())),
1951 },
1952 PureStmt::Semi(PureExpr::Match {
1953 expr: Box::new(PureExpr::Path("x".into())),
1954 arms: vec![PureMatchArm {
1955 pattern: PurePattern::Ident {
1956 name: "val".into(),
1957 is_mut: false,
1958 },
1959 guard: None,
1960 body: PureExpr::Block {
1961 label: None,
1962 block: PureBlock { stmts: vec![] },
1963 },
1964 }],
1965 }),
1966 ];
1967
1968 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1969 let val = find_var(&graph, "val").expect("match arm variable 'val' should exist");
1970 let incoming: Vec<FlowKind> = graph
1971 .incoming(val)
1972 .iter()
1973 .filter_map(|&fid| graph.flow(fid).map(|f| f.kind))
1974 .collect();
1975 assert!(
1976 incoming.contains(&FlowKind::Assign),
1977 "val should have incoming Assign from match scrutinee, got: {:?}",
1978 incoming
1979 );
1980 }
1981
1982 #[test]
1985 fn test_match_scrutinee_generates_read_flow() {
1986 let stmts = vec![
1989 PureStmt::Local {
1990 pattern: PurePattern::Ident {
1991 name: "x".into(),
1992 is_mut: false,
1993 },
1994 ty: None,
1995 init: Some(PureExpr::Lit("1".into())),
1996 },
1997 PureStmt::Semi(PureExpr::Match {
1998 expr: Box::new(PureExpr::Path("x".into())),
1999 arms: vec![PureMatchArm {
2000 pattern: PurePattern::Wild,
2001 guard: None,
2002 body: PureExpr::Block {
2003 label: None,
2004 block: PureBlock { stmts: vec![] },
2005 },
2006 }],
2007 }),
2008 ];
2009
2010 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2011 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2012 let kinds = outgoing_kinds(&graph, x);
2013
2014 assert!(
2015 !kinds.is_empty(),
2016 "x used as match scrutinee should have outgoing flow, got: {:?}",
2017 kinds
2018 );
2019 }
2020
2021 #[test]
2024 fn test_match_expr_as_init_collects_arm_body_sources() {
2025 let stmts = vec![
2028 PureStmt::Local {
2029 pattern: PurePattern::Ident {
2030 name: "a".into(),
2031 is_mut: false,
2032 },
2033 ty: None,
2034 init: Some(PureExpr::Lit("1".into())),
2035 },
2036 PureStmt::Local {
2037 pattern: PurePattern::Ident {
2038 name: "b".into(),
2039 is_mut: false,
2040 },
2041 ty: None,
2042 init: Some(PureExpr::Lit("2".into())),
2043 },
2044 PureStmt::Local {
2045 pattern: PurePattern::Ident {
2046 name: "result".into(),
2047 is_mut: false,
2048 },
2049 ty: None,
2050 init: Some(PureExpr::Match {
2051 expr: Box::new(PureExpr::Lit("0".into())),
2052 arms: vec![
2053 PureMatchArm {
2054 pattern: PurePattern::Lit("1".into()),
2055 guard: None,
2056 body: PureExpr::Path("a".into()),
2057 },
2058 PureMatchArm {
2059 pattern: PurePattern::Wild,
2060 guard: None,
2061 body: PureExpr::Path("b".into()),
2062 },
2063 ],
2064 }),
2065 },
2066 ];
2067
2068 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2069 let result = find_var(&graph, "result").expect("variable 'result' should exist");
2070 let incoming: Vec<FlowKind> = graph
2071 .incoming(result)
2072 .iter()
2073 .filter_map(|&fid| graph.flow(fid).map(|f| f.kind))
2074 .collect();
2075 assert!(
2076 incoming.len() >= 2,
2077 "result should have incoming Assign from both arms, got: {:?}",
2078 incoming
2079 );
2080 }
2081
2082 #[test]
2085 fn test_struct_expr_field_values_generate_read_flow() {
2086 let stmts = vec![
2089 PureStmt::Local {
2090 pattern: PurePattern::Ident {
2091 name: "x".into(),
2092 is_mut: false,
2093 },
2094 ty: None,
2095 init: Some(PureExpr::Lit("1".into())),
2096 },
2097 PureStmt::Local {
2098 pattern: PurePattern::Ident {
2099 name: "y".into(),
2100 is_mut: false,
2101 },
2102 ty: None,
2103 init: Some(PureExpr::Lit("2".into())),
2104 },
2105 PureStmt::Local {
2106 pattern: PurePattern::Ident {
2107 name: "s".into(),
2108 is_mut: false,
2109 },
2110 ty: None,
2111 init: Some(PureExpr::Struct {
2112 path: "Foo".into(),
2113 fields: vec![
2114 ("a".into(), PureExpr::Path("x".into())),
2115 ("b".into(), PureExpr::Path("y".into())),
2116 ],
2117 }),
2118 },
2119 ];
2120
2121 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2122 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2123 let y = find_var(&graph, "y").expect("variable 'y' should exist");
2124
2125 assert!(
2126 !outgoing_kinds(&graph, x).is_empty(),
2127 "x used in struct field should have outgoing flow, got: {:?}",
2128 outgoing_kinds(&graph, x)
2129 );
2130 assert!(
2131 !outgoing_kinds(&graph, y).is_empty(),
2132 "y used in struct field should have outgoing flow, got: {:?}",
2133 outgoing_kinds(&graph, y)
2134 );
2135 }
2136
2137 #[test]
2140 fn test_struct_expr_as_stmt_generates_read_flow() {
2141 let stmts = vec![
2144 PureStmt::Local {
2145 pattern: PurePattern::Ident {
2146 name: "x".into(),
2147 is_mut: false,
2148 },
2149 ty: None,
2150 init: Some(PureExpr::Lit("1".into())),
2151 },
2152 PureStmt::Semi(PureExpr::Struct {
2153 path: "Foo".into(),
2154 fields: vec![("a".into(), PureExpr::Path("x".into()))],
2155 }),
2156 ];
2157
2158 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2159 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2160 let kinds = outgoing_kinds(&graph, x);
2161
2162 assert!(
2163 !kinds.is_empty(),
2164 "x used in struct expr statement should have outgoing flow, got: {:?}",
2165 kinds
2166 );
2167 }
2168
2169 #[test]
2172 fn test_field_assign_rhs_generates_read_flow() {
2173 let stmts = vec![
2176 PureStmt::Local {
2177 pattern: PurePattern::Ident {
2178 name: "x".into(),
2179 is_mut: false,
2180 },
2181 ty: None,
2182 init: Some(PureExpr::Lit("1".into())),
2183 },
2184 PureStmt::Semi(PureExpr::Binary {
2185 op: "=".into(),
2186 left: Box::new(PureExpr::Field {
2187 expr: Box::new(PureExpr::Path("self".into())),
2188 field: "data".into(),
2189 }),
2190 right: Box::new(PureExpr::Path("x".into())),
2191 }),
2192 ];
2193
2194 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2195 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2196 let kinds = outgoing_kinds(&graph, x);
2197
2198 assert!(
2199 !kinds.is_empty(),
2200 "x assigned to self.data should have outgoing flow, got: {:?}",
2201 kinds
2202 );
2203 }
2204
2205 #[test]
2208 fn test_async_move_block_captures_generate_flow() {
2209 let stmts = vec![
2212 PureStmt::Local {
2213 pattern: PurePattern::Ident {
2214 name: "x".into(),
2215 is_mut: false,
2216 },
2217 ty: None,
2218 init: Some(PureExpr::Lit("1".into())),
2219 },
2220 PureStmt::Semi(PureExpr::Async {
2221 is_move: true,
2222 body: PureBlock {
2223 stmts: vec![PureStmt::Expr(PureExpr::Binary {
2224 op: "+".into(),
2225 left: Box::new(PureExpr::Path("x".into())),
2226 right: Box::new(PureExpr::Lit("1".into())),
2227 })],
2228 },
2229 }),
2230 ];
2231
2232 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2233 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2234 let kinds = outgoing_kinds(&graph, x);
2235
2236 assert!(
2237 !kinds.is_empty(),
2238 "x captured by async move should have outgoing flow, got: {:?}",
2239 kinds
2240 );
2241 }
2242
2243 #[test]
2246 fn test_closure_captures_var_in_non_tail_stmt() {
2247 let stmts = vec![
2250 PureStmt::Local {
2251 pattern: PurePattern::Ident {
2252 name: "x".into(),
2253 is_mut: false,
2254 },
2255 ty: None,
2256 init: Some(PureExpr::Lit("1".into())),
2257 },
2258 PureStmt::Local {
2259 pattern: PurePattern::Ident {
2260 name: "f".into(),
2261 is_mut: false,
2262 },
2263 ty: None,
2264 init: Some(PureExpr::Closure {
2265 is_async: false,
2266 is_move: true,
2267 params: vec![],
2268 ret: None,
2269 body: Box::new(PureExpr::Block {
2270 label: None,
2271 block: PureBlock {
2272 stmts: vec![PureStmt::Local {
2273 pattern: PurePattern::Ident {
2274 name: "y".into(),
2275 is_mut: false,
2276 },
2277 ty: None,
2278 init: Some(PureExpr::Path("x".into())),
2279 }],
2280 },
2281 }),
2282 }),
2283 },
2284 ];
2285
2286 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2287 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2288 let kinds = outgoing_kinds(&graph, x);
2289
2290 assert!(
2291 kinds.contains(&FlowKind::Move),
2292 "x captured in non-tail stmt of move closure should have Move flow, got: {:?}",
2293 kinds
2294 );
2295 }
2296
2297 #[test]
2300 fn test_while_condition_generates_read_flow() {
2301 let stmts = vec![
2304 PureStmt::Local {
2305 pattern: PurePattern::Ident {
2306 name: "x".into(),
2307 is_mut: false,
2308 },
2309 ty: None,
2310 init: Some(PureExpr::Lit("1".into())),
2311 },
2312 PureStmt::Semi(PureExpr::While {
2313 label: None,
2314 cond: Box::new(PureExpr::Binary {
2315 op: ">".into(),
2316 left: Box::new(PureExpr::Path("x".into())),
2317 right: Box::new(PureExpr::Lit("0".into())),
2318 }),
2319 body: PureBlock { stmts: vec![] },
2320 }),
2321 ];
2322
2323 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2324 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2325 let kinds = outgoing_kinds(&graph, x);
2326
2327 assert!(
2328 !kinds.is_empty(),
2329 "x used in while condition should have outgoing flow, got: {:?}",
2330 kinds
2331 );
2332 }
2333
2334 #[test]
2337 fn test_tail_expr_generates_read_flow() {
2338 let stmts = vec![
2341 PureStmt::Local {
2342 pattern: PurePattern::Ident {
2343 name: "x".into(),
2344 is_mut: false,
2345 },
2346 ty: None,
2347 init: Some(PureExpr::Lit("1".into())),
2348 },
2349 PureStmt::Expr(PureExpr::Tuple(vec![
2350 PureExpr::Path("x".into()),
2351 PureExpr::Lit("2".into()),
2352 ])),
2353 ];
2354
2355 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2356 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2357 let kinds = outgoing_kinds(&graph, x);
2358
2359 assert!(
2360 !kinds.is_empty(),
2361 "x in tail expression should have outgoing flow, got: {:?}",
2362 kinds
2363 );
2364 }
2365
2366 #[test]
2369 fn test_macro_expr_variable_generates_read_flow() {
2370 let stmts = vec![
2373 PureStmt::Local {
2374 pattern: PurePattern::Ident {
2375 name: "x".into(),
2376 is_mut: false,
2377 },
2378 ty: None,
2379 init: Some(PureExpr::Lit("1".into())),
2380 },
2381 PureStmt::Semi(PureExpr::Macro {
2382 name: "println".into(),
2383 delimiter: MacroDelimiter::Paren,
2384 tokens: "\"{}\", x".into(),
2385 }),
2386 ];
2387
2388 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2389 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2390 let kinds = outgoing_kinds(&graph, x);
2391
2392 assert!(
2393 !kinds.is_empty(),
2394 "x referenced in macro tokens should have outgoing flow, got: {:?}",
2395 kinds
2396 );
2397 }
2398
2399 #[test]
2402 fn test_while_let_condition_generates_read_flow() {
2403 let stmts = vec![
2406 PureStmt::Local {
2407 pattern: PurePattern::Ident {
2408 name: "x".into(),
2409 is_mut: false,
2410 },
2411 ty: None,
2412 init: Some(PureExpr::Lit("1".into())),
2413 },
2414 PureStmt::Semi(PureExpr::While {
2415 label: None,
2416 cond: Box::new(PureExpr::Let {
2417 pattern: PurePattern::Ident {
2418 name: "v".into(),
2419 is_mut: false,
2420 },
2421 expr: Box::new(PureExpr::MethodCall {
2422 receiver: Box::new(PureExpr::Path("x".into())),
2423 method: "parent".into(),
2424 turbofish: None,
2425 args: vec![],
2426 }),
2427 }),
2428 body: PureBlock { stmts: vec![] },
2429 }),
2430 ];
2431
2432 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2433 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2434 let kinds = outgoing_kinds(&graph, x);
2435
2436 assert!(
2437 !kinds.is_empty(),
2438 "x used in while-let condition should have outgoing flow, got: {:?}",
2439 kinds
2440 );
2441 }
2442
2443 #[test]
2446 fn test_lock_direct_call_detected() {
2447 let stmts = vec![
2449 PureStmt::Local {
2450 pattern: PurePattern::Ident {
2451 name: "m".into(),
2452 is_mut: false,
2453 },
2454 ty: None,
2455 init: Some(PureExpr::Lit("1".into())),
2456 },
2457 PureStmt::Local {
2458 pattern: PurePattern::Ident {
2459 name: "guard".into(),
2460 is_mut: false,
2461 },
2462 ty: None,
2463 init: Some(PureExpr::MethodCall {
2464 receiver: Box::new(PureExpr::Path("m".into())),
2465 method: "lock".into(),
2466 turbofish: None,
2467 args: vec![],
2468 }),
2469 },
2470 ];
2471
2472 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2473 let tracker = graph.lock_tracker();
2474 assert_eq!(
2475 tracker.acquisitions().len(),
2476 1,
2477 "should detect 1 lock acquisition"
2478 );
2479 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::Mutex);
2480 assert!(!tracker.acquisitions()[0].is_try);
2481 assert_eq!(tracker.acquisitions()[0].lock_name, "m");
2482 assert_eq!(tracker.acquisitions()[0].guard_name, "guard");
2483 }
2484
2485 #[test]
2486 fn test_lock_unwrap_pattern_detected() {
2487 let stmts = vec![
2489 PureStmt::Local {
2490 pattern: PurePattern::Ident {
2491 name: "m".into(),
2492 is_mut: false,
2493 },
2494 ty: None,
2495 init: Some(PureExpr::Lit("1".into())),
2496 },
2497 PureStmt::Local {
2498 pattern: PurePattern::Ident {
2499 name: "guard".into(),
2500 is_mut: false,
2501 },
2502 ty: None,
2503 init: Some(PureExpr::MethodCall {
2504 receiver: Box::new(PureExpr::MethodCall {
2505 receiver: Box::new(PureExpr::Path("m".into())),
2506 method: "lock".into(),
2507 turbofish: None,
2508 args: vec![],
2509 }),
2510 method: "unwrap".into(),
2511 turbofish: None,
2512 args: vec![],
2513 }),
2514 },
2515 ];
2516
2517 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2518 let tracker = graph.lock_tracker();
2519 assert_eq!(
2520 tracker.acquisitions().len(),
2521 1,
2522 "should detect lock().unwrap() pattern"
2523 );
2524 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::Mutex);
2525 }
2526
2527 #[test]
2528 fn test_rwlock_read_write_detected() {
2529 let stmts = vec![
2531 PureStmt::Local {
2532 pattern: PurePattern::Ident {
2533 name: "rw".into(),
2534 is_mut: false,
2535 },
2536 ty: None,
2537 init: Some(PureExpr::Lit("1".into())),
2538 },
2539 PureStmt::Local {
2540 pattern: PurePattern::Ident {
2541 name: "r".into(),
2542 is_mut: false,
2543 },
2544 ty: None,
2545 init: Some(PureExpr::MethodCall {
2546 receiver: Box::new(PureExpr::Path("rw".into())),
2547 method: "read".into(),
2548 turbofish: None,
2549 args: vec![],
2550 }),
2551 },
2552 PureStmt::Local {
2553 pattern: PurePattern::Ident {
2554 name: "w".into(),
2555 is_mut: false,
2556 },
2557 ty: None,
2558 init: Some(PureExpr::MethodCall {
2559 receiver: Box::new(PureExpr::Path("rw".into())),
2560 method: "write".into(),
2561 turbofish: None,
2562 args: vec![],
2563 }),
2564 },
2565 ];
2566
2567 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2568 let tracker = graph.lock_tracker();
2569 assert_eq!(
2570 tracker.acquisitions().len(),
2571 2,
2572 "should detect both read and write locks"
2573 );
2574 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::RwLockRead);
2575 assert_eq!(tracker.acquisitions()[1].lock_type, LockType::RwLockWrite);
2576 }
2577
2578 #[test]
2579 fn test_try_lock_detected() {
2580 let stmts = vec![
2582 PureStmt::Local {
2583 pattern: PurePattern::Ident {
2584 name: "m".into(),
2585 is_mut: false,
2586 },
2587 ty: None,
2588 init: Some(PureExpr::Lit("1".into())),
2589 },
2590 PureStmt::Local {
2591 pattern: PurePattern::Ident {
2592 name: "guard".into(),
2593 is_mut: false,
2594 },
2595 ty: None,
2596 init: Some(PureExpr::MethodCall {
2597 receiver: Box::new(PureExpr::Path("m".into())),
2598 method: "try_lock".into(),
2599 turbofish: None,
2600 args: vec![],
2601 }),
2602 },
2603 ];
2604
2605 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2606 let tracker = graph.lock_tracker();
2607 assert_eq!(tracker.acquisitions().len(), 1, "should detect try_lock");
2608 assert!(tracker.acquisitions()[0].is_try);
2609 }
2610
2611 #[test]
2612 fn test_lock_await_pattern_detected() {
2613 let stmts = vec![
2615 PureStmt::Local {
2616 pattern: PurePattern::Ident {
2617 name: "m".into(),
2618 is_mut: false,
2619 },
2620 ty: None,
2621 init: Some(PureExpr::Lit("1".into())),
2622 },
2623 PureStmt::Local {
2624 pattern: PurePattern::Ident {
2625 name: "guard".into(),
2626 is_mut: false,
2627 },
2628 ty: None,
2629 init: Some(PureExpr::Await(Box::new(PureExpr::MethodCall {
2630 receiver: Box::new(PureExpr::Path("m".into())),
2631 method: "lock".into(),
2632 turbofish: None,
2633 args: vec![],
2634 }))),
2635 },
2636 ];
2637
2638 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2639 let tracker = graph.lock_tracker();
2640 assert_eq!(
2641 tracker.acquisitions().len(),
2642 1,
2643 "should detect lock().await pattern"
2644 );
2645 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::Mutex);
2646 }
2647
2648 #[test]
2649 fn test_borrow_borrow_mut_detected() {
2650 let stmts = vec![
2652 PureStmt::Local {
2653 pattern: PurePattern::Ident {
2654 name: "cell".into(),
2655 is_mut: false,
2656 },
2657 ty: None,
2658 init: Some(PureExpr::Lit("1".into())),
2659 },
2660 PureStmt::Local {
2661 pattern: PurePattern::Ident {
2662 name: "r".into(),
2663 is_mut: false,
2664 },
2665 ty: None,
2666 init: Some(PureExpr::MethodCall {
2667 receiver: Box::new(PureExpr::Path("cell".into())),
2668 method: "borrow".into(),
2669 turbofish: None,
2670 args: vec![],
2671 }),
2672 },
2673 PureStmt::Local {
2674 pattern: PurePattern::Ident {
2675 name: "w".into(),
2676 is_mut: false,
2677 },
2678 ty: None,
2679 init: Some(PureExpr::MethodCall {
2680 receiver: Box::new(PureExpr::Path("cell".into())),
2681 method: "borrow_mut".into(),
2682 turbofish: None,
2683 args: vec![],
2684 }),
2685 },
2686 ];
2687
2688 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2689 let tracker = graph.lock_tracker();
2690 assert_eq!(
2691 tracker.acquisitions().len(),
2692 2,
2693 "should detect borrow and borrow_mut"
2694 );
2695 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::RefCell);
2696 assert_eq!(tracker.acquisitions()[1].lock_type, LockType::RefCellMut);
2697 }
2698
2699 #[test]
2700 fn test_non_lock_method_not_detected() {
2701 let stmts = vec![
2703 PureStmt::Local {
2704 pattern: PurePattern::Ident {
2705 name: "x".into(),
2706 is_mut: false,
2707 },
2708 ty: None,
2709 init: Some(PureExpr::Lit("1".into())),
2710 },
2711 PureStmt::Local {
2712 pattern: PurePattern::Ident {
2713 name: "y".into(),
2714 is_mut: false,
2715 },
2716 ty: None,
2717 init: Some(PureExpr::MethodCall {
2718 receiver: Box::new(PureExpr::Path("x".into())),
2719 method: "foo".into(),
2720 turbofish: None,
2721 args: vec![],
2722 }),
2723 },
2724 ];
2725
2726 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2727 let tracker = graph.lock_tracker();
2728 assert_eq!(
2729 tracker.acquisitions().len(),
2730 0,
2731 "non-lock method should not be detected"
2732 );
2733 }
2734
2735 #[test]
2736 fn test_guard_field_read_tracked() {
2737 let stmts = vec![
2739 PureStmt::Local {
2740 pattern: PurePattern::Ident {
2741 name: "m".into(),
2742 is_mut: false,
2743 },
2744 ty: None,
2745 init: Some(PureExpr::Lit("1".into())),
2746 },
2747 PureStmt::Local {
2748 pattern: PurePattern::Ident {
2749 name: "guard".into(),
2750 is_mut: false,
2751 },
2752 ty: None,
2753 init: Some(PureExpr::MethodCall {
2754 receiver: Box::new(PureExpr::Path("m".into())),
2755 method: "lock".into(),
2756 turbofish: None,
2757 args: vec![],
2758 }),
2759 },
2760 PureStmt::Local {
2762 pattern: PurePattern::Wild,
2763 ty: None,
2764 init: Some(PureExpr::Field {
2765 expr: Box::new(PureExpr::Path("guard".into())),
2766 field: "counter".into(),
2767 }),
2768 },
2769 ];
2770
2771 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2772 let tracker = graph.lock_tracker();
2773 assert_eq!(tracker.acquisitions().len(), 1);
2774 let sections = tracker.critical_sections();
2775 assert_eq!(sections.len(), 1, "should have 1 completed section");
2776 assert_eq!(
2777 sections[0].field_accesses.len(),
2778 1,
2779 "should track 1 field access"
2780 );
2781 assert_eq!(sections[0].field_accesses[0].field_name, "counter");
2782 assert_eq!(
2783 sections[0].field_accesses[0].access_kind,
2784 super::super::lock_v2::AccessKind::Read
2785 );
2786 }
2787
2788 #[test]
2789 fn test_guard_field_write_tracked() {
2790 let stmts = vec![
2792 PureStmt::Local {
2793 pattern: PurePattern::Ident {
2794 name: "m".into(),
2795 is_mut: false,
2796 },
2797 ty: None,
2798 init: Some(PureExpr::Lit("1".into())),
2799 },
2800 PureStmt::Local {
2801 pattern: PurePattern::Ident {
2802 name: "guard".into(),
2803 is_mut: false,
2804 },
2805 ty: None,
2806 init: Some(PureExpr::MethodCall {
2807 receiver: Box::new(PureExpr::Path("m".into())),
2808 method: "lock".into(),
2809 turbofish: None,
2810 args: vec![],
2811 }),
2812 },
2813 PureStmt::Semi(PureExpr::Binary {
2815 op: "=".into(),
2816 left: Box::new(PureExpr::Field {
2817 expr: Box::new(PureExpr::Path("guard".into())),
2818 field: "counter".into(),
2819 }),
2820 right: Box::new(PureExpr::Lit("42".into())),
2821 }),
2822 ];
2823
2824 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2825 let tracker = graph.lock_tracker();
2826 assert_eq!(tracker.acquisitions().len(), 1);
2827 let sections = tracker.critical_sections();
2828 assert_eq!(sections.len(), 1);
2829 assert_eq!(sections[0].field_accesses.len(), 1);
2830 assert_eq!(sections[0].field_accesses[0].field_name, "counter");
2831 assert_eq!(
2832 sections[0].field_accesses[0].access_kind,
2833 super::super::lock_v2::AccessKind::Write
2834 );
2835 }
2836
2837 #[test]
2838 fn test_non_guard_field_not_tracked() {
2839 let stmts = vec![
2842 PureStmt::Local {
2843 pattern: PurePattern::Ident {
2844 name: "x".into(),
2845 is_mut: false,
2846 },
2847 ty: None,
2848 init: Some(PureExpr::Lit("1".into())),
2849 },
2850 PureStmt::Local {
2851 pattern: PurePattern::Wild,
2852 ty: None,
2853 init: Some(PureExpr::Field {
2854 expr: Box::new(PureExpr::Path("x".into())),
2855 field: "field".into(),
2856 }),
2857 },
2858 ];
2859
2860 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2861 let tracker = graph.lock_tracker();
2862 assert_eq!(tracker.acquisitions().len(), 0);
2863 assert_eq!(tracker.critical_sections().len(), 0);
2864 }
2865
2866 fn build_graph_for_impl(impl_block: PureImpl) -> DataFlowGraphV2 {
2873 let mut registry = SymbolRegistry::new();
2874 let module_path = SymbolPath::parse("test_crate").unwrap();
2875
2876 let base_type = impl_block
2878 .self_ty
2879 .split('<')
2880 .next()
2881 .unwrap_or(&impl_block.self_ty)
2882 .trim();
2883 let type_path = module_path.child(base_type).unwrap();
2884
2885 for item in &impl_block.items {
2886 if let PureImplItem::Fn(f) = item {
2887 let method_path = type_path.child(&f.name).unwrap();
2888 registry.register(method_path, SymbolKind::Method).unwrap();
2889 }
2890 }
2891
2892 let file = PureFile {
2893 attrs: vec![],
2894 items: vec![PureItem::Impl(impl_block)],
2895 };
2896
2897 let mut graph = DataFlowGraphV2::new();
2898 let mut collector = FlowCollectorV2::new(Some(module_path), ®istry, "test_crate");
2899 collector.visit_file(&file);
2900 collector.apply_to(&mut graph);
2901 graph
2902 }
2903
2904 #[test]
2905 fn test_lock_in_generic_inherent_impl_detected() {
2906 let method = PureFn {
2908 attrs: vec![],
2909 vis: PureVis::Public,
2910 is_async: false,
2911 is_async_inferred: false,
2912 is_const: false,
2913 is_unsafe: false,
2914 abi: None,
2915 name: "take".to_string(),
2916 generics: PureGenerics::default(),
2917 params: vec![PureParam::SelfValue {
2918 is_ref: true,
2919 is_mut: false,
2920 }],
2921 ret: None,
2922 body: PureBlock {
2923 stmts: vec![PureStmt::Local {
2924 pattern: PurePattern::Ident {
2925 name: "g".into(),
2926 is_mut: false,
2927 },
2928 ty: None,
2929 init: Some(PureExpr::MethodCall {
2930 receiver: Box::new(PureExpr::Field {
2931 expr: Box::new(PureExpr::Path("self".into())),
2932 field: "write".into(),
2933 }),
2934 method: "lock".into(),
2935 turbofish: None,
2936 args: vec![],
2937 }),
2938 }],
2939 },
2940 };
2941
2942 let impl_block = PureImpl {
2943 attrs: vec![],
2944 generics: PureGenerics::default(),
2945 is_unsafe: false,
2946 trait_: None,
2947 self_ty: "Handle < S >".to_string(), items: vec![PureImplItem::Fn(method)],
2949 };
2950
2951 let graph = build_graph_for_impl(impl_block);
2952 let tracker = graph.lock_tracker();
2953 assert_eq!(
2954 tracker.acquisitions().len(),
2955 1,
2956 "lock in generic inherent impl should be detected; \
2957 visit_impl_item must strip generics from self_ty"
2958 );
2959 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::Mutex);
2960 }
2961}