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 let incremental_analyzer = if config.performance.incremental_analysis {
53 Some(IncrementalAnalyzer::new(config.clone()))
54 } else {
55 None
56 };
57
58 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 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, autofix_time_ms: None,
103 fixes_applied: 0,
104 };
105
106 (all_issues, Some(perf_stats))
107 } else {
108 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 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 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 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 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 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 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 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}