Skip to main content

cargo_fl/
analyzer.rs

1use crate::config::Config;
2use crate::rules::{Rule, RuleContext, Issue};
3use crate::walker::RustFileWalker;
4use crate::incremental::{IncrementalAnalyzer, IncrementalResults};
5use crate::ast_cache::{ASTCache, read_rust_file};
6use crate::autofix::{AutoFixEngine, ImportOrganizer, NamingConventionFixer, DocTemplateGenerator};
7use ahash::AHashMap;
8use dashmap::DashMap;
9use rayon::prelude::*;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13pub struct Analyzer {
14    config: Arc<Config>,
15    rules: Vec<Box<dyn Rule>>,
16    incremental_analyzer: Option<IncrementalAnalyzer>,
17    ast_cache: Option<ASTCache>,
18    autofix_engine: AutoFixEngine,
19}
20
21#[derive(Debug, serde::Serialize)]
22pub struct AnalysisResults {
23    pub file_issues: AHashMap<PathBuf, Vec<Issue>>,
24    pub stats: AnalysisStats,
25    pub performance_stats: Option<PerformanceStats>,
26    pub fixed_files: Option<AHashMap<PathBuf, String>>,
27}
28
29#[derive(Debug, Default, serde::Serialize)]
30pub struct AnalysisStats {
31    pub total_files: usize,
32    pub files_with_issues: usize,
33    pub total_issues: usize,
34    pub issues_by_severity: AHashMap<String, usize>,
35}
36
37#[derive(Debug, serde::Serialize)]
38pub struct PerformanceStats {
39    pub cache_hit_rate: f64,
40    pub files_from_cache: usize,
41    pub analysis_time_ms: u128,
42    pub memory_usage_mb: Option<f64>,
43    pub autofix_time_ms: Option<u128>,
44    pub fixes_applied: usize,
45}
46
47impl Analyzer {
48    pub fn new(config: Config) -> Self {
49        let rules = crate::rules::get_enabled_rules(&config);
50        
51        // Initialize incremental analyzer if enabled
52        let incremental_analyzer = if config.performance.incremental_analysis {
53            Some(IncrementalAnalyzer::new(config.clone()))
54        } else {
55            None
56        };
57        
58        // Initialize AST cache if enabled
59        let ast_cache = if config.cache.ast_cache_enabled {
60            let cache_dir = config.cache.cache_dir.clone()
61                .unwrap_or_else(|| std::env::temp_dir().join("cargo-fl-ast"));
62            Some(ASTCache::new(cache_dir))
63        } else {
64            None
65        };
66        
67        Self {
68            config: Arc::new(config),
69            rules,
70            incremental_analyzer,
71            ast_cache,
72            autofix_engine: AutoFixEngine::new(),
73        }
74    }
75    
76    pub fn analyze_path(&mut self, path: &Path) -> AnalysisResults {
77        self.analyze_path_with_options(path, false)
78    }
79    
80    pub fn analyze_path_with_autofix(&mut self, path: &Path) -> AnalysisResults {
81        self.analyze_path_with_options(path, true)
82    }
83    
84    fn analyze_path_with_options(&mut self, path: &Path, apply_autofix: bool) -> AnalysisResults {
85        let start_time = std::time::Instant::now();
86        
87        let walker = RustFileWalker::new();
88        let files: Vec<_> = walker.walk(path).collect();
89        
90        // Use incremental analysis if available
91        let total_files = files.len();
92        
93        let (file_issues, mut performance_stats) = if let Some(ref mut incremental) = self.incremental_analyzer {
94            let incremental_results = incremental.analyze_files(files);
95            let all_issues = incremental_results.all_issues();
96            
97            let perf_stats = PerformanceStats {
98                cache_hit_rate: incremental_results.stats.cache_hit_rate,
99                files_from_cache: incremental_results.stats.files_from_cache,
100                analysis_time_ms: start_time.elapsed().as_millis(),
101                memory_usage_mb: None, // Could implement memory tracking
102                autofix_time_ms: None,
103                fixes_applied: 0,
104            };
105            
106            (all_issues, Some(perf_stats))
107        } else {
108            // Fall back to traditional parallel analysis
109            let file_issues = self.analyze_files_parallel(&files);
110            let perf_stats = PerformanceStats {
111                cache_hit_rate: 0.0,
112                files_from_cache: 0,
113                analysis_time_ms: start_time.elapsed().as_millis(),
114                memory_usage_mb: None,
115                autofix_time_ms: None,
116                fixes_applied: 0,
117            };
118            
119            (file_issues, Some(perf_stats))
120        };
121        
122        // Apply auto-fixes if requested and enabled
123        let mut fixed_files = None;
124        let mut total_fixes_applied = 0;
125        
126        if apply_autofix && self.config.autofix.enabled {
127            let autofix_start = std::time::Instant::now();
128            let mut fixes = AHashMap::new();
129            
130            for (file_path, issues) in &file_issues {
131                if let Ok(content) = read_rust_file(file_path) {
132                    if let Ok(fixed_content) = self.autofix_engine.apply_fixes(&content, issues) {
133                        if fixed_content != content {
134                            fixes.insert(file_path.clone(), fixed_content);
135                        }
136                    }
137                }
138            }
139            
140            total_fixes_applied = self.autofix_engine.fixes_applied;
141            if !fixes.is_empty() {
142                fixed_files = Some(fixes);
143            }
144            
145            // Update performance stats with autofix timing
146            if let Some(ref mut perf_stats) = performance_stats.as_mut() {
147                perf_stats.autofix_time_ms = Some(autofix_start.elapsed().as_millis());
148                perf_stats.fixes_applied = total_fixes_applied;
149            }
150        }
151        
152        // Calculate regular stats
153        let mut stats = AnalysisStats::default();
154        stats.total_files = total_files;
155        stats.files_with_issues = file_issues.len();
156        
157        for issues in file_issues.values() {
158            stats.total_issues += issues.len();
159            for issue in issues {
160                *stats.issues_by_severity
161                    .entry(issue.severity.to_string())
162                    .or_insert(0) += 1;
163            }
164        }
165        
166        AnalysisResults { 
167            file_issues, 
168            stats,
169            performance_stats,
170            fixed_files,
171        }
172    }
173    
174    fn analyze_files_parallel(&self, files: &[PathBuf]) -> AHashMap<PathBuf, Vec<Issue>> {
175        let file_issues: DashMap<PathBuf, Vec<Issue>> = DashMap::new();
176        
177        if self.config.performance.parallel_analysis {
178            files.par_iter().for_each(|file_path| {
179                if let Some(issues) = self.analyze_single_file(file_path) {
180                    if !issues.is_empty() {
181                        file_issues.insert(file_path.clone(), issues);
182                    }
183                }
184            });
185        } else {
186            for file_path in files {
187                if let Some(issues) = self.analyze_single_file(file_path) {
188                    if !issues.is_empty() {
189                        file_issues.insert(file_path.clone(), issues);
190                    }
191                }
192            }
193        }
194        
195        file_issues.into_iter().collect()
196    }
197    
198    fn analyze_single_file(&self, file_path: &Path) -> Option<Vec<Issue>> {
199        // Use memory-mapped reading for large files if enabled
200        let content = if self.config.performance.memory_mapped_io {
201            read_rust_file(file_path).ok()?
202        } else {
203            std::fs::read_to_string(file_path).ok()?
204        };
205        
206        // Use AST cache if available
207        let syntax_tree = if let Some(ref ast_cache) = self.ast_cache {
208            ast_cache.get_or_parse(file_path).ok()?
209        } else {
210            syn::parse_file(&content).ok()?
211        };
212        
213        let mut ctx = RuleContext::new(
214            file_path.to_path_buf(),
215            content,
216            syntax_tree,
217        );
218        
219        // Apply each rule
220        for rule in &self.rules {
221            rule.check(&mut ctx);
222        }
223        
224        Some(ctx.issues)
225    }
226
227    pub fn analyze_file(&self, path: &Path) -> AnalysisResults {
228        let mut file_issues = AHashMap::new();
229        
230        if let Ok(content) = std::fs::read_to_string(path) {
231            if let Ok(syntax_tree) = syn::parse_file(&content) {
232                let mut ctx = RuleContext::new(
233                    path.to_path_buf(),
234                    content.clone(),
235                    syntax_tree,
236                );
237                
238                // Apply each rule
239                for rule in &self.rules {
240                    rule.check(&mut ctx);
241                }
242                
243                if !ctx.issues.is_empty() {
244                    file_issues.insert(path.to_path_buf(), ctx.issues);
245                }
246            }
247        }
248        
249        let mut stats = AnalysisStats::default();
250        stats.total_files = 1;
251        stats.files_with_issues = if file_issues.is_empty() { 0 } else { 1 };
252        
253        for issues in file_issues.values() {
254            stats.total_issues += issues.len();
255            for issue in issues {
256                *stats.issues_by_severity
257                    .entry(issue.severity.to_string())
258                    .or_insert(0) += 1;
259            }
260        }
261        
262        AnalysisResults { 
263            file_issues, 
264            stats,
265            performance_stats: None,
266            fixed_files: None,
267        }
268    }
269}
270
271impl AnalysisResults {
272    pub fn total_issues(&self) -> usize {
273        self.stats.total_issues
274    }
275    
276    pub fn file_count(&self) -> usize {
277        self.stats.total_files
278    }
279    
280    pub fn files_with_issues(&self) -> usize {
281        self.stats.files_with_issues
282    }
283    
284    pub fn fixable_count(&self) -> usize {
285        self.file_issues
286            .values()
287            .flat_map(|issues| issues.iter())
288            .filter(|issue| issue.fix.is_some())
289            .count()
290    }
291    
292    pub fn cache_hit_rate(&self) -> f64 {
293        self.performance_stats
294            .as_ref()
295            .map(|stats| stats.cache_hit_rate)
296            .unwrap_or(0.0)
297    }
298    
299    pub fn analysis_time_ms(&self) -> u128 {
300        self.performance_stats
301            .as_ref()
302            .map(|stats| stats.analysis_time_ms)
303            .unwrap_or(0)
304    }
305    
306    pub fn fixes_applied(&self) -> usize {
307        self.performance_stats
308            .as_ref()
309            .map(|stats| stats.fixes_applied)
310            .unwrap_or(0)
311    }
312    
313    pub fn has_fixes(&self) -> bool {
314        self.fixed_files.is_some()
315    }
316}