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