ricecoder_refactoring/impact/
graph.rs

1//! Dependency graph construction and analysis
2
3use std::collections::{HashMap, HashSet, VecDeque};
4
5/// Represents a symbol in the dependency graph
6#[allow(dead_code)]
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct Symbol {
9    /// Symbol name
10    pub name: String,
11    /// File path where the symbol is defined
12    pub file: String,
13    /// Symbol type (function, struct, enum, etc.)
14    pub symbol_type: SymbolType,
15}
16
17/// Type of symbol
18#[allow(dead_code)]
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum SymbolType {
21    /// Function or method
22    Function,
23    /// Struct or class
24    Struct,
25    /// Enum
26    Enum,
27    /// Trait or interface
28    Trait,
29    /// Module
30    Module,
31    /// Variable or constant
32    Variable,
33    /// Type alias
34    TypeAlias,
35    /// Macro
36    Macro,
37    /// Other
38    Other,
39}
40
41/// Represents a dependency between two symbols
42#[allow(dead_code)]
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Dependency {
45    /// Source symbol
46    pub from: Symbol,
47    /// Target symbol
48    pub to: Symbol,
49    /// Type of dependency
50    pub dep_type: DependencyType,
51}
52
53/// Type of dependency
54#[allow(dead_code)]
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum DependencyType {
57    /// Direct call or reference
58    Direct,
59    /// Trait implementation
60    TraitImpl,
61    /// Type reference
62    TypeRef,
63    /// Module import
64    Import,
65    /// Generic type parameter
66    Generic,
67}
68
69/// Dependency graph for code analysis
70#[derive(Debug, Clone)]
71pub struct DependencyGraph {
72    /// All symbols in the graph (keyed by "name:file" to handle duplicate names in different files)
73    symbols: HashMap<String, Symbol>,
74    /// Dependencies between symbols
75    dependencies: Vec<Dependency>,
76    /// Adjacency list for efficient traversal (keyed by "name:file")
77    adjacency: HashMap<String, Vec<String>>,
78    /// Reverse adjacency list (who depends on this symbol) (keyed by "name:file")
79    reverse_adjacency: HashMap<String, Vec<String>>,
80}
81
82impl DependencyGraph {
83    /// Create a new empty dependency graph
84    pub fn new() -> Self {
85        Self {
86            symbols: HashMap::new(),
87            dependencies: Vec::new(),
88            adjacency: HashMap::new(),
89            reverse_adjacency: HashMap::new(),
90        }
91    }
92
93    /// Add a symbol to the graph
94    pub fn add_symbol(&mut self, symbol: Symbol) {
95        let key = format!("{}:{}", symbol.name, symbol.file);
96        self.symbols.insert(key, symbol);
97    }
98
99    /// Add a dependency between two symbols
100    pub fn add_dependency(&mut self, dependency: Dependency) {
101        let from_key = format!("{}:{}", dependency.from.name, dependency.from.file);
102        let to_key = format!("{}:{}", dependency.to.name, dependency.to.file);
103
104        // Add symbols if they don't exist
105        self.add_symbol(dependency.from.clone());
106        self.add_symbol(dependency.to.clone());
107
108        // Add to adjacency list
109        self.adjacency
110            .entry(from_key.clone())
111            .or_default()
112            .push(to_key.clone());
113
114        // Add to reverse adjacency list
115        self.reverse_adjacency
116            .entry(to_key)
117            .or_default()
118            .push(from_key);
119
120        self.dependencies.push(dependency);
121    }
122
123    /// Get all symbols that depend on a given symbol (direct dependents)
124    /// Returns just the symbol names (without file paths)
125    pub fn get_direct_dependents(&self, symbol_key: &str) -> Vec<String> {
126        let dependents = self.reverse_adjacency
127            .get(symbol_key)
128            .cloned()
129            .unwrap_or_default();
130        
131        // Extract symbol names from composite keys (name:file)
132        dependents.iter().map(|key| {
133            if let Some(colon_pos) = key.find(':') {
134                key[..colon_pos].to_string()
135            } else {
136                key.clone()
137            }
138        }).collect()
139    }
140
141    /// Get all symbols that a given symbol depends on (direct dependencies)
142    /// Returns just the symbol names (without file paths)
143    pub fn get_direct_dependencies(&self, symbol_key: &str) -> Vec<String> {
144        let dependencies = self.adjacency
145            .get(symbol_key)
146            .cloned()
147            .unwrap_or_default();
148        
149        // Extract symbol names from composite keys (name:file)
150        dependencies.iter().map(|key| {
151            if let Some(colon_pos) = key.find(':') {
152                key[..colon_pos].to_string()
153            } else {
154                key.clone()
155            }
156        }).collect()
157    }
158
159    /// Get all symbols transitively affected by a change to a given symbol
160    /// This includes all symbols that depend on the changed symbol, directly or indirectly
161    /// Returns just the symbol names (without file paths)
162    pub fn get_transitive_dependents(&self, symbol_key: &str) -> HashSet<String> {
163        let mut affected_keys = HashSet::new();
164        let mut queue = VecDeque::new();
165
166        // Start with direct dependents
167        if let Some(dependents) = self.reverse_adjacency.get(symbol_key) {
168            for dependent in dependents {
169                queue.push_back(dependent.clone());
170                affected_keys.insert(dependent.clone());
171            }
172        }
173
174        // BFS to find all transitive dependents
175        while let Some(current) = queue.pop_front() {
176            if let Some(dependents) = self.reverse_adjacency.get(&current) {
177                for dependent in dependents {
178                    if !affected_keys.contains(dependent) {
179                        affected_keys.insert(dependent.clone());
180                        queue.push_back(dependent.clone());
181                    }
182                }
183            }
184        }
185
186        // Extract symbol names from composite keys (name:file)
187        affected_keys.iter().map(|key| {
188            if let Some(colon_pos) = key.find(':') {
189                key[..colon_pos].to_string()
190            } else {
191                key.clone()
192            }
193        }).collect()
194    }
195
196    /// Get all symbols that a given symbol transitively depends on
197    pub fn get_transitive_dependencies(&self, symbol_name: &str) -> HashSet<String> {
198        let mut dependencies = HashSet::new();
199        let mut queue = VecDeque::new();
200
201        // Start with direct dependencies
202        if let Some(deps) = self.adjacency.get(symbol_name) {
203            for dep in deps {
204                queue.push_back(dep.clone());
205                dependencies.insert(dep.clone());
206            }
207        }
208
209        // BFS to find all transitive dependencies
210        while let Some(current) = queue.pop_front() {
211            if let Some(deps) = self.adjacency.get(&current) {
212                for dep in deps {
213                    if !dependencies.contains(dep) {
214                        dependencies.insert(dep.clone());
215                        queue.push_back(dep.clone());
216                    }
217                }
218            }
219        }
220
221        dependencies
222    }
223
224    /// Detect circular dependencies
225    pub fn find_circular_dependencies(&self) -> Vec<Vec<String>> {
226        let mut cycles = Vec::new();
227        let mut visited = HashSet::new();
228        let mut rec_stack = HashSet::new();
229
230        for symbol_name in self.symbols.keys() {
231            if !visited.contains(symbol_name) {
232                self.dfs_cycles(symbol_name, &mut visited, &mut rec_stack, &mut cycles, Vec::new());
233            }
234        }
235
236        cycles
237    }
238
239    /// DFS helper for cycle detection
240    fn dfs_cycles(
241        &self,
242        node: &str,
243        visited: &mut HashSet<String>,
244        rec_stack: &mut HashSet<String>,
245        cycles: &mut Vec<Vec<String>>,
246        mut path: Vec<String>,
247    ) {
248        visited.insert(node.to_string());
249        rec_stack.insert(node.to_string());
250        path.push(node.to_string());
251
252        if let Some(neighbors) = self.adjacency.get(node) {
253            for neighbor in neighbors {
254                if !visited.contains(neighbor) {
255                    self.dfs_cycles(neighbor, visited, rec_stack, cycles, path.clone());
256                } else if rec_stack.contains(neighbor) {
257                    // Found a cycle
258                    if let Some(start_idx) = path.iter().position(|x| x == neighbor) {
259                        let cycle: Vec<String> = path[start_idx..].to_vec();
260                        if !cycles.contains(&cycle) {
261                            cycles.push(cycle);
262                        }
263                    }
264                }
265            }
266        }
267
268        rec_stack.remove(node);
269    }
270
271    /// Get all symbols in the graph
272    pub fn get_symbols(&self) -> Vec<Symbol> {
273        self.symbols.values().cloned().collect()
274    }
275
276    /// Get all dependencies in the graph
277    pub fn get_dependencies(&self) -> &[Dependency] {
278        &self.dependencies
279    }
280
281    /// Get the number of symbols in the graph
282    pub fn symbol_count(&self) -> usize {
283        self.symbols.len()
284    }
285
286    /// Get the number of dependencies in the graph
287    pub fn dependency_count(&self) -> usize {
288        self.dependencies.len()
289    }
290}
291
292impl Default for DependencyGraph {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_add_symbol() {
304        let mut graph = DependencyGraph::new();
305        let symbol = Symbol {
306            name: "foo".to_string(),
307            file: "main.rs".to_string(),
308            symbol_type: SymbolType::Function,
309        };
310
311        graph.add_symbol(symbol.clone());
312        assert_eq!(graph.symbol_count(), 1);
313    }
314
315    #[test]
316    fn test_add_dependency() {
317        let mut graph = DependencyGraph::new();
318        let from = Symbol {
319            name: "foo".to_string(),
320            file: "main.rs".to_string(),
321            symbol_type: SymbolType::Function,
322        };
323        let to = Symbol {
324            name: "bar".to_string(),
325            file: "main.rs".to_string(),
326            symbol_type: SymbolType::Function,
327        };
328
329        let dep = Dependency {
330            from,
331            to,
332            dep_type: DependencyType::Direct,
333        };
334
335        graph.add_dependency(dep);
336        assert_eq!(graph.symbol_count(), 2);
337        assert_eq!(graph.dependency_count(), 1);
338    }
339
340    #[test]
341    fn test_get_direct_dependents() {
342        let mut graph = DependencyGraph::new();
343        let foo = Symbol {
344            name: "foo".to_string(),
345            file: "main.rs".to_string(),
346            symbol_type: SymbolType::Function,
347        };
348        let bar = Symbol {
349            name: "bar".to_string(),
350            file: "main.rs".to_string(),
351            symbol_type: SymbolType::Function,
352        };
353
354        graph.add_dependency(Dependency {
355            from: bar.clone(),
356            to: foo.clone(),
357            dep_type: DependencyType::Direct,
358        });
359
360        let dependents = graph.get_direct_dependents("foo:main.rs");
361        assert_eq!(dependents.len(), 1);
362        assert_eq!(dependents[0], "bar");
363    }
364
365    #[test]
366    fn test_transitive_dependents() {
367        let mut graph = DependencyGraph::new();
368        let a = Symbol {
369            name: "a".to_string(),
370            file: "main.rs".to_string(),
371            symbol_type: SymbolType::Function,
372        };
373        let b = Symbol {
374            name: "b".to_string(),
375            file: "main.rs".to_string(),
376            symbol_type: SymbolType::Function,
377        };
378        let c = Symbol {
379            name: "c".to_string(),
380            file: "main.rs".to_string(),
381            symbol_type: SymbolType::Function,
382        };
383
384        // a -> b -> c (c depends on b, b depends on a)
385        graph.add_dependency(Dependency {
386            from: b.clone(),
387            to: a.clone(),
388            dep_type: DependencyType::Direct,
389        });
390        graph.add_dependency(Dependency {
391            from: c.clone(),
392            to: b.clone(),
393            dep_type: DependencyType::Direct,
394        });
395
396        let affected = graph.get_transitive_dependents("a:main.rs");
397        assert_eq!(affected.len(), 2);
398        assert!(affected.contains("b"));
399        assert!(affected.contains("c"));
400    }
401
402    #[test]
403    fn test_circular_dependencies() {
404        let mut graph = DependencyGraph::new();
405        let a = Symbol {
406            name: "a".to_string(),
407            file: "main.rs".to_string(),
408            symbol_type: SymbolType::Function,
409        };
410        let b = Symbol {
411            name: "b".to_string(),
412            file: "main.rs".to_string(),
413            symbol_type: SymbolType::Function,
414        };
415
416        // Create a cycle: a -> b -> a
417        graph.add_dependency(Dependency {
418            from: a.clone(),
419            to: b.clone(),
420            dep_type: DependencyType::Direct,
421        });
422        graph.add_dependency(Dependency {
423            from: b.clone(),
424            to: a.clone(),
425            dep_type: DependencyType::Direct,
426        });
427
428        let cycles = graph.find_circular_dependencies();
429        assert!(!cycles.is_empty());
430    }
431}