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}