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 PureStmt::Verbatim(_) => {}
615 }
616 }
617 }
618
619 fn track_macro_var_refs(&mut self, tokens: &str) {
624 let Some(sink) = self.usage_sink() else {
625 return;
626 };
627 for token in tokens.split(|c: char| !c.is_alphanumeric() && c != '_') {
629 if token.is_empty() || token.starts_with(|c: char| c.is_ascii_digit()) {
630 continue;
631 }
632 if let Some(&var_id) = self.var_name_map.get(token) {
634 self.add_flow(var_id, sink, FlowKind::Read);
635 }
636 }
637 }
638
639 fn bind_pattern_vars(&mut self, pattern: &PurePattern, scrutinee_sources: &[usize]) {
644 match pattern {
645 PurePattern::Ident { name, .. } => {
646 if let Some(target_id) = self.get_or_create_var(name, VarKind::Local) {
647 for &source_id in scrutinee_sources {
648 self.add_flow(source_id, target_id, FlowKind::Assign);
649 }
650 }
651 }
652 PurePattern::Tuple(pats) | PurePattern::Slice(pats) | PurePattern::Or(pats) => {
653 for pat in pats {
654 self.bind_pattern_vars(pat, scrutinee_sources);
655 }
656 }
657 PurePattern::Struct { fields, .. } => {
658 for (_, pat) in fields {
659 self.bind_pattern_vars(pat, scrutinee_sources);
660 }
661 }
662 PurePattern::Ref { pattern, .. } => {
663 self.bind_pattern_vars(pattern, scrutinee_sources);
664 }
665 _ => {} }
667 }
668
669 fn track_return(&mut self, expr: &PureExpr) {
671 let Some(sink) = self.usage_sink() else {
672 return;
673 };
674 let sources = self.collect_expr_sources(expr);
675 for source_id in sources {
676 self.add_flow(source_id, sink, FlowKind::Return);
677 }
678 }
679
680 fn add_flow(&mut self, from_id: usize, to_id: usize, kind: FlowKind) {
682 let scope = self.current_scope();
683 self.flows.push(CollectedFlowV2 {
684 from_id,
685 to_id,
686 data: FlowData {
687 kind,
688 line: self.current_line,
689 scope,
690 },
691 });
692 }
693
694 fn current_scope(&self) -> ScopeId {
698 self.scope_stack
699 .last()
700 .copied()
701 .unwrap_or(ScopeId::from_raw(0))
702 }
703
704 fn push_scope(&mut self, kind: ScopeKind, guard: Option<Guard>) {
706 let parent = self.scope_stack.last().copied();
707 let id = ScopeId::from_raw(self.collected_scopes.len() as u32);
708 self.collected_scopes.push(ScopeData {
709 parent,
710 kind,
711 guard,
712 });
713 self.scope_stack.push(id);
714 }
715
716 fn pop_scope(&mut self) {
718 self.scope_stack.pop();
719 }
720
721 fn parse_guard(&mut self, cond: &PureExpr) -> Option<Guard> {
728 match cond {
729 PureExpr::Binary { op, left, right } => {
731 let (kind, left_expr, _right_expr) = match op.as_str() {
732 "<" => (GuardKind::LessThan, left, right),
733 "<=" => (GuardKind::LessEqual, left, right),
734 ">" => (GuardKind::LessThan, right, left),
736 ">=" => (GuardKind::LessEqual, right, left),
737 "&&" => {
739 return self.parse_guard(left).or_else(|| self.parse_guard(right));
741 }
742 _ => return None,
743 };
744 let var_name = extract_expr_name(left_expr)?;
746 Some(Guard {
747 kind,
748 var_names: smallvec![var_name],
749 })
750 }
751 PureExpr::MethodCall {
753 receiver, method, ..
754 } => {
755 let kind = match method.as_str() {
756 "is_some" => GuardKind::IsSome,
757 "is_ok" => GuardKind::IsOk,
758 _ => return None,
759 };
760 let var_name = extract_expr_name(receiver)?;
761 Some(Guard {
762 kind,
763 var_names: smallvec![var_name],
764 })
765 }
766 PureExpr::Let { pattern, expr } => {
768 let kind = match pattern {
769 PurePattern::Struct { path, .. } | PurePattern::Path(path) => {
770 if path == "Some" || path.ends_with("::Some") {
771 GuardKind::LetSome
772 } else if path == "Ok" || path.ends_with("::Ok") {
773 GuardKind::LetOk
774 } else {
775 return None;
776 }
777 }
778 _ => return None,
779 };
780 let var_name = extract_expr_name(expr)?;
781 Some(Guard {
782 kind,
783 var_names: smallvec![var_name],
784 })
785 }
786 _ => None,
787 }
788 }
789
790 fn parse_match_arm_guard(&self, _pattern: &PurePattern) -> Option<Guard> {
792 None
794 }
795
796 fn emit_conditional_flow(&mut self, cond: &PureExpr) {
798 let mut names = Vec::new();
799 FlowCollectorV2::collect_all_referenced_names(cond, &mut names);
800 let var_ids: Vec<usize> = names
802 .iter()
803 .filter_map(|name| self.lookup_existing_var(name))
804 .collect();
805 for &var_id in &var_ids {
808 self.add_flow(var_id, var_id, FlowKind::Conditional);
809 }
810 }
811
812 fn visit_file(&mut self, file: &PureFile) {
813 for item in &file.items {
814 self.visit_item(item);
815 }
816 }
817
818 fn visit_item(&mut self, item: &PureItem) {
819 match item {
820 PureItem::Fn(f) => {
821 let scope_path = self
823 .module_path
824 .as_ref()
825 .and_then(|mp| mp.child(&f.name).ok())
826 .unwrap_or_else(|| {
827 SymbolPath::parse(&format!("{}::unknown::{}", self.crate_name, f.name))
828 .unwrap_or_else(|_| {
829 SymbolPath::parse(&format!("{}::unknown", self.crate_name)).unwrap()
830 })
831 });
832
833 self.current_scope_path = Some(scope_path.clone());
834 self.current_line = 0;
835 self.var_name_map.clear();
837 self.guard_temp_ids.clear();
838
839 self.current_symbol = self.registry.lookup(&scope_path);
841
842 self.push_scope(ScopeKind::Function, None);
844
845 for param in &f.params {
847 if let Some(name) = extract_param_name(param) {
848 self.get_or_create_var(&name, VarKind::Parameter);
849 }
850 }
851
852 self.visit_block(&f.body);
853 self.pop_scope();
854 self.current_scope_path = None;
855 self.current_symbol = None;
856 }
857 PureItem::Impl(i) => {
858 for impl_item in &i.items {
859 self.visit_impl_item(impl_item, &i.self_ty, i.trait_.as_deref());
860 }
861 }
862 PureItem::Mod(m) => {
863 for item in &m.items {
864 self.visit_item(item);
865 }
866 }
867 PureItem::Static(_) | PureItem::Const(_) => {}
869 _ => {}
870 }
871 }
872
873 fn visit_impl_item(&mut self, item: &PureImplItem, self_ty: &str, trait_: Option<&str>) {
874 if let PureImplItem::Fn(f) = item {
875 let scope_path = self
879 .module_path
880 .as_ref()
881 .and_then(|mp| {
882 let method_base = if let Some(trait_name) = trait_ {
883 mp.child_trait_impl(trait_name, self_ty)
884 } else {
885 let base_type = self_ty.split('<').next().unwrap_or(self_ty).trim();
887 mp.child(base_type).ok()?
888 };
889 method_base.child(&f.name).ok()
890 })
891 .unwrap_or_else(|| {
892 SymbolPath::parse(&format!(
893 "{}::unknown::{}::{}",
894 self.crate_name, self_ty, f.name
895 ))
896 .unwrap_or_else(|_| {
897 SymbolPath::parse(&format!("{}::unknown", self.crate_name)).unwrap()
898 })
899 });
900
901 self.current_scope_path = Some(scope_path.clone());
902 self.current_line = 0;
903 self.var_name_map.clear();
905
906 self.current_symbol = self.registry.lookup(&scope_path);
908
909 self.push_scope(ScopeKind::Function, None);
911
912 for param in &f.params {
914 if let Some(name) = extract_param_name(param) {
915 if name != "self" {
916 self.get_or_create_var(&name, VarKind::Parameter);
917 }
918 }
919 }
920
921 self.visit_block(&f.body);
922 self.pop_scope();
923 self.current_scope_path = None;
924 self.current_symbol = None;
925 }
926 }
927
928 fn visit_block(&mut self, block: &PureBlock) {
929 for stmt in &block.stmts {
930 self.visit_stmt(stmt);
931 }
932 if let Some(PureStmt::Expr(expr)) = block.stmts.last() {
934 self.track_return(expr);
935 }
936 }
937
938 fn visit_stmt(&mut self, stmt: &PureStmt) {
939 match stmt {
940 PureStmt::Local { pattern, init, .. } => {
941 if let Some(name) = extract_pattern_name(pattern) {
942 if let Some(target_id) = self.get_or_create_var(&name, VarKind::Local) {
943 if matches!(pattern, PurePattern::Ident { is_mut: true, .. }) {
945 self.set_var_mut(target_id);
946 }
947 if let Some(expr) = init {
948 if let Some((lock_receiver, lock_type, is_try)) =
951 extract_lock_call(expr)
952 {
953 let lock_sources = self.collect_expr_sources(lock_receiver);
954 if let Some(&lock_source_id) = lock_sources.first() {
955 let lock_name = extract_expr_name(lock_receiver)
956 .unwrap_or_else(|| "?".to_string());
957 self.collected_locks.push(CollectedLockV2 {
958 lock_temp_id: lock_source_id,
959 guard_temp_id: target_id,
960 lock_type,
961 line: self.current_line,
962 is_try,
963 lock_name,
964 guard_name: name.clone(),
965 owner_fn: self.current_symbol,
966 });
967 self.guard_temp_ids.insert(target_id);
968 }
969 }
970 if let Some(clone_receiver) = is_clone_call(expr) {
972 let sources = self.collect_expr_sources(clone_receiver);
974 for source_id in sources {
975 self.add_flow(source_id, target_id, FlowKind::Clone);
976 }
977 } else if let Some((is_mut, inner)) = is_borrow_expr(expr) {
978 let kind = if is_mut {
980 FlowKind::MutBorrow
981 } else {
982 FlowKind::SharedBorrow
983 };
984 let sources = self.collect_expr_sources(inner);
985 for source_id in sources {
986 self.add_flow(source_id, target_id, kind);
987 }
988 } else {
989 let sources = self.collect_expr_sources(expr);
991 for source_id in sources {
992 self.add_flow(source_id, target_id, FlowKind::Assign);
993 }
994 }
995 self.in_local_init = true;
999 self.visit_expr_for_assignments(expr);
1000 self.in_local_init = false;
1001 }
1002 }
1003 } else if let Some(expr) = init {
1004 self.in_local_init = true;
1008 self.visit_expr_for_assignments(expr);
1009 self.in_local_init = false;
1010 }
1011 }
1012 PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
1013 self.visit_expr_for_assignments(expr);
1014 }
1015 PureStmt::Item(item) => {
1016 self.visit_item(item);
1017 }
1018 PureStmt::Verbatim(_) => {}
1021 }
1022 }
1023
1024 fn visit_expr_for_assignments(&mut self, expr: &PureExpr) {
1025 match expr {
1026 PureExpr::Binary { op, left, right } if op == "=" => {
1027 if let Some((name, kind)) = classify_lvalue(left) {
1028 if let Some(target_id) = self.get_or_create_var(&name, kind) {
1029 let sources = self.collect_expr_sources(right);
1030
1031 for source_id in sources {
1032 self.add_flow(source_id, target_id, FlowKind::Assign);
1033 }
1034 }
1035 }
1036 self.track_guard_field_access(left, AccessKind::Write);
1038 self.visit_expr_for_assignments(right);
1039 }
1040 PureExpr::Binary { left, right, .. } => {
1041 self.visit_expr_for_assignments(left);
1042 self.visit_expr_for_assignments(right);
1043 }
1044 PureExpr::Block { block, .. } => {
1045 self.push_scope(ScopeKind::Block, None);
1046 self.visit_block(block);
1047 self.pop_scope();
1048 }
1049 PureExpr::If {
1050 cond,
1051 then_branch,
1052 else_branch,
1053 } => {
1054 self.visit_expr_for_assignments(cond);
1055 self.emit_conditional_flow(cond);
1057 let guard = self.parse_guard(cond);
1059 self.push_scope(ScopeKind::IfThen, guard);
1060 self.visit_block(then_branch);
1061 self.pop_scope();
1062 if let Some(else_expr) = else_branch {
1063 self.push_scope(ScopeKind::IfElse, None);
1065 self.visit_expr_for_assignments(else_expr);
1066 self.pop_scope();
1067 }
1068 }
1069 PureExpr::Match { expr, arms } => {
1070 self.visit_expr_for_assignments(expr);
1071 self.track_receiver_read(expr);
1073 let scrutinee_sources = self.collect_expr_sources(expr);
1075 for arm in arms {
1076 let arm_guard = self.parse_match_arm_guard(&arm.pattern);
1078 self.push_scope(ScopeKind::MatchArm, arm_guard);
1079 self.bind_pattern_vars(&arm.pattern, &scrutinee_sources);
1081 if let Some(guard) = &arm.guard {
1082 self.visit_expr_for_assignments(guard);
1083 }
1084 self.visit_expr_for_assignments(&arm.body);
1085 self.pop_scope();
1086 }
1087 }
1088 PureExpr::Loop { body: block, .. } => {
1089 self.push_scope(ScopeKind::LoopBody, None);
1090 self.visit_block(block);
1091 self.pop_scope();
1092 }
1093 PureExpr::While { cond, body, .. } => {
1094 self.visit_expr_for_assignments(cond);
1095 self.track_receiver_read(cond);
1096 self.emit_conditional_flow(cond);
1097 let guard = self.parse_guard(cond);
1098 self.push_scope(ScopeKind::WhileBody, guard);
1099 self.visit_block(body);
1100 self.pop_scope();
1101 }
1102 PureExpr::For {
1103 pat, expr, body, ..
1104 } => {
1105 self.visit_expr_for_assignments(expr);
1106 if let Some(name) = extract_pattern_name(pat) {
1108 if let Some(target_id) = self.get_or_create_var(&name, VarKind::Local) {
1109 let sources = self.collect_expr_sources(expr);
1110 for source_id in sources {
1111 self.add_flow(source_id, target_id, FlowKind::Assign);
1112 }
1113 }
1114 }
1115 self.track_receiver_read(expr);
1117 self.push_scope(ScopeKind::ForBody, None);
1119 self.visit_block(body);
1120 self.pop_scope();
1121 }
1122 PureExpr::Closure { body, is_move, .. } => {
1123 self.track_closure_captures(body, *is_move);
1125 self.push_scope(ScopeKind::Closure, None);
1126 self.visit_expr_for_assignments(body);
1127 self.pop_scope();
1128 }
1129 PureExpr::Call { func, args } => {
1130 self.visit_expr_for_assignments(func);
1131 for arg in args {
1132 self.visit_expr_for_assignments(arg);
1133 }
1134 self.track_call_args(args);
1136 }
1137 PureExpr::MethodCall {
1138 receiver,
1139 method,
1140 args,
1141 ..
1142 } => {
1143 self.visit_expr_for_assignments(receiver);
1144 for arg in args {
1145 self.visit_expr_for_assignments(arg);
1146 }
1147 if !self.in_local_init {
1151 if let Some((lock_receiver, lock_type, is_try)) = extract_lock_call(expr) {
1152 let lock_sources = self.collect_expr_sources(lock_receiver);
1153 if let Some(&lock_source_id) = lock_sources.first() {
1154 let lock_name =
1155 extract_expr_name(lock_receiver).unwrap_or_else(|| "?".to_string());
1156 let guard_name = format!("_anon_guard_{}", self.current_line);
1157 if let Some(guard_id) =
1158 self.get_or_create_var(&guard_name, VarKind::Temp)
1159 {
1160 self.collected_locks.push(CollectedLockV2 {
1161 lock_temp_id: lock_source_id,
1162 guard_temp_id: guard_id,
1163 lock_type,
1164 line: self.current_line,
1165 is_try,
1166 lock_name,
1167 guard_name,
1168 owner_fn: self.current_symbol,
1169 });
1170 self.guard_temp_ids.insert(guard_id);
1171 }
1172 }
1173 }
1174 }
1175 if method.ends_with("_mut") {
1187 self.track_receiver_mut_borrow(receiver);
1188 } else {
1189 self.track_receiver_read(receiver);
1190 }
1191 self.track_call_args(args);
1192 }
1193 PureExpr::Return(Some(expr)) => {
1194 self.visit_expr_for_assignments(expr);
1195 self.track_return(expr);
1196 }
1197 PureExpr::Return(None) => {}
1198 PureExpr::Break {
1199 expr: Some(expr), ..
1200 } => {
1201 self.visit_expr_for_assignments(expr);
1202 self.track_return(expr); }
1204 PureExpr::Struct { fields, .. } => {
1205 for (_, value_expr) in fields {
1207 self.visit_expr_for_assignments(value_expr);
1208 self.track_receiver_read(value_expr);
1209 }
1210 }
1211 PureExpr::Async { is_move, body } => {
1212 if *is_move {
1213 let body_expr = PureExpr::Block {
1214 label: None,
1215 block: body.clone(),
1216 };
1217 self.track_closure_captures(&body_expr, true);
1218 }
1219 self.push_scope(ScopeKind::Block, None);
1220 self.visit_block(body);
1221 self.pop_scope();
1222 }
1223 PureExpr::Macro { tokens, .. } => {
1224 self.track_macro_var_refs(tokens);
1226 }
1227 PureExpr::Index { expr, index } => {
1228 self.visit_expr_for_assignments(expr);
1230 self.visit_expr_for_assignments(index);
1231 if let Some(index_name) = extract_expr_name(index) {
1233 if let Some(index_id) = self.lookup_existing_var(&index_name) {
1234 if let Some(coll_name) = extract_expr_name(expr) {
1235 if let Some(coll_id) = self.lookup_existing_var(&coll_name) {
1236 self.add_flow(index_id, coll_id, FlowKind::Index);
1237 }
1238 }
1239 }
1240 }
1241 }
1242 PureExpr::Let { pattern, expr } => {
1243 self.visit_expr_for_assignments(expr);
1245 self.track_receiver_read(expr);
1246 let scrutinee_sources = self.collect_expr_sources(expr);
1250 self.bind_pattern_vars(pattern, &scrutinee_sources);
1251 }
1252 PureExpr::Field { .. } => {
1253 self.track_guard_field_access(expr, AccessKind::Read);
1255 }
1256 _ => {}
1257 }
1258 }
1259
1260 fn track_guard_field_access(&mut self, expr: &PureExpr, access_kind: AccessKind) {
1265 if let PureExpr::Field {
1266 expr: receiver,
1267 field,
1268 } = expr
1269 {
1270 if let PureExpr::Path(name) = &**receiver {
1271 if let Some(&guard_id) = self.var_name_map.get(name.as_str()) {
1272 if self.guard_temp_ids.contains(&guard_id) {
1273 self.collected_field_accesses.push(CollectedFieldAccessV2 {
1274 guard_temp_id: guard_id,
1275 field_name: field.clone(),
1276 access_kind,
1277 });
1278 }
1279 }
1280 }
1281 }
1282 }
1283
1284 fn collect_expr_sources(&mut self, expr: &PureExpr) -> Vec<usize> {
1285 let mut sources = Vec::new();
1286 self.collect_sources_recursive(expr, &mut sources);
1287 sources
1288 }
1289
1290 fn collect_sources_recursive(&mut self, expr: &PureExpr, sources: &mut Vec<usize>) {
1291 match expr {
1292 PureExpr::Path(name) => {
1293 let simple_name = name.split("::").last().unwrap_or(name);
1294 if let Some(id) = self.lookup_or_create_var(simple_name, VarKind::Local) {
1296 sources.push(id);
1297 }
1298 }
1299 PureExpr::Field { expr, field } => {
1300 if is_self_expr(expr) {
1301 if let Some(id) = self.get_or_create_var(field, VarKind::Field) {
1303 sources.push(id);
1304 }
1305 } else {
1306 self.collect_sources_recursive(expr, sources);
1307 }
1308 }
1309 PureExpr::Binary { left, right, .. } => {
1310 self.collect_sources_recursive(left, sources);
1311 self.collect_sources_recursive(right, sources);
1312 }
1313 PureExpr::Unary { expr, .. } => {
1314 self.collect_sources_recursive(expr, sources);
1315 }
1316 PureExpr::Call { func, args } => {
1317 for arg in args {
1318 self.collect_sources_recursive(arg, sources);
1319 }
1320 self.collect_sources_recursive(func, sources);
1321 }
1322 PureExpr::MethodCall { receiver, args, .. } => {
1323 self.collect_sources_recursive(receiver, sources);
1324 for arg in args {
1325 self.collect_sources_recursive(arg, sources);
1326 }
1327 }
1328 PureExpr::If {
1329 cond,
1330 then_branch,
1331 else_branch,
1332 } => {
1333 self.collect_sources_recursive(cond, sources);
1334 if let Some(PureStmt::Expr(e)) = then_branch.stmts.last() {
1335 self.collect_sources_recursive(e, sources);
1336 }
1337 if let Some(else_expr) = else_branch {
1338 self.collect_sources_recursive(else_expr, sources);
1339 }
1340 }
1341 PureExpr::Block { block, .. } => {
1342 if let Some(PureStmt::Expr(e)) = block.stmts.last() {
1343 self.collect_sources_recursive(e, sources);
1344 }
1345 }
1346 PureExpr::Match { expr, arms } => {
1347 self.collect_sources_recursive(expr, sources);
1348 for arm in arms {
1349 self.collect_sources_recursive(&arm.body, sources);
1350 }
1351 }
1352 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1353 for e in exprs {
1354 self.collect_sources_recursive(e, sources);
1355 }
1356 }
1357 PureExpr::Index { expr, index } => {
1358 self.collect_sources_recursive(expr, sources);
1359 self.collect_sources_recursive(index, sources);
1360 }
1361 PureExpr::Ref { expr, .. } | PureExpr::Await(expr) | PureExpr::Try(expr) => {
1362 self.collect_sources_recursive(expr, sources);
1363 }
1364 PureExpr::Struct { fields, .. } => {
1365 for (_, e) in fields {
1366 self.collect_sources_recursive(e, sources);
1367 }
1368 }
1369 _ => {}
1370 }
1371 }
1372}
1373
1374fn extract_param_name(param: &PureParam) -> Option<String> {
1375 match param {
1376 PureParam::SelfValue { .. } => Some("self".to_string()),
1377 PureParam::Typed { name, .. } => Some(name.clone()),
1378 }
1379}
1380
1381fn extract_pattern_name(pat: &PurePattern) -> Option<String> {
1382 match pat {
1383 PurePattern::Ident { name, .. } => Some(name.clone()),
1384 PurePattern::Ref { pattern, .. } => extract_pattern_name(pattern),
1385 _ => None,
1386 }
1387}
1388
1389fn extract_expr_name(expr: &PureExpr) -> Option<String> {
1393 match expr {
1394 PureExpr::Path(name) => Some(name.clone()),
1395 PureExpr::Field { expr, field } if is_self_expr(expr) => Some(field.clone()),
1396 PureExpr::Field { expr, field } => {
1397 if let Some(receiver_name) = extract_expr_name(expr) {
1399 Some(format!("{}.{}", receiver_name, field))
1400 } else {
1401 Some(field.clone())
1402 }
1403 }
1404 _ => None,
1405 }
1406}
1407
1408fn classify_lvalue(expr: &PureExpr) -> Option<(String, VarKind)> {
1409 match expr {
1410 PureExpr::Path(name) => Some((name.clone(), VarKind::Local)),
1411 PureExpr::Field { expr, field } => {
1412 if is_self_expr(expr) {
1413 Some((field.clone(), VarKind::Field))
1415 } else {
1416 None
1417 }
1418 }
1419 _ => None,
1420 }
1421}
1422
1423fn is_self_expr(expr: &PureExpr) -> bool {
1424 matches!(expr, PureExpr::Path(name) if name == "self")
1425}
1426
1427fn is_borrow_expr(expr: &PureExpr) -> Option<(bool, &PureExpr)> {
1431 match expr {
1432 PureExpr::Ref { is_mut, expr } => Some((*is_mut, expr)),
1433 _ => None,
1434 }
1435}
1436
1437fn is_clone_call(expr: &PureExpr) -> Option<&PureExpr> {
1441 match expr {
1442 PureExpr::MethodCall {
1443 receiver,
1444 method,
1445 args,
1446 ..
1447 } if method == "clone" && args.is_empty() => Some(receiver),
1448 _ => None,
1449 }
1450}
1451
1452fn classify_lock_method(method: &str) -> Option<(LockType, bool)> {
1456 match method {
1457 "lock" => Some((LockType::Mutex, false)),
1458 "try_lock" => Some((LockType::Mutex, true)),
1459 "read" => Some((LockType::RwLockRead, false)),
1460 "try_read" => Some((LockType::RwLockRead, true)),
1461 "write" => Some((LockType::RwLockWrite, false)),
1462 "try_write" => Some((LockType::RwLockWrite, true)),
1463 "borrow" => Some((LockType::RefCell, false)),
1464 "borrow_mut" => Some((LockType::RefCellMut, false)),
1465 _ => None,
1466 }
1467}
1468
1469fn extract_lock_call(expr: &PureExpr) -> Option<(&PureExpr, LockType, bool)> {
1479 match expr {
1480 PureExpr::MethodCall {
1481 receiver,
1482 method,
1483 args,
1484 ..
1485 } => {
1486 if args.is_empty() {
1488 if let Some((lock_type, is_try)) = classify_lock_method(method) {
1489 return Some((receiver, lock_type, is_try));
1490 }
1491 }
1492 if args.is_empty() {
1495 return extract_lock_call(receiver);
1496 }
1497 None
1498 }
1499 PureExpr::Await(inner) => extract_lock_call(inner),
1501 PureExpr::Try(inner) => extract_lock_call(inner),
1503 _ => None,
1504 }
1505}
1506
1507#[cfg(test)]
1508mod tests {
1509 use super::*;
1510 use crate::symbol::SymbolKind;
1511 use ryo_source::pure::{
1512 MacroDelimiter, PureFn, PureGenerics, PureImpl, PureImplItem, PureMatchArm, PureVis,
1513 };
1514
1515 fn build_graph_for_fn(fn_def: PureFn) -> DataFlowGraphV2 {
1520 let mut registry = SymbolRegistry::new();
1521 let module_path = SymbolPath::parse("test_crate").unwrap();
1522 let fn_path = module_path.child(&fn_def.name).unwrap();
1523 registry.register(fn_path, SymbolKind::Function).unwrap();
1524
1525 let file = PureFile {
1526 attrs: vec![],
1527 items: vec![PureItem::Fn(fn_def)],
1528 };
1529
1530 let mut graph = DataFlowGraphV2::new();
1531 let mut collector = FlowCollectorV2::new(Some(module_path), ®istry, "test_crate");
1532 collector.visit_file(&file);
1533 collector.apply_to(&mut graph);
1534 graph
1535 }
1536
1537 fn make_fn(name: &str, stmts: Vec<PureStmt>) -> PureFn {
1538 PureFn {
1539 attrs: vec![],
1540 vis: PureVis::Public,
1541 is_async: false,
1542 is_async_inferred: false,
1543 is_const: false,
1544 is_unsafe: false,
1545 abi: None,
1546 name: name.to_string(),
1547 generics: PureGenerics::default(),
1548 params: vec![],
1549 ret: None,
1550 body: PureBlock { stmts },
1551 }
1552 }
1553
1554 fn find_var(graph: &DataFlowGraphV2, name: &str) -> Option<VarId> {
1555 graph
1556 .iter_vars()
1557 .find(|(vid, _)| graph.var_name(*vid) == Some(name))
1558 .map(|(vid, _)| vid)
1559 }
1560
1561 fn outgoing_kinds(graph: &DataFlowGraphV2, var: VarId) -> Vec<FlowKind> {
1562 graph
1563 .outgoing(var)
1564 .iter()
1565 .filter_map(|&fid| graph.flow(fid).map(|f| f.kind))
1566 .collect()
1567 }
1568
1569 #[test]
1572 fn test_call_arg_generates_argument_flow() {
1573 let stmts = vec![
1575 PureStmt::Local {
1576 pattern: PurePattern::Ident {
1577 name: "x".into(),
1578 is_mut: false,
1579 by_ref: false,
1580 },
1581 ty: None,
1582 init: Some(PureExpr::Lit("1".into())),
1583 else_branch: None,
1584 },
1585 PureStmt::Semi(PureExpr::Call {
1586 func: Box::new(PureExpr::Path("foo".into())),
1587 args: vec![PureExpr::Path("x".into())],
1588 }),
1589 ];
1590
1591 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1592 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1593 let kinds = outgoing_kinds(&graph, x);
1594
1595 assert!(
1596 kinds.contains(&FlowKind::Argument),
1597 "x passed to foo() should have Argument flow, got: {:?}",
1598 kinds
1599 );
1600 }
1601
1602 #[test]
1605 fn test_method_receiver_generates_read_flow() {
1606 let stmts = vec![
1608 PureStmt::Local {
1609 pattern: PurePattern::Ident {
1610 name: "x".into(),
1611 is_mut: false,
1612 by_ref: false,
1613 },
1614 ty: None,
1615 init: Some(PureExpr::Lit("1".into())),
1616 else_branch: None,
1617 },
1618 PureStmt::Semi(PureExpr::MethodCall {
1619 receiver: Box::new(PureExpr::Path("x".into())),
1620 method: "push".into(),
1621 turbofish: None,
1622 args: vec![PureExpr::Lit("1".into())],
1623 }),
1624 ];
1625
1626 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1627 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1628 let kinds = outgoing_kinds(&graph, x);
1629
1630 assert!(
1631 kinds.contains(&FlowKind::Read),
1632 "x used as method receiver should have Read flow, got: {:?}",
1633 kinds
1634 );
1635 }
1636
1637 #[test]
1640 fn test_return_generates_return_flow() {
1641 let stmts = vec![
1643 PureStmt::Local {
1644 pattern: PurePattern::Ident {
1645 name: "x".into(),
1646 is_mut: false,
1647 by_ref: false,
1648 },
1649 ty: None,
1650 init: Some(PureExpr::Lit("1".into())),
1651 else_branch: None,
1652 },
1653 PureStmt::Semi(PureExpr::Return(Some(Box::new(PureExpr::Path("x".into()))))),
1654 ];
1655
1656 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1657 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1658 let kinds = outgoing_kinds(&graph, x);
1659
1660 assert!(
1661 kinds.contains(&FlowKind::Return),
1662 "x in return expression should have Return flow, got: {:?}",
1663 kinds
1664 );
1665 }
1666
1667 #[test]
1670 fn test_shared_borrow_generates_borrow_flow() {
1671 let stmts = vec![
1673 PureStmt::Local {
1674 pattern: PurePattern::Ident {
1675 name: "x".into(),
1676 is_mut: false,
1677 by_ref: false,
1678 },
1679 ty: None,
1680 init: Some(PureExpr::Lit("1".into())),
1681 else_branch: None,
1682 },
1683 PureStmt::Local {
1684 pattern: PurePattern::Ident {
1685 name: "y".into(),
1686 is_mut: false,
1687 by_ref: false,
1688 },
1689 ty: None,
1690 init: Some(PureExpr::Ref {
1691 is_mut: false,
1692 expr: Box::new(PureExpr::Path("x".into())),
1693 }),
1694 else_branch: None,
1695 },
1696 ];
1697
1698 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1699 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1700 let kinds = outgoing_kinds(&graph, x);
1701
1702 assert!(
1703 kinds.contains(&FlowKind::SharedBorrow),
1704 "x in &x should have SharedBorrow flow, got: {:?}",
1705 kinds
1706 );
1707 }
1708
1709 #[test]
1710 fn test_mut_borrow_generates_mut_borrow_flow() {
1711 let stmts = vec![
1713 PureStmt::Local {
1714 pattern: PurePattern::Ident {
1715 name: "x".into(),
1716 is_mut: true,
1717 by_ref: false,
1718 },
1719 ty: None,
1720 init: Some(PureExpr::Lit("1".into())),
1721 else_branch: None,
1722 },
1723 PureStmt::Local {
1724 pattern: PurePattern::Ident {
1725 name: "y".into(),
1726 is_mut: false,
1727 by_ref: false,
1728 },
1729 ty: None,
1730 init: Some(PureExpr::Ref {
1731 is_mut: true,
1732 expr: Box::new(PureExpr::Path("x".into())),
1733 }),
1734 else_branch: None,
1735 },
1736 ];
1737
1738 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1739 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1740 let kinds = outgoing_kinds(&graph, x);
1741
1742 assert!(
1743 kinds.contains(&FlowKind::MutBorrow),
1744 "x in &mut x should have MutBorrow flow, got: {:?}",
1745 kinds
1746 );
1747 }
1748
1749 #[test]
1752 fn test_method_call_arg_generates_argument_flow() {
1753 let stmts = vec![
1755 PureStmt::Local {
1756 pattern: PurePattern::Ident {
1757 name: "x".into(),
1758 is_mut: false,
1759 by_ref: false,
1760 },
1761 ty: None,
1762 init: Some(PureExpr::Lit("1".into())),
1763 else_branch: None,
1764 },
1765 PureStmt::Local {
1766 pattern: PurePattern::Ident {
1767 name: "y".into(),
1768 is_mut: false,
1769 by_ref: false,
1770 },
1771 ty: None,
1772 init: Some(PureExpr::Lit("2".into())),
1773 else_branch: None,
1774 },
1775 PureStmt::Semi(PureExpr::MethodCall {
1776 receiver: Box::new(PureExpr::Path("y".into())),
1777 method: "foo".into(),
1778 turbofish: None,
1779 args: vec![PureExpr::Path("x".into())],
1780 }),
1781 ];
1782
1783 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1784 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1785 let kinds = outgoing_kinds(&graph, x);
1786
1787 assert!(
1788 kinds.contains(&FlowKind::Argument),
1789 "x passed as method arg should have Argument flow, got: {:?}",
1790 kinds
1791 );
1792 }
1793
1794 #[test]
1797 fn test_for_loop_creates_assign_to_pattern_var() {
1798 let stmts = vec![
1800 PureStmt::Local {
1801 pattern: PurePattern::Ident {
1802 name: "items".into(),
1803 is_mut: false,
1804 by_ref: false,
1805 },
1806 ty: None,
1807 init: Some(PureExpr::Lit("1".into())),
1808 else_branch: None,
1809 },
1810 PureStmt::Semi(PureExpr::For {
1811 label: None,
1812 pat: PurePattern::Ident {
1813 name: "item".into(),
1814 is_mut: false,
1815 by_ref: false,
1816 },
1817 expr: Box::new(PureExpr::Path("items".into())),
1818 body: PureBlock { stmts: vec![] },
1819 }),
1820 ];
1821
1822 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1823 let items = find_var(&graph, "items").expect("variable 'items' should exist");
1824 let kinds = outgoing_kinds(&graph, items);
1825
1826 assert!(
1827 kinds.contains(&FlowKind::Read),
1828 "items used as for-loop iterator should have Read flow, got: {:?}",
1829 kinds
1830 );
1831
1832 let item = find_var(&graph, "item").expect("loop variable 'item' should exist");
1833 let incoming: Vec<FlowKind> = graph
1834 .incoming(item)
1835 .iter()
1836 .filter_map(|&fid| graph.flow(fid).map(|f| f.kind))
1837 .collect();
1838 assert!(
1839 incoming.contains(&FlowKind::Assign),
1840 "item should have incoming Assign from iterator, got: {:?}",
1841 incoming
1842 );
1843 }
1844
1845 #[test]
1848 fn test_break_with_value_generates_read_flow() {
1849 let stmts = vec![
1851 PureStmt::Local {
1852 pattern: PurePattern::Ident {
1853 name: "x".into(),
1854 is_mut: false,
1855 by_ref: false,
1856 },
1857 ty: None,
1858 init: Some(PureExpr::Lit("1".into())),
1859 else_branch: None,
1860 },
1861 PureStmt::Semi(PureExpr::Loop {
1862 label: None,
1863 body: PureBlock {
1864 stmts: vec![PureStmt::Semi(PureExpr::Break {
1865 label: None,
1866 expr: Some(Box::new(PureExpr::Path("x".into()))),
1867 })],
1868 },
1869 }),
1870 ];
1871
1872 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1873 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1874 let kinds = outgoing_kinds(&graph, x);
1875
1876 assert!(
1877 !kinds.is_empty(),
1878 "x in break expression should have outgoing flow, got: {:?}",
1879 kinds
1880 );
1881 }
1882
1883 #[test]
1886 fn test_closure_capture_generates_flow() {
1887 let stmts = vec![
1889 PureStmt::Local {
1890 pattern: PurePattern::Ident {
1891 name: "x".into(),
1892 is_mut: false,
1893 by_ref: false,
1894 },
1895 ty: None,
1896 init: Some(PureExpr::Lit("1".into())),
1897 else_branch: None,
1898 },
1899 PureStmt::Local {
1900 pattern: PurePattern::Ident {
1901 name: "f".into(),
1902 is_mut: false,
1903 by_ref: false,
1904 },
1905 ty: None,
1906 init: Some(PureExpr::Closure {
1907 is_async: false,
1908 is_move: false,
1909 params: vec![],
1910 ret: None,
1911 body: Box::new(PureExpr::Binary {
1912 op: "+".into(),
1913 left: Box::new(PureExpr::Path("x".into())),
1914 right: Box::new(PureExpr::Lit("1".into())),
1915 }),
1916 }),
1917 else_branch: None,
1918 },
1919 ];
1920
1921 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1922 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1923 let kinds = outgoing_kinds(&graph, x);
1924
1925 assert!(
1926 !kinds.is_empty(),
1927 "x captured by closure should have outgoing flow, got: {:?}",
1928 kinds
1929 );
1930 }
1931
1932 #[test]
1933 fn test_move_closure_capture_generates_move_flow() {
1934 let stmts = vec![
1936 PureStmt::Local {
1937 pattern: PurePattern::Ident {
1938 name: "x".into(),
1939 is_mut: false,
1940 by_ref: false,
1941 },
1942 ty: None,
1943 init: Some(PureExpr::Lit("1".into())),
1944 else_branch: None,
1945 },
1946 PureStmt::Local {
1947 pattern: PurePattern::Ident {
1948 name: "f".into(),
1949 is_mut: false,
1950 by_ref: false,
1951 },
1952 ty: None,
1953 init: Some(PureExpr::Closure {
1954 is_async: false,
1955 is_move: true,
1956 params: vec![],
1957 ret: None,
1958 body: Box::new(PureExpr::Binary {
1959 op: "+".into(),
1960 left: Box::new(PureExpr::Path("x".into())),
1961 right: Box::new(PureExpr::Lit("1".into())),
1962 }),
1963 }),
1964 else_branch: None,
1965 },
1966 ];
1967
1968 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
1969 let x = find_var(&graph, "x").expect("variable 'x' should exist");
1970 let kinds = outgoing_kinds(&graph, x);
1971
1972 assert!(
1973 kinds.contains(&FlowKind::Move),
1974 "x captured by move closure should have Move flow, got: {:?}",
1975 kinds
1976 );
1977 }
1978
1979 #[test]
1982 fn test_match_arm_pattern_creates_variable_with_assign() {
1983 let stmts = vec![
1986 PureStmt::Local {
1987 pattern: PurePattern::Ident {
1988 name: "x".into(),
1989 is_mut: false,
1990 by_ref: false,
1991 },
1992 ty: None,
1993 init: Some(PureExpr::Lit("1".into())),
1994 else_branch: None,
1995 },
1996 PureStmt::Semi(PureExpr::Match {
1997 expr: Box::new(PureExpr::Path("x".into())),
1998 arms: vec![PureMatchArm {
1999 pattern: PurePattern::Ident {
2000 name: "val".into(),
2001 is_mut: false,
2002 by_ref: false,
2003 },
2004 guard: None,
2005 body: PureExpr::Block {
2006 label: None,
2007 block: PureBlock { stmts: vec![] },
2008 },
2009 }],
2010 }),
2011 ];
2012
2013 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2014 let val = find_var(&graph, "val").expect("match arm variable 'val' should exist");
2015 let incoming: Vec<FlowKind> = graph
2016 .incoming(val)
2017 .iter()
2018 .filter_map(|&fid| graph.flow(fid).map(|f| f.kind))
2019 .collect();
2020 assert!(
2021 incoming.contains(&FlowKind::Assign),
2022 "val should have incoming Assign from match scrutinee, got: {:?}",
2023 incoming
2024 );
2025 }
2026
2027 #[test]
2030 fn test_match_scrutinee_generates_read_flow() {
2031 let stmts = vec![
2034 PureStmt::Local {
2035 pattern: PurePattern::Ident {
2036 name: "x".into(),
2037 is_mut: false,
2038 by_ref: false,
2039 },
2040 ty: None,
2041 init: Some(PureExpr::Lit("1".into())),
2042 else_branch: None,
2043 },
2044 PureStmt::Semi(PureExpr::Match {
2045 expr: Box::new(PureExpr::Path("x".into())),
2046 arms: vec![PureMatchArm {
2047 pattern: PurePattern::Wild,
2048 guard: None,
2049 body: PureExpr::Block {
2050 label: None,
2051 block: PureBlock { stmts: vec![] },
2052 },
2053 }],
2054 }),
2055 ];
2056
2057 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2058 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2059 let kinds = outgoing_kinds(&graph, x);
2060
2061 assert!(
2062 !kinds.is_empty(),
2063 "x used as match scrutinee should have outgoing flow, got: {:?}",
2064 kinds
2065 );
2066 }
2067
2068 #[test]
2071 fn test_match_expr_as_init_collects_arm_body_sources() {
2072 let stmts = vec![
2075 PureStmt::Local {
2076 pattern: PurePattern::Ident {
2077 name: "a".into(),
2078 is_mut: false,
2079 by_ref: false,
2080 },
2081 ty: None,
2082 init: Some(PureExpr::Lit("1".into())),
2083 else_branch: None,
2084 },
2085 PureStmt::Local {
2086 pattern: PurePattern::Ident {
2087 name: "b".into(),
2088 is_mut: false,
2089 by_ref: false,
2090 },
2091 ty: None,
2092 init: Some(PureExpr::Lit("2".into())),
2093 else_branch: None,
2094 },
2095 PureStmt::Local {
2096 pattern: PurePattern::Ident {
2097 name: "result".into(),
2098 is_mut: false,
2099 by_ref: false,
2100 },
2101 ty: None,
2102 init: Some(PureExpr::Match {
2103 expr: Box::new(PureExpr::Lit("0".into())),
2104 arms: vec![
2105 PureMatchArm {
2106 pattern: PurePattern::Lit("1".into()),
2107 guard: None,
2108 body: PureExpr::Path("a".into()),
2109 },
2110 PureMatchArm {
2111 pattern: PurePattern::Wild,
2112 guard: None,
2113 body: PureExpr::Path("b".into()),
2114 },
2115 ],
2116 }),
2117 else_branch: None,
2118 },
2119 ];
2120
2121 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2122 let result = find_var(&graph, "result").expect("variable 'result' should exist");
2123 let incoming: Vec<FlowKind> = graph
2124 .incoming(result)
2125 .iter()
2126 .filter_map(|&fid| graph.flow(fid).map(|f| f.kind))
2127 .collect();
2128 assert!(
2129 incoming.len() >= 2,
2130 "result should have incoming Assign from both arms, got: {:?}",
2131 incoming
2132 );
2133 }
2134
2135 #[test]
2138 fn test_struct_expr_field_values_generate_read_flow() {
2139 let stmts = vec![
2142 PureStmt::Local {
2143 pattern: PurePattern::Ident {
2144 name: "x".into(),
2145 is_mut: false,
2146 by_ref: false,
2147 },
2148 ty: None,
2149 init: Some(PureExpr::Lit("1".into())),
2150 else_branch: None,
2151 },
2152 PureStmt::Local {
2153 pattern: PurePattern::Ident {
2154 name: "y".into(),
2155 is_mut: false,
2156 by_ref: false,
2157 },
2158 ty: None,
2159 init: Some(PureExpr::Lit("2".into())),
2160 else_branch: None,
2161 },
2162 PureStmt::Local {
2163 pattern: PurePattern::Ident {
2164 name: "s".into(),
2165 is_mut: false,
2166 by_ref: false,
2167 },
2168 ty: None,
2169 init: Some(PureExpr::Struct {
2170 path: "Foo".into(),
2171 fields: vec![
2172 ("a".into(), PureExpr::Path("x".into())),
2173 ("b".into(), PureExpr::Path("y".into())),
2174 ],
2175 rest: None,
2176 }),
2177 else_branch: None,
2178 },
2179 ];
2180
2181 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2182 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2183 let y = find_var(&graph, "y").expect("variable 'y' should exist");
2184
2185 assert!(
2186 !outgoing_kinds(&graph, x).is_empty(),
2187 "x used in struct field should have outgoing flow, got: {:?}",
2188 outgoing_kinds(&graph, x)
2189 );
2190 assert!(
2191 !outgoing_kinds(&graph, y).is_empty(),
2192 "y used in struct field should have outgoing flow, got: {:?}",
2193 outgoing_kinds(&graph, y)
2194 );
2195 }
2196
2197 #[test]
2200 fn test_struct_expr_as_stmt_generates_read_flow() {
2201 let stmts = vec![
2204 PureStmt::Local {
2205 pattern: PurePattern::Ident {
2206 name: "x".into(),
2207 is_mut: false,
2208 by_ref: false,
2209 },
2210 ty: None,
2211 init: Some(PureExpr::Lit("1".into())),
2212 else_branch: None,
2213 },
2214 PureStmt::Semi(PureExpr::Struct {
2215 path: "Foo".into(),
2216 fields: vec![("a".into(), PureExpr::Path("x".into()))],
2217 rest: None,
2218 }),
2219 ];
2220
2221 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2222 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2223 let kinds = outgoing_kinds(&graph, x);
2224
2225 assert!(
2226 !kinds.is_empty(),
2227 "x used in struct expr statement should have outgoing flow, got: {:?}",
2228 kinds
2229 );
2230 }
2231
2232 #[test]
2235 fn test_field_assign_rhs_generates_read_flow() {
2236 let stmts = vec![
2239 PureStmt::Local {
2240 pattern: PurePattern::Ident {
2241 name: "x".into(),
2242 is_mut: false,
2243 by_ref: false,
2244 },
2245 ty: None,
2246 init: Some(PureExpr::Lit("1".into())),
2247 else_branch: None,
2248 },
2249 PureStmt::Semi(PureExpr::Binary {
2250 op: "=".into(),
2251 left: Box::new(PureExpr::Field {
2252 expr: Box::new(PureExpr::Path("self".into())),
2253 field: "data".into(),
2254 }),
2255 right: Box::new(PureExpr::Path("x".into())),
2256 }),
2257 ];
2258
2259 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2260 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2261 let kinds = outgoing_kinds(&graph, x);
2262
2263 assert!(
2264 !kinds.is_empty(),
2265 "x assigned to self.data should have outgoing flow, got: {:?}",
2266 kinds
2267 );
2268 }
2269
2270 #[test]
2273 fn test_async_move_block_captures_generate_flow() {
2274 let stmts = vec![
2277 PureStmt::Local {
2278 pattern: PurePattern::Ident {
2279 name: "x".into(),
2280 is_mut: false,
2281 by_ref: false,
2282 },
2283 ty: None,
2284 init: Some(PureExpr::Lit("1".into())),
2285 else_branch: None,
2286 },
2287 PureStmt::Semi(PureExpr::Async {
2288 is_move: true,
2289 body: PureBlock {
2290 stmts: vec![PureStmt::Expr(PureExpr::Binary {
2291 op: "+".into(),
2292 left: Box::new(PureExpr::Path("x".into())),
2293 right: Box::new(PureExpr::Lit("1".into())),
2294 })],
2295 },
2296 }),
2297 ];
2298
2299 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2300 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2301 let kinds = outgoing_kinds(&graph, x);
2302
2303 assert!(
2304 !kinds.is_empty(),
2305 "x captured by async move should have outgoing flow, got: {:?}",
2306 kinds
2307 );
2308 }
2309
2310 #[test]
2313 fn test_closure_captures_var_in_non_tail_stmt() {
2314 let stmts = vec![
2317 PureStmt::Local {
2318 pattern: PurePattern::Ident {
2319 name: "x".into(),
2320 is_mut: false,
2321 by_ref: false,
2322 },
2323 ty: None,
2324 init: Some(PureExpr::Lit("1".into())),
2325 else_branch: None,
2326 },
2327 PureStmt::Local {
2328 pattern: PurePattern::Ident {
2329 name: "f".into(),
2330 is_mut: false,
2331 by_ref: false,
2332 },
2333 ty: None,
2334 init: Some(PureExpr::Closure {
2335 is_async: false,
2336 is_move: true,
2337 params: vec![],
2338 ret: None,
2339 body: Box::new(PureExpr::Block {
2340 label: None,
2341 block: PureBlock {
2342 stmts: vec![PureStmt::Local {
2343 pattern: PurePattern::Ident {
2344 name: "y".into(),
2345 is_mut: false,
2346 by_ref: false,
2347 },
2348 ty: None,
2349 init: Some(PureExpr::Path("x".into())),
2350 else_branch: None,
2351 }],
2352 },
2353 }),
2354 }),
2355 else_branch: None,
2356 },
2357 ];
2358
2359 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2360 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2361 let kinds = outgoing_kinds(&graph, x);
2362
2363 assert!(
2364 kinds.contains(&FlowKind::Move),
2365 "x captured in non-tail stmt of move closure should have Move flow, got: {:?}",
2366 kinds
2367 );
2368 }
2369
2370 #[test]
2373 fn test_while_condition_generates_read_flow() {
2374 let stmts = vec![
2377 PureStmt::Local {
2378 pattern: PurePattern::Ident {
2379 name: "x".into(),
2380 is_mut: false,
2381 by_ref: false,
2382 },
2383 ty: None,
2384 init: Some(PureExpr::Lit("1".into())),
2385 else_branch: None,
2386 },
2387 PureStmt::Semi(PureExpr::While {
2388 label: None,
2389 cond: Box::new(PureExpr::Binary {
2390 op: ">".into(),
2391 left: Box::new(PureExpr::Path("x".into())),
2392 right: Box::new(PureExpr::Lit("0".into())),
2393 }),
2394 body: PureBlock { stmts: vec![] },
2395 }),
2396 ];
2397
2398 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2399 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2400 let kinds = outgoing_kinds(&graph, x);
2401
2402 assert!(
2403 !kinds.is_empty(),
2404 "x used in while condition should have outgoing flow, got: {:?}",
2405 kinds
2406 );
2407 }
2408
2409 #[test]
2412 fn test_tail_expr_generates_read_flow() {
2413 let stmts = vec![
2416 PureStmt::Local {
2417 pattern: PurePattern::Ident {
2418 name: "x".into(),
2419 is_mut: false,
2420 by_ref: false,
2421 },
2422 ty: None,
2423 init: Some(PureExpr::Lit("1".into())),
2424 else_branch: None,
2425 },
2426 PureStmt::Expr(PureExpr::Tuple(vec![
2427 PureExpr::Path("x".into()),
2428 PureExpr::Lit("2".into()),
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 in tail expression should have outgoing flow, got: {:?}",
2439 kinds
2440 );
2441 }
2442
2443 #[test]
2446 fn test_macro_expr_variable_generates_read_flow() {
2447 let stmts = vec![
2450 PureStmt::Local {
2451 pattern: PurePattern::Ident {
2452 name: "x".into(),
2453 is_mut: false,
2454 by_ref: false,
2455 },
2456 ty: None,
2457 init: Some(PureExpr::Lit("1".into())),
2458 else_branch: None,
2459 },
2460 PureStmt::Semi(PureExpr::Macro {
2461 name: "println".into(),
2462 delimiter: MacroDelimiter::Paren,
2463 tokens: "\"{}\", x".into(),
2464 }),
2465 ];
2466
2467 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2468 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2469 let kinds = outgoing_kinds(&graph, x);
2470
2471 assert!(
2472 !kinds.is_empty(),
2473 "x referenced in macro tokens should have outgoing flow, got: {:?}",
2474 kinds
2475 );
2476 }
2477
2478 #[test]
2481 fn test_while_let_condition_generates_read_flow() {
2482 let stmts = vec![
2485 PureStmt::Local {
2486 pattern: PurePattern::Ident {
2487 name: "x".into(),
2488 is_mut: false,
2489 by_ref: false,
2490 },
2491 ty: None,
2492 init: Some(PureExpr::Lit("1".into())),
2493 else_branch: None,
2494 },
2495 PureStmt::Semi(PureExpr::While {
2496 label: None,
2497 cond: Box::new(PureExpr::Let {
2498 pattern: PurePattern::Ident {
2499 name: "v".into(),
2500 is_mut: false,
2501 by_ref: false,
2502 },
2503 expr: Box::new(PureExpr::MethodCall {
2504 receiver: Box::new(PureExpr::Path("x".into())),
2505 method: "parent".into(),
2506 turbofish: None,
2507 args: vec![],
2508 }),
2509 }),
2510 body: PureBlock { stmts: vec![] },
2511 }),
2512 ];
2513
2514 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2515 let x = find_var(&graph, "x").expect("variable 'x' should exist");
2516 let kinds = outgoing_kinds(&graph, x);
2517
2518 assert!(
2519 !kinds.is_empty(),
2520 "x used in while-let condition should have outgoing flow, got: {:?}",
2521 kinds
2522 );
2523 }
2524
2525 #[test]
2528 fn test_lock_direct_call_detected() {
2529 let stmts = vec![
2531 PureStmt::Local {
2532 pattern: PurePattern::Ident {
2533 name: "m".into(),
2534 is_mut: false,
2535 by_ref: false,
2536 },
2537 ty: None,
2538 init: Some(PureExpr::Lit("1".into())),
2539 else_branch: None,
2540 },
2541 PureStmt::Local {
2542 pattern: PurePattern::Ident {
2543 name: "guard".into(),
2544 is_mut: false,
2545 by_ref: false,
2546 },
2547 ty: None,
2548 init: Some(PureExpr::MethodCall {
2549 receiver: Box::new(PureExpr::Path("m".into())),
2550 method: "lock".into(),
2551 turbofish: None,
2552 args: vec![],
2553 }),
2554 else_branch: None,
2555 },
2556 ];
2557
2558 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2559 let tracker = graph.lock_tracker();
2560 assert_eq!(
2561 tracker.acquisitions().len(),
2562 1,
2563 "should detect 1 lock acquisition"
2564 );
2565 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::Mutex);
2566 assert!(!tracker.acquisitions()[0].is_try);
2567 assert_eq!(tracker.acquisitions()[0].lock_name, "m");
2568 assert_eq!(tracker.acquisitions()[0].guard_name, "guard");
2569 }
2570
2571 #[test]
2572 fn test_lock_unwrap_pattern_detected() {
2573 let stmts = vec![
2575 PureStmt::Local {
2576 pattern: PurePattern::Ident {
2577 name: "m".into(),
2578 is_mut: false,
2579 by_ref: false,
2580 },
2581 ty: None,
2582 init: Some(PureExpr::Lit("1".into())),
2583 else_branch: None,
2584 },
2585 PureStmt::Local {
2586 pattern: PurePattern::Ident {
2587 name: "guard".into(),
2588 is_mut: false,
2589 by_ref: false,
2590 },
2591 ty: None,
2592 init: Some(PureExpr::MethodCall {
2593 receiver: Box::new(PureExpr::MethodCall {
2594 receiver: Box::new(PureExpr::Path("m".into())),
2595 method: "lock".into(),
2596 turbofish: None,
2597 args: vec![],
2598 }),
2599 method: "unwrap".into(),
2600 turbofish: None,
2601 args: vec![],
2602 }),
2603 else_branch: None,
2604 },
2605 ];
2606
2607 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2608 let tracker = graph.lock_tracker();
2609 assert_eq!(
2610 tracker.acquisitions().len(),
2611 1,
2612 "should detect lock().unwrap() pattern"
2613 );
2614 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::Mutex);
2615 }
2616
2617 #[test]
2618 fn test_rwlock_read_write_detected() {
2619 let stmts = vec![
2621 PureStmt::Local {
2622 pattern: PurePattern::Ident {
2623 name: "rw".into(),
2624 is_mut: false,
2625 by_ref: false,
2626 },
2627 ty: None,
2628 init: Some(PureExpr::Lit("1".into())),
2629 else_branch: None,
2630 },
2631 PureStmt::Local {
2632 pattern: PurePattern::Ident {
2633 name: "r".into(),
2634 is_mut: false,
2635 by_ref: false,
2636 },
2637 ty: None,
2638 init: Some(PureExpr::MethodCall {
2639 receiver: Box::new(PureExpr::Path("rw".into())),
2640 method: "read".into(),
2641 turbofish: None,
2642 args: vec![],
2643 }),
2644 else_branch: None,
2645 },
2646 PureStmt::Local {
2647 pattern: PurePattern::Ident {
2648 name: "w".into(),
2649 is_mut: false,
2650 by_ref: false,
2651 },
2652 ty: None,
2653 init: Some(PureExpr::MethodCall {
2654 receiver: Box::new(PureExpr::Path("rw".into())),
2655 method: "write".into(),
2656 turbofish: None,
2657 args: vec![],
2658 }),
2659 else_branch: None,
2660 },
2661 ];
2662
2663 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2664 let tracker = graph.lock_tracker();
2665 assert_eq!(
2666 tracker.acquisitions().len(),
2667 2,
2668 "should detect both read and write locks"
2669 );
2670 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::RwLockRead);
2671 assert_eq!(tracker.acquisitions()[1].lock_type, LockType::RwLockWrite);
2672 }
2673
2674 #[test]
2675 fn test_try_lock_detected() {
2676 let stmts = vec![
2678 PureStmt::Local {
2679 pattern: PurePattern::Ident {
2680 name: "m".into(),
2681 is_mut: false,
2682 by_ref: false,
2683 },
2684 ty: None,
2685 init: Some(PureExpr::Lit("1".into())),
2686 else_branch: None,
2687 },
2688 PureStmt::Local {
2689 pattern: PurePattern::Ident {
2690 name: "guard".into(),
2691 is_mut: false,
2692 by_ref: false,
2693 },
2694 ty: None,
2695 init: Some(PureExpr::MethodCall {
2696 receiver: Box::new(PureExpr::Path("m".into())),
2697 method: "try_lock".into(),
2698 turbofish: None,
2699 args: vec![],
2700 }),
2701 else_branch: None,
2702 },
2703 ];
2704
2705 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2706 let tracker = graph.lock_tracker();
2707 assert_eq!(tracker.acquisitions().len(), 1, "should detect try_lock");
2708 assert!(tracker.acquisitions()[0].is_try);
2709 }
2710
2711 #[test]
2712 fn test_lock_await_pattern_detected() {
2713 let stmts = vec![
2715 PureStmt::Local {
2716 pattern: PurePattern::Ident {
2717 name: "m".into(),
2718 is_mut: false,
2719 by_ref: false,
2720 },
2721 ty: None,
2722 init: Some(PureExpr::Lit("1".into())),
2723 else_branch: None,
2724 },
2725 PureStmt::Local {
2726 pattern: PurePattern::Ident {
2727 name: "guard".into(),
2728 is_mut: false,
2729 by_ref: false,
2730 },
2731 ty: None,
2732 init: Some(PureExpr::Await(Box::new(PureExpr::MethodCall {
2733 receiver: Box::new(PureExpr::Path("m".into())),
2734 method: "lock".into(),
2735 turbofish: None,
2736 args: vec![],
2737 }))),
2738 else_branch: None,
2739 },
2740 ];
2741
2742 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2743 let tracker = graph.lock_tracker();
2744 assert_eq!(
2745 tracker.acquisitions().len(),
2746 1,
2747 "should detect lock().await pattern"
2748 );
2749 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::Mutex);
2750 }
2751
2752 #[test]
2753 fn test_borrow_borrow_mut_detected() {
2754 let stmts = vec![
2756 PureStmt::Local {
2757 pattern: PurePattern::Ident {
2758 name: "cell".into(),
2759 is_mut: false,
2760 by_ref: false,
2761 },
2762 ty: None,
2763 init: Some(PureExpr::Lit("1".into())),
2764 else_branch: None,
2765 },
2766 PureStmt::Local {
2767 pattern: PurePattern::Ident {
2768 name: "r".into(),
2769 is_mut: false,
2770 by_ref: false,
2771 },
2772 ty: None,
2773 init: Some(PureExpr::MethodCall {
2774 receiver: Box::new(PureExpr::Path("cell".into())),
2775 method: "borrow".into(),
2776 turbofish: None,
2777 args: vec![],
2778 }),
2779 else_branch: None,
2780 },
2781 PureStmt::Local {
2782 pattern: PurePattern::Ident {
2783 name: "w".into(),
2784 is_mut: false,
2785 by_ref: false,
2786 },
2787 ty: None,
2788 init: Some(PureExpr::MethodCall {
2789 receiver: Box::new(PureExpr::Path("cell".into())),
2790 method: "borrow_mut".into(),
2791 turbofish: None,
2792 args: vec![],
2793 }),
2794 else_branch: None,
2795 },
2796 ];
2797
2798 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2799 let tracker = graph.lock_tracker();
2800 assert_eq!(
2801 tracker.acquisitions().len(),
2802 2,
2803 "should detect borrow and borrow_mut"
2804 );
2805 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::RefCell);
2806 assert_eq!(tracker.acquisitions()[1].lock_type, LockType::RefCellMut);
2807 }
2808
2809 #[test]
2810 fn test_non_lock_method_not_detected() {
2811 let stmts = vec![
2813 PureStmt::Local {
2814 pattern: PurePattern::Ident {
2815 name: "x".into(),
2816 is_mut: false,
2817 by_ref: false,
2818 },
2819 ty: None,
2820 init: Some(PureExpr::Lit("1".into())),
2821 else_branch: None,
2822 },
2823 PureStmt::Local {
2824 pattern: PurePattern::Ident {
2825 name: "y".into(),
2826 is_mut: false,
2827 by_ref: false,
2828 },
2829 ty: None,
2830 init: Some(PureExpr::MethodCall {
2831 receiver: Box::new(PureExpr::Path("x".into())),
2832 method: "foo".into(),
2833 turbofish: None,
2834 args: vec![],
2835 }),
2836 else_branch: None,
2837 },
2838 ];
2839
2840 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2841 let tracker = graph.lock_tracker();
2842 assert_eq!(
2843 tracker.acquisitions().len(),
2844 0,
2845 "non-lock method should not be detected"
2846 );
2847 }
2848
2849 #[test]
2850 fn test_guard_field_read_tracked() {
2851 let stmts = vec![
2853 PureStmt::Local {
2854 pattern: PurePattern::Ident {
2855 name: "m".into(),
2856 is_mut: false,
2857 by_ref: false,
2858 },
2859 ty: None,
2860 init: Some(PureExpr::Lit("1".into())),
2861 else_branch: None,
2862 },
2863 PureStmt::Local {
2864 pattern: PurePattern::Ident {
2865 name: "guard".into(),
2866 is_mut: false,
2867 by_ref: false,
2868 },
2869 ty: None,
2870 init: Some(PureExpr::MethodCall {
2871 receiver: Box::new(PureExpr::Path("m".into())),
2872 method: "lock".into(),
2873 turbofish: None,
2874 args: vec![],
2875 }),
2876 else_branch: None,
2877 },
2878 PureStmt::Local {
2880 pattern: PurePattern::Wild,
2881 ty: None,
2882 init: Some(PureExpr::Field {
2883 expr: Box::new(PureExpr::Path("guard".into())),
2884 field: "counter".into(),
2885 }),
2886 else_branch: None,
2887 },
2888 ];
2889
2890 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2891 let tracker = graph.lock_tracker();
2892 assert_eq!(tracker.acquisitions().len(), 1);
2893 let sections = tracker.critical_sections();
2894 assert_eq!(sections.len(), 1, "should have 1 completed section");
2895 assert_eq!(
2896 sections[0].field_accesses.len(),
2897 1,
2898 "should track 1 field access"
2899 );
2900 assert_eq!(sections[0].field_accesses[0].field_name, "counter");
2901 assert_eq!(
2902 sections[0].field_accesses[0].access_kind,
2903 super::super::lock_v2::AccessKind::Read
2904 );
2905 }
2906
2907 #[test]
2908 fn test_guard_field_write_tracked() {
2909 let stmts = vec![
2911 PureStmt::Local {
2912 pattern: PurePattern::Ident {
2913 name: "m".into(),
2914 is_mut: false,
2915 by_ref: false,
2916 },
2917 ty: None,
2918 init: Some(PureExpr::Lit("1".into())),
2919 else_branch: None,
2920 },
2921 PureStmt::Local {
2922 pattern: PurePattern::Ident {
2923 name: "guard".into(),
2924 is_mut: false,
2925 by_ref: false,
2926 },
2927 ty: None,
2928 init: Some(PureExpr::MethodCall {
2929 receiver: Box::new(PureExpr::Path("m".into())),
2930 method: "lock".into(),
2931 turbofish: None,
2932 args: vec![],
2933 }),
2934 else_branch: None,
2935 },
2936 PureStmt::Semi(PureExpr::Binary {
2938 op: "=".into(),
2939 left: Box::new(PureExpr::Field {
2940 expr: Box::new(PureExpr::Path("guard".into())),
2941 field: "counter".into(),
2942 }),
2943 right: Box::new(PureExpr::Lit("42".into())),
2944 }),
2945 ];
2946
2947 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2948 let tracker = graph.lock_tracker();
2949 assert_eq!(tracker.acquisitions().len(), 1);
2950 let sections = tracker.critical_sections();
2951 assert_eq!(sections.len(), 1);
2952 assert_eq!(sections[0].field_accesses.len(), 1);
2953 assert_eq!(sections[0].field_accesses[0].field_name, "counter");
2954 assert_eq!(
2955 sections[0].field_accesses[0].access_kind,
2956 super::super::lock_v2::AccessKind::Write
2957 );
2958 }
2959
2960 #[test]
2961 fn test_non_guard_field_not_tracked() {
2962 let stmts = vec![
2965 PureStmt::Local {
2966 pattern: PurePattern::Ident {
2967 name: "x".into(),
2968 is_mut: false,
2969 by_ref: false,
2970 },
2971 ty: None,
2972 init: Some(PureExpr::Lit("1".into())),
2973 else_branch: None,
2974 },
2975 PureStmt::Local {
2976 pattern: PurePattern::Wild,
2977 ty: None,
2978 init: Some(PureExpr::Field {
2979 expr: Box::new(PureExpr::Path("x".into())),
2980 field: "field".into(),
2981 }),
2982 else_branch: None,
2983 },
2984 ];
2985
2986 let graph = build_graph_for_fn(make_fn("test_fn", stmts));
2987 let tracker = graph.lock_tracker();
2988 assert_eq!(tracker.acquisitions().len(), 0);
2989 assert_eq!(tracker.critical_sections().len(), 0);
2990 }
2991
2992 fn build_graph_for_impl(impl_block: PureImpl) -> DataFlowGraphV2 {
2999 let mut registry = SymbolRegistry::new();
3000 let module_path = SymbolPath::parse("test_crate").unwrap();
3001
3002 let base_type = impl_block
3004 .self_ty
3005 .split('<')
3006 .next()
3007 .unwrap_or(&impl_block.self_ty)
3008 .trim();
3009 let type_path = module_path.child(base_type).unwrap();
3010
3011 for item in &impl_block.items {
3012 if let PureImplItem::Fn(f) = item {
3013 let method_path = type_path.child(&f.name).unwrap();
3014 registry.register(method_path, SymbolKind::Method).unwrap();
3015 }
3016 }
3017
3018 let file = PureFile {
3019 attrs: vec![],
3020 items: vec![PureItem::Impl(impl_block)],
3021 };
3022
3023 let mut graph = DataFlowGraphV2::new();
3024 let mut collector = FlowCollectorV2::new(Some(module_path), ®istry, "test_crate");
3025 collector.visit_file(&file);
3026 collector.apply_to(&mut graph);
3027 graph
3028 }
3029
3030 #[test]
3031 fn test_lock_in_generic_inherent_impl_detected() {
3032 let method = PureFn {
3034 attrs: vec![],
3035 vis: PureVis::Public,
3036 is_async: false,
3037 is_async_inferred: false,
3038 is_const: false,
3039 is_unsafe: false,
3040 abi: None,
3041 name: "take".to_string(),
3042 generics: PureGenerics::default(),
3043 params: vec![PureParam::SelfValue {
3044 is_ref: true,
3045 is_mut: false,
3046 }],
3047 ret: None,
3048 body: PureBlock {
3049 stmts: vec![PureStmt::Local {
3050 pattern: PurePattern::Ident {
3051 name: "g".into(),
3052 is_mut: false,
3053 by_ref: false,
3054 },
3055 ty: None,
3056 init: Some(PureExpr::MethodCall {
3057 receiver: Box::new(PureExpr::Field {
3058 expr: Box::new(PureExpr::Path("self".into())),
3059 field: "write".into(),
3060 }),
3061 method: "lock".into(),
3062 turbofish: None,
3063 args: vec![],
3064 }),
3065 else_branch: None,
3066 }],
3067 },
3068 };
3069
3070 let impl_block = PureImpl {
3071 attrs: vec![],
3072 generics: PureGenerics::default(),
3073 is_unsafe: false,
3074 trait_: None,
3075 self_ty: "Handle < S >".to_string(), items: vec![PureImplItem::Fn(method)],
3077 };
3078
3079 let graph = build_graph_for_impl(impl_block);
3080 let tracker = graph.lock_tracker();
3081 assert_eq!(
3082 tracker.acquisitions().len(),
3083 1,
3084 "lock in generic inherent impl should be detected; \
3085 visit_impl_item must strip generics from self_ty"
3086 );
3087 assert_eq!(tracker.acquisitions()[0].lock_type, LockType::Mutex);
3088 }
3089}