ricecoder_refactoring/impact/
analyzer.rs

1//! Impact analysis for refactoring operations
2
3use crate::error::Result;
4use crate::types::{ImpactAnalysis, Refactoring, RefactoringTarget, RiskLevel};
5#[allow(unused_imports)]
6use super::graph::{DependencyGraph, Symbol, SymbolType, Dependency, DependencyType};
7use std::collections::HashSet;
8use std::path::PathBuf;
9
10/// Analyzes the impact of refactoring operations
11pub struct ImpactAnalyzer {
12    /// Dependency graph for the codebase
13    graph: DependencyGraph,
14}
15
16impl ImpactAnalyzer {
17    /// Create a new impact analyzer
18    pub fn new() -> Self {
19        Self {
20            graph: DependencyGraph::new(),
21        }
22    }
23
24    /// Create an analyzer with a pre-built dependency graph
25    pub fn with_graph(graph: DependencyGraph) -> Self {
26        Self { graph }
27    }
28
29    /// Add a symbol to the dependency graph
30    pub fn add_symbol(&mut self, symbol: Symbol) {
31        self.graph.add_symbol(symbol);
32    }
33
34    /// Add a dependency to the graph
35    pub fn add_dependency(&mut self, dependency: Dependency) {
36        self.graph.add_dependency(dependency);
37    }
38
39    /// Analyze the impact of a refactoring
40    pub fn analyze(&self, refactoring: &Refactoring) -> Result<ImpactAnalysis> {
41        let affected_symbols = self.find_affected_symbols(&refactoring.target)?;
42        let affected_files = self.find_affected_files(&affected_symbols)?;
43        
44        let affected_symbols_vec: Vec<String> = affected_symbols.iter().cloned().collect();
45        let risk_level = Self::calculate_risk_level(&affected_files, &affected_symbols_vec);
46        let estimated_effort = Self::estimate_effort(&affected_files, &affected_symbols_vec);
47
48        Ok(ImpactAnalysis {
49            affected_files,
50            affected_symbols: affected_symbols_vec,
51            risk_level,
52            estimated_effort,
53        })
54    }
55
56    /// Find all symbols affected by a refactoring
57    /// This includes the target symbol and all symbols that transitively depend on it
58    fn find_affected_symbols(&self, target: &RefactoringTarget) -> Result<HashSet<String>> {
59        let mut affected = HashSet::new();
60
61        // Add the target symbol itself
62        affected.insert(target.symbol.clone());
63
64        // Create a composite key for the target symbol (name:file)
65        let target_key = format!("{}:{}", target.symbol, target.file.display());
66        
67        // Find all symbols that transitively depend on the target
68        // get_transitive_dependents returns just symbol names (not composite keys)
69        let transitive_dependents = self.graph.get_transitive_dependents(&target_key);
70        affected.extend(transitive_dependents);
71
72        Ok(affected)
73    }
74
75    /// Find files affected by the refactoring
76    /// Returns unique files that contain affected symbols
77    fn find_affected_files(&self, affected_symbols: &HashSet<String>) -> Result<Vec<PathBuf>> {
78        let mut files = HashSet::new();
79
80        // Get all symbols from the graph
81        let all_symbols = self.graph.get_symbols();
82        
83        // Build a map of symbol names to their files
84        let mut symbol_files: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
85        for symbol in all_symbols {
86            symbol_files
87                .entry(symbol.name.clone())
88                .or_default()
89                .push(symbol.file.clone());
90        }
91
92        // For each affected symbol, add all files where it appears
93        for symbol_name in affected_symbols {
94            if let Some(file_list) = symbol_files.get(symbol_name) {
95                for file in file_list {
96                    files.insert(PathBuf::from(file));
97                }
98            }
99        }
100
101        Ok(files.into_iter().collect())
102    }
103
104    /// Calculate risk level based on impact
105    fn calculate_risk_level(affected_files: &[PathBuf], affected_symbols: &[String]) -> RiskLevel {
106        let total_impact = affected_files.len() + affected_symbols.len();
107
108        match total_impact {
109            0..=2 => RiskLevel::Low,
110            3..=10 => RiskLevel::Medium,
111            _ => RiskLevel::High,
112        }
113    }
114
115    /// Estimate effort required for the refactoring
116    fn estimate_effort(affected_files: &[PathBuf], affected_symbols: &[String]) -> u8 {
117        let total_impact = affected_files.len() + affected_symbols.len();
118        std::cmp::min(10, (total_impact as u8) + 1)
119    }
120
121    /// Generate a detailed impact report
122    pub fn generate_report(&self, refactoring: &Refactoring) -> Result<ImpactReport> {
123        let analysis = self.analyze(refactoring)?;
124        let circular_deps = self.graph.find_circular_dependencies();
125        let breaking_changes = self.detect_breaking_changes(&analysis)?;
126        let recommendations = self.generate_recommendations(&analysis);
127
128        Ok(ImpactReport {
129            analysis,
130            circular_dependencies: circular_deps,
131            breaking_changes,
132            recommendations,
133        })
134    }
135
136    /// Detect potential breaking changes
137    fn detect_breaking_changes(&self, analysis: &ImpactAnalysis) -> Result<Vec<String>> {
138        let mut breaking_changes = Vec::new();
139
140        // Check for high-risk changes
141        if analysis.risk_level == RiskLevel::High {
142            breaking_changes.push(
143                "High-risk refactoring: affects many symbols and files".to_string(),
144            );
145        }
146
147        // Check for circular dependencies
148        let cycles = self.graph.find_circular_dependencies();
149        if !cycles.is_empty() {
150            breaking_changes.push(format!(
151                "Circular dependencies detected: {} cycles found",
152                cycles.len()
153            ));
154        }
155
156        Ok(breaking_changes)
157    }
158
159    /// Generate recommendations for the refactoring
160    fn generate_recommendations(&self, analysis: &ImpactAnalysis) -> Vec<String> {
161        let mut recommendations = Vec::new();
162
163        match analysis.risk_level {
164            RiskLevel::Low => {
165                recommendations.push("Low-risk refactoring. Safe to proceed.".to_string());
166            }
167            RiskLevel::Medium => {
168                recommendations.push("Medium-risk refactoring. Review affected symbols carefully.".to_string());
169                recommendations.push("Consider running tests after refactoring.".to_string());
170            }
171            RiskLevel::High => {
172                recommendations.push("High-risk refactoring. Proceed with caution.".to_string());
173                recommendations.push("Create a backup before applying changes.".to_string());
174                recommendations.push("Run comprehensive tests after refactoring.".to_string());
175                recommendations.push("Consider breaking the refactoring into smaller steps.".to_string());
176            }
177        }
178
179        if analysis.affected_files.len() > 10 {
180            recommendations.push(format!(
181                "Many files affected ({}). Consider a phased approach.",
182                analysis.affected_files.len()
183            ));
184        }
185
186        recommendations
187    }
188
189    /// Get the dependency graph
190    pub fn graph(&self) -> &DependencyGraph {
191        &self.graph
192    }
193}
194
195impl Default for ImpactAnalyzer {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201/// Detailed impact report for a refactoring
202#[derive(Debug, Clone)]
203pub struct ImpactReport {
204    /// Impact analysis
205    pub analysis: ImpactAnalysis,
206    /// Circular dependencies found
207    pub circular_dependencies: Vec<Vec<String>>,
208    /// Potential breaking changes
209    pub breaking_changes: Vec<String>,
210    /// Recommendations
211    pub recommendations: Vec<String>,
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::types::{Refactoring, RefactoringOptions, RefactoringType};
218
219    #[test]
220    fn test_analyze_refactoring() -> Result<()> {
221        let mut analyzer = ImpactAnalyzer::new();
222
223        // Add symbols to the graph
224        let target_symbol = Symbol {
225            name: "old_name".to_string(),
226            file: "src/main.rs".to_string(),
227            symbol_type: SymbolType::Function,
228        };
229        analyzer.add_symbol(target_symbol);
230
231        let refactoring = Refactoring {
232            id: "test-refactoring".to_string(),
233            refactoring_type: RefactoringType::Rename,
234            target: RefactoringTarget {
235                file: PathBuf::from("src/main.rs"),
236                symbol: "old_name".to_string(),
237                range: None,
238            },
239            options: RefactoringOptions::default(),
240        };
241
242        let impact = analyzer.analyze(&refactoring)?;
243        assert_eq!(impact.affected_files.len(), 1);
244        assert_eq!(impact.affected_symbols.len(), 1);
245
246        Ok(())
247    }
248
249    #[test]
250    fn test_transitive_impact() -> Result<()> {
251        let mut analyzer = ImpactAnalyzer::new();
252
253        // Create a chain: a -> b -> c
254        let a = Symbol {
255            name: "a".to_string(),
256            file: "src/main.rs".to_string(),
257            symbol_type: SymbolType::Function,
258        };
259        let b = Symbol {
260            name: "b".to_string(),
261            file: "src/lib.rs".to_string(),
262            symbol_type: SymbolType::Function,
263        };
264        let c = Symbol {
265            name: "c".to_string(),
266            file: "src/utils.rs".to_string(),
267            symbol_type: SymbolType::Function,
268        };
269
270        analyzer.add_symbol(a.clone());
271        analyzer.add_symbol(b.clone());
272        analyzer.add_symbol(c.clone());
273
274        // b depends on a, c depends on b
275        analyzer.add_dependency(Dependency {
276            from: b.clone(),
277            to: a.clone(),
278            dep_type: DependencyType::Direct,
279        });
280        analyzer.add_dependency(Dependency {
281            from: c.clone(),
282            to: b.clone(),
283            dep_type: DependencyType::Direct,
284        });
285
286        let refactoring = Refactoring {
287            id: "test-refactoring".to_string(),
288            refactoring_type: RefactoringType::Rename,
289            target: RefactoringTarget {
290                file: PathBuf::from("src/main.rs"),
291                symbol: "a".to_string(),
292                range: None,
293            },
294            options: RefactoringOptions::default(),
295        };
296
297        let impact = analyzer.analyze(&refactoring)?;
298        // Should affect a, b, and c
299        assert_eq!(impact.affected_symbols.len(), 3);
300        assert_eq!(impact.affected_files.len(), 3);
301
302        Ok(())
303    }
304
305    #[test]
306    fn test_risk_level_low() {
307        let files = vec![PathBuf::from("src/main.rs")];
308        let symbols = vec!["symbol1".to_string()];
309        let risk = ImpactAnalyzer::calculate_risk_level(&files, &symbols);
310        assert_eq!(risk, RiskLevel::Low);
311    }
312
313    #[test]
314    fn test_risk_level_medium() {
315        let files = vec![
316            PathBuf::from("src/main.rs"),
317            PathBuf::from("src/lib.rs"),
318            PathBuf::from("src/utils.rs"),
319        ];
320        let symbols = vec!["symbol1".to_string(), "symbol2".to_string()];
321        let risk = ImpactAnalyzer::calculate_risk_level(&files, &symbols);
322        assert_eq!(risk, RiskLevel::Medium);
323    }
324
325    #[test]
326    fn test_risk_level_high() {
327        let files: Vec<PathBuf> = (0..15).map(|i| PathBuf::from(format!("src/file{}.rs", i))).collect();
328        let symbols: Vec<String> = (0..10).map(|i| format!("symbol{}", i)).collect();
329        let risk = ImpactAnalyzer::calculate_risk_level(&files, &symbols);
330        assert_eq!(risk, RiskLevel::High);
331    }
332
333    #[test]
334    fn test_estimate_effort() {
335        let files = vec![PathBuf::from("src/main.rs")];
336        let symbols = vec!["symbol1".to_string()];
337        let effort = ImpactAnalyzer::estimate_effort(&files, &symbols);
338        assert!(effort > 0 && effort <= 10);
339    }
340
341    #[test]
342    fn test_generate_report() -> Result<()> {
343        let mut analyzer = ImpactAnalyzer::new();
344
345        let target_symbol = Symbol {
346            name: "old_name".to_string(),
347            file: "src/main.rs".to_string(),
348            symbol_type: SymbolType::Function,
349        };
350        analyzer.add_symbol(target_symbol);
351
352        let refactoring = Refactoring {
353            id: "test-refactoring".to_string(),
354            refactoring_type: RefactoringType::Rename,
355            target: RefactoringTarget {
356                file: PathBuf::from("src/main.rs"),
357                symbol: "old_name".to_string(),
358                range: None,
359            },
360            options: RefactoringOptions::default(),
361        };
362
363        let report = analyzer.generate_report(&refactoring)?;
364        assert!(!report.recommendations.is_empty());
365
366        Ok(())
367    }
368}