cs/trace/
graph_builder.rs

1//! # Lifetimes - Rust Book Chapter 10.3
2//!
3//! This module demonstrates lifetime annotations from
4//! [The Rust Book Chapter 10.3](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html).
5//!
6//! ## Key Concepts Demonstrated
7//!
8//! 1. **Lifetime Parameters on Structs** (Chapter 10.3)
9//!    - `CallGraphBuilder<'a>` holds references with lifetime `'a`
10//!    - Prevents the struct from outliving its referenced data
11//!    - Zero-cost abstraction - no runtime overhead
12//!
13//! 2. **Why Lifetimes Matter**
14//!    - Prevent dangling references at compile time
15//!    - Enable borrowing without ownership transfer
16//!    - Alternative to heap allocation (`Box`) or reference counting (`Rc`)
17//!
18//! 3. **When You Need Explicit Lifetimes**
19//!    - Structs that store references
20//!    - Functions returning references
21//!    - Multiple references with different lifetimes
22//!
23//! ## Learning Notes
24//!
25//! **Most code doesn't need explicit lifetimes!**
26//! - Rust has "lifetime elision" rules that infer lifetimes
27//! - You only write lifetimes when Rust can't figure them out
28//! - This module is a good example of when they're necessary
29//!
30//! **The lifetime syntax:**
31//! - `'a` - A lifetime parameter (can be any name: `'b`, `'lifetime`, etc.)
32//! - `&'a T` - A reference to `T` that lives for lifetime `'a`
33//! - `&'a mut T` - A mutable reference to `T` that lives for lifetime `'a`
34//!
35//! **Common lifetime names:**
36//! - `'a`, `'b`, `'c` - Generic lifetime parameters
37//! - `'static` - Special lifetime for the entire program duration
38
39use crate::error::Result;
40use crate::trace::{CallExtractor, FunctionDef, FunctionFinder};
41use std::collections::HashSet;
42
43/// Direction of the call graph trace
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub enum TraceDirection {
46    /// Trace forward: which functions does this function call?
47    Forward,
48    /// Trace backward: which functions call this function?
49    Backward,
50}
51
52/// A node in the call graph tree
53#[derive(Debug, Clone)]
54pub struct CallNode {
55    /// The function definition for this node
56    pub def: FunctionDef,
57    /// Child nodes (called functions or callers, depending on direction)
58    pub children: Vec<CallNode>,
59    /// Whether the tree was truncated at this node due to depth limit
60    pub truncated: bool,
61}
62
63/// Represents a complete call graph tree
64#[derive(Debug, Clone)]
65pub struct CallTree {
66    /// The root node of the tree (the starting function)
67    pub root: CallNode,
68}
69
70/// Builds a call graph by recursively tracing function calls.
71///
72/// # Rust Book Reference
73///
74/// **Chapter 10.3: Validating References with Lifetimes**
75/// https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html
76///
77/// # Educational Notes - Lifetime Parameters
78///
79/// This struct demonstrates explicit lifetime annotations:
80///
81/// ```rust,ignore
82/// pub struct CallGraphBuilder<'a> {
83///     finder: &'a mut FunctionFinder,
84///     extractor: &'a CallExtractor,
85/// }
86/// ```
87///
88/// **What does `<'a>` mean?**
89/// - `'a` is a **lifetime parameter** (read as "lifetime a")
90/// - It says: "This struct holds references that live for lifetime 'a"
91/// - The struct cannot outlive the data it references
92///
93/// **Why do we need lifetimes here?**
94/// - `CallGraphBuilder` stores references to `FunctionFinder` and `CallExtractor`
95/// - Without lifetimes, Rust doesn't know how long these references are valid
96/// - The lifetime `'a` connects the struct's lifetime to its references
97///
98/// **What does this prevent?**
99/// ```rust,ignore
100/// let builder = {
101///     let finder = FunctionFinder::new(...);
102///     let extractor = CallExtractor::new(...);
103///     CallGraphBuilder::new(..., &mut finder, &extractor)
104/// }; // ERROR! finder and extractor are dropped here
105/// // builder would have dangling references!
106/// ```
107///
108/// **The lifetime constraint:**
109/// - `CallGraphBuilder<'a>` can only exist while `finder` and `extractor` exist
110/// - Rust enforces this at compile time
111/// - No runtime overhead - pure compile-time safety
112///
113/// **Alternative without lifetimes:**
114/// - Could use `Box<FunctionFinder>` (heap allocation + ownership)
115/// - Could use `Rc<RefCell<FunctionFinder>>` (reference counting + runtime checks)
116/// - Lifetimes are zero-cost: no allocation, no runtime checks
117pub struct CallGraphBuilder<'a> {
118    direction: TraceDirection,
119    max_depth: usize,
120    finder: &'a mut FunctionFinder,
121    extractor: &'a CallExtractor,
122}
123
124impl<'a> CallGraphBuilder<'a> {
125    /// Create a new CallGraphBuilder.
126    ///
127    /// # Rust Book Reference
128    ///
129    /// **Chapter 10.3: Lifetime Annotations in Function Signatures**
130    /// https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotations-in-function-signatures
131    ///
132    /// # Educational Notes - Lifetime in Function Signatures
133    ///
134    /// This function signature shows how lifetimes flow through constructors:
135    ///
136    /// ```rust,ignore
137    /// pub fn new(
138    ///     finder: &'a mut FunctionFinder,
139    ///     extractor: &'a CallExtractor,
140    /// ) -> Self
141    /// ```
142    ///
143    /// **What this means:**
144    /// - The returned `CallGraphBuilder<'a>` contains references with lifetime `'a`
145    /// - Those references come from the input parameters
146    /// - The builder cannot outlive `finder` or `extractor`
147    ///
148    /// **Lifetime elision:**
149    /// Without the struct's `<'a>`, we'd need to write:
150    /// ```rust,ignore
151    /// pub fn new<'a>(
152    ///     finder: &'a mut FunctionFinder,
153    ///     extractor: &'a CallExtractor,
154    /// ) -> CallGraphBuilder<'a>
155    /// ```
156    /// But since the struct already has `<'a>`, we can use `Self`.
157    ///
158    /// # Arguments
159    /// * `direction` - Whether to trace forward or backward
160    /// * `max_depth` - Maximum depth of the call tree
161    /// * `finder` - Service to find function definitions
162    /// * `extractor` - Service to extract calls from code
163    pub fn new(
164        direction: TraceDirection,
165        max_depth: usize,
166        finder: &'a mut FunctionFinder,
167        extractor: &'a CallExtractor,
168    ) -> Self {
169        Self {
170            direction,
171            max_depth,
172            finder,
173            extractor,
174        }
175    }
176
177    /// Build a call trace tree starting from the given function
178    pub fn build_trace(&mut self, start_fn: &FunctionDef) -> Result<Option<CallTree>> {
179        let mut current_path = HashSet::new();
180
181        match self.build_node(start_fn, 0, &mut current_path) {
182            Some(root) => Ok(Some(CallTree { root })),
183            None => Ok(None),
184        }
185    }
186
187    /// Recursively build a call tree node
188    ///
189    /// Uses proper cycle detection with current_path to prevent infinite recursion
190    /// while still allowing the same function to appear in different branches.
191    fn build_node(
192        &mut self,
193        func: &FunctionDef,
194        depth: usize,
195        current_path: &mut HashSet<FunctionDef>,
196    ) -> Option<CallNode> {
197        // Check depth limit
198        if depth >= self.max_depth {
199            return Some(CallNode {
200                def: func.clone(),
201                children: vec![],
202                truncated: true,
203            });
204        }
205
206        // Check for cycles in current path (prevents infinite recursion)
207        if current_path.contains(func) {
208            return Some(CallNode {
209                def: func.clone(),
210                children: vec![],
211                truncated: false, // Not truncated by depth, but by cycle
212            });
213        }
214
215        // Add to current path for cycle detection
216        current_path.insert(func.clone());
217
218        let children = match self.direction {
219            TraceDirection::Forward => self.build_forward_children(func, depth, current_path),
220            TraceDirection::Backward => self.build_backward_children(func, depth, current_path),
221        };
222
223        // Remove from current path (allows same function in different branches)
224        current_path.remove(func);
225
226        Some(CallNode {
227            def: func.clone(),
228            children,
229            truncated: false,
230        })
231    }
232
233    /// Build children for forward tracing (what does this function call?)
234    fn build_forward_children(
235        &mut self,
236        func: &FunctionDef,
237        depth: usize,
238        current_path: &mut HashSet<FunctionDef>,
239    ) -> Vec<CallNode> {
240        // Extract function calls from this function's body
241        let call_names = match self.extractor.extract_calls(func) {
242            Ok(calls) => calls,
243            Err(_) => return vec![], // If extraction fails, return empty children
244        };
245
246        let mut children = Vec::new();
247
248        for call_name in call_names {
249            // Find the definition of the called function
250            if let Some(called_func) = self.finder.find_function(&call_name) {
251                // Recursively build the child node
252                if let Some(child_node) = self.build_node(&called_func, depth + 1, current_path) {
253                    children.push(child_node);
254                }
255            }
256            // If function not found, we simply don't include it (graceful handling)
257        }
258
259        children
260    }
261
262    /// Build children for backward tracing (who calls this function?)
263    fn build_backward_children(
264        &mut self,
265        func: &FunctionDef,
266        depth: usize,
267        current_path: &mut HashSet<FunctionDef>,
268    ) -> Vec<CallNode> {
269        // Find all functions that call this function
270        let callers = match self.extractor.find_callers(&func.name) {
271            Ok(caller_infos) => caller_infos,
272            Err(_) => return vec![], // If finding callers fails, return empty children
273        };
274
275        let mut children = Vec::new();
276
277        for caller_info in callers {
278            // Try to find the caller function definition
279            if let Some(caller_func) = self.finder.find_function(&caller_info.caller_name) {
280                // Avoid adding the same caller multiple times
281                if !children.iter().any(|child: &CallNode| {
282                    child.def.name == caller_func.name && child.def.file == caller_func.file
283                }) {
284                    // Recursively build the child node
285                    if let Some(child_node) = self.build_node(&caller_func, depth + 1, current_path)
286                    {
287                        children.push(child_node);
288                    }
289                }
290            }
291            // If caller function not found, we simply don't include it (graceful handling)
292        }
293
294        children
295    }
296}
297
298impl CallTree {
299    /// Get the total number of nodes in the tree
300    pub fn node_count(&self) -> usize {
301        Self::count_nodes(&self.root)
302    }
303
304    /// Get the maximum depth of the tree
305    pub fn max_depth(&self) -> usize {
306        Self::calculate_depth(&self.root, 0)
307    }
308
309    /// Check if the tree contains cycles
310    pub fn has_cycles(&self) -> bool {
311        let mut visited = HashSet::new();
312        let mut path = HashSet::new();
313        Self::has_cycle_helper(&self.root, &mut visited, &mut path)
314    }
315
316    fn count_nodes(node: &CallNode) -> usize {
317        1 + node.children.iter().map(Self::count_nodes).sum::<usize>()
318    }
319
320    fn calculate_depth(node: &CallNode, current_depth: usize) -> usize {
321        if node.children.is_empty() {
322            current_depth
323        } else {
324            node.children
325                .iter()
326                .map(|child| Self::calculate_depth(child, current_depth + 1))
327                .max()
328                .unwrap_or(current_depth)
329        }
330    }
331
332    fn has_cycle_helper(
333        node: &CallNode,
334        visited: &mut HashSet<FunctionDef>,
335        path: &mut HashSet<FunctionDef>,
336    ) -> bool {
337        if path.contains(&node.def) {
338            return true; // Found a cycle
339        }
340
341        if visited.contains(&node.def) {
342            return false; // Already processed this node
343        }
344
345        visited.insert(node.def.clone());
346        path.insert(node.def.clone());
347
348        for child in &node.children {
349            if Self::has_cycle_helper(child, visited, path) {
350                return true;
351            }
352        }
353
354        path.remove(&node.def);
355        false
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use std::path::PathBuf;
363
364    fn create_test_function(name: &str, file: &str, line: usize) -> FunctionDef {
365        FunctionDef {
366            name: name.to_string(),
367            file: PathBuf::from(file),
368            line,
369            body: format!("function {}() {{}}", name),
370        }
371    }
372
373    #[test]
374    fn test_trace_direction_equality() {
375        assert_eq!(TraceDirection::Forward, TraceDirection::Forward);
376        assert_eq!(TraceDirection::Backward, TraceDirection::Backward);
377        assert_ne!(TraceDirection::Forward, TraceDirection::Backward);
378    }
379
380    #[test]
381    fn test_call_node_creation() {
382        let func = create_test_function("test_func", "test.js", 10);
383        let node = CallNode {
384            def: func.clone(),
385            children: vec![],
386            truncated: false,
387        };
388
389        assert_eq!(node.def.name, "test_func");
390        assert_eq!(node.children.len(), 0);
391        assert!(!node.truncated);
392    }
393
394    #[test]
395    fn test_call_tree_creation() {
396        let func = create_test_function("main", "main.js", 1);
397        let root = CallNode {
398            def: func,
399            children: vec![],
400            truncated: false,
401        };
402        let tree = CallTree { root };
403
404        assert_eq!(tree.root.def.name, "main");
405    }
406
407    #[test]
408    fn test_call_tree_node_count() {
409        let main_func = create_test_function("main", "main.js", 1);
410        let helper_func = create_test_function("helper", "utils.js", 5);
411
412        let helper_node = CallNode {
413            def: helper_func,
414            children: vec![],
415            truncated: false,
416        };
417
418        let root = CallNode {
419            def: main_func,
420            children: vec![helper_node],
421            truncated: false,
422        };
423
424        let tree = CallTree { root };
425        assert_eq!(tree.node_count(), 2);
426    }
427
428    #[test]
429    fn test_call_tree_max_depth() {
430        let func1 = create_test_function("func1", "test.js", 1);
431        let func2 = create_test_function("func2", "test.js", 10);
432        let func3 = create_test_function("func3", "test.js", 20);
433
434        // Create a chain: func1 -> func2 -> func3
435        let node3 = CallNode {
436            def: func3,
437            children: vec![],
438            truncated: false,
439        };
440
441        let node2 = CallNode {
442            def: func2,
443            children: vec![node3],
444            truncated: false,
445        };
446
447        let root = CallNode {
448            def: func1,
449            children: vec![node2],
450            truncated: false,
451        };
452
453        let tree = CallTree { root };
454        assert_eq!(tree.max_depth(), 2); // 0-indexed depth
455    }
456
457    #[test]
458    fn test_call_graph_builder_creation() {
459        use crate::trace::{CallExtractor, FunctionFinder};
460        use std::env;
461
462        let base_dir = env::current_dir().unwrap();
463        let mut finder = FunctionFinder::new(base_dir.clone());
464        let extractor = CallExtractor::new(base_dir);
465
466        let builder = CallGraphBuilder::new(TraceDirection::Forward, 5, &mut finder, &extractor);
467
468        assert_eq!(builder.direction, TraceDirection::Forward);
469        assert_eq!(builder.max_depth, 5);
470    }
471
472    #[test]
473    fn test_depth_limit_handling() {
474        use crate::trace::{CallExtractor, FunctionFinder};
475        use std::env;
476
477        let base_dir = env::current_dir().unwrap();
478        let mut finder = FunctionFinder::new(base_dir.clone());
479        let extractor = CallExtractor::new(base_dir);
480
481        let mut builder = CallGraphBuilder::new(
482            TraceDirection::Forward,
483            0, // Max depth of 0 should only return root
484            &mut finder,
485            &extractor,
486        );
487
488        let test_func = create_test_function("test", "test.js", 1);
489        let mut path = HashSet::new();
490        let result = builder.build_node(&test_func, 0, &mut path);
491
492        assert!(result.is_some());
493        let node = result.unwrap();
494        assert_eq!(node.def.name, "test");
495        assert_eq!(node.children.len(), 0); // Should have no children due to depth limit
496        assert!(node.truncated); // Should be truncated
497    }
498
499    #[test]
500    fn test_cycle_detection() {
501        use crate::trace::{CallExtractor, FunctionFinder};
502        use std::env;
503
504        let base_dir = env::current_dir().unwrap();
505        let mut finder = FunctionFinder::new(base_dir.clone());
506        let extractor = CallExtractor::new(base_dir);
507
508        let mut builder =
509            CallGraphBuilder::new(TraceDirection::Forward, 10, &mut finder, &extractor);
510
511        let test_func = create_test_function("recursive", "test.js", 1);
512        let mut path = HashSet::new();
513
514        // Add the function to path to simulate cycle detection
515        path.insert(test_func.clone());
516
517        let result = builder.build_node(&test_func, 0, &mut path);
518
519        assert!(result.is_some());
520        let node = result.unwrap();
521        assert_eq!(node.children.len(), 0); // Should stop due to cycle detection
522    }
523
524    #[test]
525    fn test_function_def_equality() {
526        let func1 = create_test_function("test", "file.js", 10);
527        let func2 = create_test_function("test", "file.js", 10);
528        let func3 = create_test_function("test", "file.js", 20);
529
530        assert_eq!(func1, func2);
531        assert_ne!(func1, func3); // Different line numbers
532    }
533}