Skip to main content

ryo_analysis/query/
builder_dataflow_v2.rs

1//! DataFlowBuilder: Construct DataFlowGraphV2 from PureFile ASTs
2//!
3//! Key improvements over V1:
4//! - **String-free**: Uses SymbolId for variable names (no String allocation)
5//! - **Proper scoping**: Uses $param and $var segments for variables
6//! - **VarId-based**: Direct VarId indexing, no petgraph
7//! - **V2 API**: Uses `ImHashMap<WorkspaceFilePath, Arc<PureFile>>` for consistency
8
9use 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
26/// Builder for constructing DataFlowGraphV2 using WorkspaceFilePath-keyed files.
27pub 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    /// Create a new workspace builder.
35    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    /// Build the DataFlowGraphV2.
48    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            // Derive module path from workspace file path
56            // Note: Using module_path_str() + parse() to bridge ryo-symbol and ryo-analysis SymbolPath types
57            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    /// Incrementally add data from specified files to an existing graph.
69    ///
70    /// This is used for incremental updates after mutations:
71    /// 1. Call `graph.clear_for_symbols()` to remove old data for affected symbols
72    /// 2. Call this method to add new data from modified files
73    ///
74    /// # Arguments
75    /// * `graph` - The existing DataFlowGraphV2 to update
76    /// * `modified_files` - Only process these files (must be subset of self.files)
77    ///
78    /// **Deprecated**: Use `build_incremental_by_symbols` for symbol-based updates.
79    #[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    /// Incrementally add data from ASTRegistry for specified symbols (Phase 2).
104    ///
105    /// This is the symbol-based incremental update path:
106    /// 1. Call `graph.clear_for_symbols()` to remove old data for affected symbols
107    /// 2. Call this method to add new data directly from ASTRegistry
108    ///
109    /// # Arguments
110    /// * `graph` - The existing DataFlowGraphV2 to update
111    /// * `ast_registry` - The ASTRegistry containing PureItem ASTs
112    /// * `affected_ids` - Only process these symbol IDs
113    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            // Get module path from symbol path
121            let module_path = self.registry.resolve(id).and_then(|path| {
122                // Get parent path (module containing this symbol)
123                let path_str = path.to_string();
124                path_str
125                    .rsplit_once("::")
126                    .and_then(|(parent, _)| SymbolPath::parse(parent).ok())
127            });
128
129            // Get AST from ASTRegistry
130            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
140/// Collected variable info before adding to graph.
141struct CollectedVarV2 {
142    data: VarData,
143    name: String,
144    temp_id: usize,
145    /// The variable's SymbolId from SymbolRegistry (None for local variables).
146    var_symbol_id: Option<SymbolId>,
147}
148
149/// Collected flow info before adding to graph.
150struct CollectedFlowV2 {
151    from_id: usize,
152    to_id: usize,
153    data: FlowData,
154}
155
156/// Collects data flows from a PureFile (V2 - String-free).
157struct FlowCollectorV2<'a> {
158    /// Module path for this file (e.g., ryo_cli::commands::graph).
159    module_path: Option<SymbolPath>,
160    registry: &'a SymbolRegistry,
161    /// Crate name for fallback paths.
162    crate_name: String,
163    /// Current function/method scope path.
164    current_scope_path: Option<SymbolPath>,
165    /// Current symbol ID for the scope.
166    current_symbol: Option<SymbolId>,
167    /// Collected variables with temporary IDs.
168    vars: Vec<CollectedVarV2>,
169    /// Variable name -> temp_id map for deduplication (local to current scope).
170    var_name_map: HashMap<String, usize>,
171    /// Collected flows.
172    flows: Vec<CollectedFlowV2>,
173    /// Collected scopes.
174    collected_scopes: Vec<ScopeData>,
175    /// Scope stack (current scope chain).
176    scope_stack: Vec<ScopeId>,
177    /// Next temp ID.
178    next_id: usize,
179    /// Current line number.
180    current_line: u32,
181    /// Collected lock acquisitions (transferred to graph in apply_to).
182    collected_locks: Vec<CollectedLockV2>,
183    /// Guard temp_ids (for tracking field accesses within critical sections).
184    guard_temp_ids: HashSet<usize>,
185    /// Collected field accesses on guard variables.
186    collected_field_accesses: Vec<CollectedFieldAccessV2>,
187    /// Skip lock detection in visit_expr_for_assignments (set during PureStmt::Local init).
188    in_local_init: bool,
189}
190
191/// Collected lock acquisition info before adding to graph.
192struct CollectedLockV2 {
193    /// Temp ID of the lock variable (receiver of .lock()/.read()/.write()).
194    lock_temp_id: usize,
195    /// Temp ID of the guard variable (let guard = ...).
196    guard_temp_id: usize,
197    /// Type of lock.
198    lock_type: LockType,
199    /// Line where the lock is acquired.
200    line: u32,
201    /// Whether this is a try_lock (non-blocking).
202    is_try: bool,
203    /// Name of the lock variable.
204    lock_name: String,
205    /// Name of the guard variable.
206    guard_name: String,
207    /// The function that owns this lock acquisition.
208    owner_fn: Option<SymbolId>,
209}
210
211/// Collected field access on a guard variable.
212struct CollectedFieldAccessV2 {
213    /// Temp ID of the guard variable.
214    guard_temp_id: usize,
215    /// Name of the accessed field.
216    field_name: String,
217    /// Kind of access (read or write).
218    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    /// Apply collected vars, flows, and scopes to a DataFlowGraphV2.
248    fn apply_to(self, graph: &mut DataFlowGraphV2) {
249        // Transfer scopes first (ScopeIds are stable indices)
250        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        // Add variables
257        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        // Add flows
263        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        // Transfer lock acquisitions
272        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        // Transfer field accesses on guard variables to lock tracker
296        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, // PureAST has no line info
303                );
304            }
305        }
306
307        // Flush active lock sections to completed.
308        // PureAST lacks scope/span info, so release() is never called per-guard.
309        // Flushing here ensures stats() counts all acquisitions.
310        graph.lock_tracker_mut().flush_active_sections();
311    }
312
313    /// Lookup an existing variable by name in current scope.
314    ///
315    /// Returns the temp ID if found, None otherwise.
316    fn lookup_existing_var(&self, name: &str) -> Option<usize> {
317        self.var_name_map.get(name).copied()
318    }
319
320    /// Lookup existing variable or create new one.
321    ///
322    /// For path expressions (variable references), this first checks if the
323    /// variable already exists as a parameter or local, avoiding duplicate creation.
324    /// Returns `None` if outside of a function scope.
325    fn lookup_or_create_var(&mut self, name: &str, default_kind: VarKind) -> Option<usize> {
326        // First, try to find existing variable
327        if let Some(id) = self.lookup_existing_var(name) {
328            return Some(id);
329        }
330
331        // Not found, create new variable
332        self.get_or_create_var(name, default_kind)
333    }
334
335    /// Get or create a variable and temp ID.
336    ///
337    /// Uses name-based deduplication within current scope.
338    /// Returns `None` if `current_symbol` is not set (outside function scope).
339    fn get_or_create_var(&mut self, name: &str, kind: VarKind) -> Option<usize> {
340        // Skip if not inside a function scope
341        let parent = self.current_symbol?;
342
343        // Check if we already have this var by name
344        if let Some(&id) = self.var_name_map.get(name) {
345            return Some(id);
346        }
347
348        // Create new var entry
349        let id = self.next_id;
350        self.next_id += 1;
351
352        // Determine VarScope from VarKind
353        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        // Try to lookup existing variable SymbolId from registry.
360        // Returns None for local variables not registered in the registry.
361        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    /// Lookup a variable's SymbolId from the registry.
381    ///
382    /// Constructs the expected path (parent::$scope::name) and looks it up.
383    fn lookup_var_symbol(&self, parent: SymbolId, scope: VarScope, name: &str) -> Option<SymbolId> {
384        // Get parent's path
385        let parent_path = self.registry.resolve(parent)?;
386
387        // Build the variable path using internal API
388        let var_path = parent_path.with_var_scope(scope, name).ok()?;
389
390        // Lookup in registry
391        self.registry.lookup(&var_path)
392    }
393
394    /// Mark a variable as `mut` binding (e.g. `let mut x = ...`).
395    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    /// Get or create the usage sink variable for the current scope.
402    ///
403    /// All non-assignment usage flows (Argument, Read, Return) point to this
404    /// single Temp variable. The important property is that the SOURCE variable
405    /// gets an outgoing flow, making it visible to analyses like impact/provenance.
406    fn usage_sink(&mut self) -> Option<usize> {
407        self.get_or_create_var("$usage", VarKind::Temp)
408    }
409
410    /// Track argument usage flows for function/method call arguments.
411    ///
412    /// Classifies each argument by its syntactic form:
413    /// - `&expr`     → `SharedBorrow` (callee receives `&T`)
414    /// - `&mut expr` → `MutBorrow`    (callee receives `&mut T`)
415    /// - `expr`      → `Argument`     (callee receives `T` by value)
416    ///
417    /// This classification enables UnnecessaryClone to distinguish between
418    /// passing a clone as `&x` (unnecessary) vs `x` (possibly necessary).
419    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            // Classify flow kind based on how the argument is passed:
425            // - &expr  → SharedBorrow (callee receives immutable reference)
426            // - &mut expr → MutBorrow  (callee receives mutable reference)
427            // - expr   → Argument     (callee receives ownership / copy)
428            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    /// Track read flow for method call receiver.
447    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    /// Track receiver as mutably borrowed (for methods like `get_mut`, `iter_mut`).
458    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    /// Track closure/async capture flows.
469    ///
470    /// Collects ALL variable references from the body (not just tail expression).
471    /// Variables that already exist in the outer scope are considered captured:
472    /// - `move` closure/async: Move flow
473    /// - non-move: Read flow
474    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        // Collect ALL variable names referenced anywhere in the body
480        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    /// Recursively collect all variable names referenced in an expression tree.
497    ///
498    /// Unlike `collect_expr_sources` which only finds value-producing sources,
499    /// this traverses the entire AST including all statements and sub-expressions.
500    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            _ => {} // Lit, Macro, Continue, Return(None), Break(None), etc.
596        }
597    }
598
599    /// Collect all referenced variable names from a block's statements.
600    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                // Verbatim is opaque raw bytes — no variable
613                // references to collect (B-3-cont carrier).
614                PureStmt::Verbatim(_) => {}
615            }
616        }
617    }
618
619    /// Extract variable references from macro tokens heuristically.
620    ///
621    /// Scans tokens for identifiers matching known variables and creates Read flows.
622    /// Handles common macros like `format!`, `println!`, `vec!`, etc.
623    fn track_macro_var_refs(&mut self, tokens: &str) {
624        let Some(sink) = self.usage_sink() else {
625            return;
626        };
627        // Extract potential identifiers from tokens (simple word boundary scan)
628        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            // Check if this token matches a known variable
633            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    /// Bind pattern variables from match arm patterns.
640    ///
641    /// Creates local variables for identifier patterns and assigns them
642    /// from the scrutinee expression sources.
643    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            _ => {} // Wild, Lit, Path, Range, Rest, Other — no variable binding
666        }
667    }
668
669    /// Track return flow for return expressions.
670    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    /// Add a data flow edge (tagged with current scope).
681    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    // ========== Scope Management ==========
695
696    /// Get the current scope (top of scope stack).
697    fn current_scope(&self) -> ScopeId {
698        self.scope_stack
699            .last()
700            .copied()
701            .unwrap_or(ScopeId::from_raw(0))
702    }
703
704    /// Push a new scope onto the stack.
705    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    /// Pop the current scope from the stack.
717    fn pop_scope(&mut self) {
718        self.scope_stack.pop();
719    }
720
721    // ========== Guard Parsing ==========
722
723    /// Parse a condition expression to extract guard constraints.
724    ///
725    /// Handles: `a < b`, `a <= b`, `a > b`, `a >= b`,
726    /// `expr.is_some()`, `expr.is_ok()`, `if let Some(x) = expr`, `if let Ok(x) = expr`.
727    fn parse_guard(&mut self, cond: &PureExpr) -> Option<Guard> {
728        match cond {
729            // a < b, a <= b, a > b, a >= b
730            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                    // a > b => b < a (constrained var is right)
735                    ">" => (GuardKind::LessThan, right, left),
736                    ">=" => (GuardKind::LessEqual, right, left),
737                    // && — merge both sides
738                    "&&" => {
739                        // Return the first parseable guard (P0 simplification)
740                        return self.parse_guard(left).or_else(|| self.parse_guard(right));
741                    }
742                    _ => return None,
743                };
744                // Extract constrained variable
745                let var_name = extract_expr_name(left_expr)?;
746                Some(Guard {
747                    kind,
748                    var_names: smallvec![var_name],
749                })
750            }
751            // expr.is_some(), expr.is_ok()
752            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            // if let Some(x) = expr / if let Ok(x) = expr
767            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    /// Parse guard from a match arm pattern (e.g., Some(x) → LetSome on scrutinee).
791    fn parse_match_arm_guard(&self, _pattern: &PurePattern) -> Option<Guard> {
792        // TODO: P1 — extract variant guards from match patterns
793        None
794    }
795
796    /// Emit FlowKind::Conditional from condition variable references.
797    fn emit_conditional_flow(&mut self, cond: &PureExpr) {
798        let mut names = Vec::new();
799        FlowCollectorV2::collect_all_referenced_names(cond, &mut names);
800        // Create pairwise Conditional flows between condition variables
801        let var_ids: Vec<usize> = names
802            .iter()
803            .filter_map(|name| self.lookup_existing_var(name))
804            .collect();
805        // Emit Conditional from each var to a synthetic "condition" node
806        // For P0: emit a self-referencing Conditional to mark these vars as condition participants
807        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                // Build scope path: module_path::function_name
822                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                // Clear var_name_map for new scope
836                self.var_name_map.clear();
837                self.guard_temp_ids.clear();
838
839                // Lookup symbol for this function (already registered in Phase 2)
840                self.current_symbol = self.registry.lookup(&scope_path);
841
842                // Push function scope
843                self.push_scope(ScopeKind::Function, None);
844
845                // Add parameters
846                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            // Static and Const are outside function scope, skip dataflow tracking
868            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            // Build scope path matching context.rs registration:
876            // - Plain impl: module::Type::method
877            // - Trait impl: module::<impl Trait for Type>::method
878            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                        // Strip generic parameters: "Router < S >" → "Router"
886                        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            // Clear var_name_map for new scope
904            self.var_name_map.clear();
905
906            // Lookup symbol for this method (already registered in Phase 2)
907            self.current_symbol = self.registry.lookup(&scope_path);
908
909            // Push function scope
910            self.push_scope(ScopeKind::Function, None);
911
912            // Add parameters (skip self)
913            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        // Treat tail expression (last Expr without semicolon) as implicit return
933        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                        // Track `let mut x` binding mutability
944                        if matches!(pattern, PurePattern::Ident { is_mut: true, .. }) {
945                            self.set_var_mut(target_id);
946                        }
947                        if let Some(expr) = init {
948                            // Check if this is a lock acquisition
949                            // Patterns: mutex.lock(), mutex.lock().unwrap(), rwlock.read(), etc.
950                            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                            // Check if this is a clone() call
971                            if let Some(clone_receiver) = is_clone_call(expr) {
972                                // Clone flow: receiver -> target
973                                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                                // Borrow flow: &source -> target or &mut source -> target
979                                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                                // Normal assignment flow
990                                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                            // Also visit init expr for nested side effects
996                            // (closures, calls, method calls, etc.)
997                            // Skip lock detection since it was already handled above.
998                            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                    // Wildcard or unnamed pattern (e.g., `let _ = foo(&x);`).
1005                    // Still visit the init expression for side effects so that
1006                    // variable usages inside it are tracked.
1007                    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            // Verbatim is opaque raw bytes — no assignment to visit
1019            // (B-3-cont carrier).
1020            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                // Track guard.field = expr as Write field access
1037                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                // Emit Conditional flow for condition variables
1056                self.emit_conditional_flow(cond);
1057                // Parse guard from condition
1058                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                    // TODO: negated guard for else branch
1064                    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                // Track scrutinee as Read
1072                self.track_receiver_read(expr);
1073                // Process each arm
1074                let scrutinee_sources = self.collect_expr_sources(expr);
1075                for arm in arms {
1076                    // Parse guard from match arm pattern
1077                    let arm_guard = self.parse_match_arm_guard(&arm.pattern);
1078                    self.push_scope(ScopeKind::MatchArm, arm_guard);
1079                    // Create pattern variable bindings from scrutinee
1080                    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                // Track iterator → pattern variable binding
1107                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                // Also track iterator as Read usage
1116                self.track_receiver_read(expr);
1117                // For loops implicitly bound the iterator variable
1118                self.push_scope(ScopeKind::ForBody, None);
1119                self.visit_block(body);
1120                self.pop_scope();
1121            }
1122            PureExpr::Closure { body, is_move, .. } => {
1123                // Track captured variables before visiting body
1124                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                // Track argument usage flows
1135                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                // Detect lock calls in standalone expressions (not bound to let)
1148                // e.g. match &mut *self.mutex.lock().unwrap() { ... }
1149                // Skip if already detected in PureStmt::Local init.
1150                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                // Track receiver flow based on method name heuristic.
1176                //
1177                // Methods ending in `_mut` (get_mut, iter_mut, as_mut, etc.)
1178                // follow Rust's naming convention for `&mut self` receivers.
1179                // Classify those as MutBorrow so downstream analyses (e.g.
1180                // UnnecessaryClone) know the receiver is mutably borrowed.
1181                //
1182                // Limitation: mutating methods that don't follow the `_mut`
1183                // convention (e.g. `push`, `insert`, `set_extension`) are
1184                // classified as Read. For clone analysis, the `let mut`
1185                // binding check in UnnecessaryClone compensates for this.
1186                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); // break-with-value is semantically similar to return
1203            }
1204            PureExpr::Struct { fields, .. } => {
1205                // Track field values as Read usage
1206                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                // Extract variable references from macro tokens heuristically
1225                self.track_macro_var_refs(tokens);
1226            }
1227            PureExpr::Index { expr, index } => {
1228                // Track collection[index] as Index flow
1229                self.visit_expr_for_assignments(expr);
1230                self.visit_expr_for_assignments(index);
1231                // Emit FlowKind::Index: index_var → collection_var
1232                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                // while let / if let: visit and track the inner expression
1244                self.visit_expr_for_assignments(expr);
1245                self.track_receiver_read(expr);
1246                // Bind pattern variables from scrutinee sources so downstream
1247                // analyses (e.g. UnnecessaryClone transitive container reuse)
1248                // can walk back from the bound variable to the container.
1249                let scrutinee_sources = self.collect_expr_sources(expr);
1250                self.bind_pattern_vars(pattern, &scrutinee_sources);
1251            }
1252            PureExpr::Field { .. } => {
1253                // Track guard.field as Read field access
1254                self.track_guard_field_access(expr, AccessKind::Read);
1255            }
1256            _ => {}
1257        }
1258    }
1259
1260    /// Track field access on a guard variable (e.g., `guard.field`).
1261    ///
1262    /// If `expr` is `PureExpr::Field { expr: Path(guard_name), field }` and
1263    /// `guard_name` is a known lock guard, records the field access.
1264    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                // Use lookup_or_create to avoid duplicating params as locals
1295                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                    // Use just the field name (not "self.field") since $field scope identifies it
1302                    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
1389/// Extract a simple variable name from an expression.
1390///
1391/// Handles `Path("x")`, `Field { expr: Path("self"), field: "x" }`.
1392fn 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            // Non-self field: "receiver.field" (e.g., shared.inner)
1398            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                // Use just the field name (not "self.field") since $field scope identifies it
1414                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
1427/// Check if an expression is a borrow (`&x` or `&mut x`).
1428///
1429/// Returns `(is_mut, inner_expr)` if this is a reference expression.
1430fn 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
1437/// Check if an expression is a clone() method call.
1438///
1439/// Returns the receiver expression if this is a `.clone()` call.
1440fn 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
1452/// Classify a method name as a lock acquisition.
1453///
1454/// Returns `(LockType, is_try)` if the method is a lock-related call.
1455fn 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
1469/// Extract a lock acquisition from an expression.
1470///
1471/// Handles common patterns:
1472/// - `receiver.lock()` / `receiver.read()` / `receiver.write()`
1473/// - `receiver.lock().unwrap()` / `receiver.lock().expect("...")`
1474/// - `receiver.lock().await` (tokio)
1475/// - `receiver.try_lock()`
1476///
1477/// Returns `(lock_receiver, LockType, is_try)`.
1478fn extract_lock_call(expr: &PureExpr) -> Option<(&PureExpr, LockType, bool)> {
1479    match expr {
1480        PureExpr::MethodCall {
1481            receiver,
1482            method,
1483            args,
1484            ..
1485        } => {
1486            // Direct lock call: receiver.lock(), receiver.read(), etc.
1487            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            // Chain suffix: receiver.lock().unwrap() / .expect("...") / .take() / etc.
1493            // Any no-arg method call on a lock result is traversed through.
1494            if args.is_empty() {
1495                return extract_lock_call(receiver);
1496            }
1497            None
1498        }
1499        // receiver.lock().await (tokio)
1500        PureExpr::Await(inner) => extract_lock_call(inner),
1501        // receiver.lock()? (try operator)
1502        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    /// Build a DataFlowGraphV2 from a single function definition.
1516    ///
1517    /// Registers the function in a fresh SymbolRegistry so that
1518    /// FlowCollectorV2 can resolve `current_symbol`.
1519    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), &registry, "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    // ========== S-rank: Function call arguments ==========
1570
1571    #[test]
1572    fn test_call_arg_generates_argument_flow() {
1573        // fn test_fn() { let x = 1; foo(x); }
1574        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    // ========== S-rank: Method call receiver ==========
1603
1604    #[test]
1605    fn test_method_receiver_generates_read_flow() {
1606        // fn test_fn() { let x = 1; x.push(1); }
1607        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    // ========== S-rank: Return expression ==========
1638
1639    #[test]
1640    fn test_return_generates_return_flow() {
1641        // fn test_fn() { let x = 1; return x; }
1642        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    // ========== S-rank: Borrow expressions ==========
1668
1669    #[test]
1670    fn test_shared_borrow_generates_borrow_flow() {
1671        // fn test_fn() { let x = 1; let y = &x; }
1672        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        // fn test_fn() { let x = 1; let y = &mut x; }
1712        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    // ========== Combined: Method call args ==========
1750
1751    #[test]
1752    fn test_method_call_arg_generates_argument_flow() {
1753        // fn test_fn() { let x = 1; let y = 2; y.foo(x); }
1754        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    // ========== A-rank: For loop pattern binding ==========
1795
1796    #[test]
1797    fn test_for_loop_creates_assign_to_pattern_var() {
1798        // fn test_fn() { let items = 1; for item in items { } }
1799        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    // ========== A-rank: Break with value ==========
1846
1847    #[test]
1848    fn test_break_with_value_generates_read_flow() {
1849        // fn test_fn() { let x = 1; loop { break x; } }
1850        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    // ========== A-rank: Closure captures ==========
1884
1885    #[test]
1886    fn test_closure_capture_generates_flow() {
1887        // fn test_fn() { let x = 1; let f = || x + 1; }
1888        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        // fn test_fn() { let x = 1; let f = move || x + 1; }
1935        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    // ========== B-rank: Match arm pattern binding ==========
1980
1981    #[test]
1982    fn test_match_arm_pattern_creates_variable_with_assign() {
1983        // fn test_fn() { let x = 1; match x { val => { } } }
1984        // Expected: x has outgoing Read (scrutinee), val exists with incoming Assign from x
1985        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    // ========== B-rank: Match scrutinee read ==========
2028
2029    #[test]
2030    fn test_match_scrutinee_generates_read_flow() {
2031        // fn test_fn() { let x = 1; match x { _ => {} } }
2032        // Expected: x has outgoing Read flow (scrutinee is read)
2033        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    // ========== B-rank: Match as source in collect_sources_recursive ==========
2069
2070    #[test]
2071    fn test_match_expr_as_init_collects_arm_body_sources() {
2072        // fn test_fn() { let a = 1; let b = 2; let result = match ... { _ => a, _ => b }; }
2073        // Expected: result has incoming Assign from both a and b
2074        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    // ========== Struct expression: field values generate Read flow ==========
2136
2137    #[test]
2138    fn test_struct_expr_field_values_generate_read_flow() {
2139        // fn test_fn() { let x = 1; let y = 2; let s = Foo { a: x, b: y }; }
2140        // Expected: x and y have outgoing Read flow (consumed by struct construction)
2141        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    // ========== Struct expression as statement (not in let init) ==========
2198
2199    #[test]
2200    fn test_struct_expr_as_stmt_generates_read_flow() {
2201        // fn test_fn() { let x = 1; Foo { a: x }; }
2202        // Expected: x has outgoing Read flow
2203        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    // ========== Field assignment: right-hand side generates Read flow ==========
2233
2234    #[test]
2235    fn test_field_assign_rhs_generates_read_flow() {
2236        // fn test_fn() { let x = 1; self.field = x; }
2237        // Expected: x has outgoing flow (Assign to self.field, but also Read via the assignment)
2238        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    // ========== Async move block captures ==========
2271
2272    #[test]
2273    fn test_async_move_block_captures_generate_flow() {
2274        // fn test_fn() { let x = 1; async move { x + 1 }; }
2275        // Expected: x has outgoing flow (captured by async move)
2276        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    // ========== Closure capture in non-tail position ==========
2311
2312    #[test]
2313    fn test_closure_captures_var_in_non_tail_stmt() {
2314        // fn test_fn() { let x = 1; let f = move || { let y = x; }; }
2315        // Expected: x has outgoing flow (captured inside closure body, non-tail stmt)
2316        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    // ========== While condition reads variable ==========
2371
2372    #[test]
2373    fn test_while_condition_generates_read_flow() {
2374        // fn test_fn() { let x = 1; while x > 0 { } }
2375        // Expected: x has outgoing Read flow from while condition
2376        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    // ========== Tail expression treated as implicit return ==========
2410
2411    #[test]
2412    fn test_tail_expr_generates_read_flow() {
2413        // fn test_fn() { let x = 1; (x, 2) }
2414        // Expected: x has outgoing flow (tail expression = implicit return)
2415        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    // ========== Macro expression variable extraction ==========
2444
2445    #[test]
2446    fn test_macro_expr_variable_generates_read_flow() {
2447        // fn test_fn() { let x = 1; println!("{}", x); }
2448        // Expected: x has outgoing flow (referenced in macro tokens)
2449        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    // ========== while let condition reads variable ==========
2479
2480    #[test]
2481    fn test_while_let_condition_generates_read_flow() {
2482        // fn test_fn() { let x = 1; while let Some(v) = x.parent() { } }
2483        // Expected: x has outgoing Read flow from while-let condition
2484        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    // ========== Lock detection tests ==========
2526
2527    #[test]
2528    fn test_lock_direct_call_detected() {
2529        // fn test_fn() { let m = 1; let guard = m.lock(); }
2530        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        // fn test_fn() { let m = 1; let guard = m.lock().unwrap(); }
2574        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        // fn test_fn() { let rw = 1; let r = rw.read(); let w = rw.write(); }
2620        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        // fn test_fn() { let m = 1; let guard = m.try_lock(); }
2677        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        // fn test_fn() { let m = 1; let guard = m.lock().await; }
2714        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        // fn test_fn() { let cell = 1; let r = cell.borrow(); let w = cell.borrow_mut(); }
2755        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        // fn test_fn() { let x = 1; let y = x.foo(); }
2812        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        // fn test_fn() { let m = 1; let guard = m.lock(); let _ = guard.counter; }
2852        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            // let _ = guard.counter;
2879            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        // fn test_fn() { let m = 1; let guard = m.lock(); guard.counter = 42; }
2910        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            // guard.counter = 42;
2937            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        // fn test_fn() { let x = 1; let _ = x.field; }
2963        // x is NOT a guard, so field access should not be tracked
2964        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    // ========== Generic impl block: lock detection ==========
2993
2994    /// Build a DataFlowGraphV2 from an impl block.
2995    ///
2996    /// Registers the impl block and its methods in a fresh SymbolRegistry
2997    /// so that FlowCollectorV2 can resolve `current_symbol`.
2998    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        // Register methods under the base type (strip generics)
3003        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), &registry, "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        // impl<S> Handle<S> { fn take(&self) { let g = self.write.lock(); } }
3033        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(), // generic self_ty
3076            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}