ricecoder_refactoring/impact/
analyzer.rs1use 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
10pub struct ImpactAnalyzer {
12 graph: DependencyGraph,
14}
15
16impl ImpactAnalyzer {
17 pub fn new() -> Self {
19 Self {
20 graph: DependencyGraph::new(),
21 }
22 }
23
24 pub fn with_graph(graph: DependencyGraph) -> Self {
26 Self { graph }
27 }
28
29 pub fn add_symbol(&mut self, symbol: Symbol) {
31 self.graph.add_symbol(symbol);
32 }
33
34 pub fn add_dependency(&mut self, dependency: Dependency) {
36 self.graph.add_dependency(dependency);
37 }
38
39 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 fn find_affected_symbols(&self, target: &RefactoringTarget) -> Result<HashSet<String>> {
59 let mut affected = HashSet::new();
60
61 affected.insert(target.symbol.clone());
63
64 let target_key = format!("{}:{}", target.symbol, target.file.display());
66
67 let transitive_dependents = self.graph.get_transitive_dependents(&target_key);
70 affected.extend(transitive_dependents);
71
72 Ok(affected)
73 }
74
75 fn find_affected_files(&self, affected_symbols: &HashSet<String>) -> Result<Vec<PathBuf>> {
78 let mut files = HashSet::new();
79
80 let all_symbols = self.graph.get_symbols();
82
83 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 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 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 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 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 fn detect_breaking_changes(&self, analysis: &ImpactAnalysis) -> Result<Vec<String>> {
138 let mut breaking_changes = Vec::new();
139
140 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 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 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 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#[derive(Debug, Clone)]
203pub struct ImpactReport {
204 pub analysis: ImpactAnalysis,
206 pub circular_dependencies: Vec<Vec<String>>,
208 pub breaking_changes: Vec<String>,
210 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 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 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 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 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}