1pub mod rust;
9
10use crate::analyzer::rust::RustAnalyzer;
11use crate::config::GuardianConfig;
12use crate::domain::violations::{GuardianError, GuardianResult, ValidationReport, Violation};
13use crate::patterns::{PathFilter, PatternEngine};
14use rayon::prelude::*;
15use std::fs;
16use std::path::{Path, PathBuf};
17use std::sync::{Arc, Mutex};
18use std::time::Instant;
19
20pub struct Analyzer {
22 config: GuardianConfig,
24 pattern_engine: PatternEngine,
26 path_filter: PathFilter,
28 rust_analyzer: RustAnalyzer,
30}
31
32#[derive(Debug, Clone)]
34pub struct AnalysisOptions {
35 pub parallel: bool,
37 pub max_files: Option<usize>,
39 pub fail_fast: bool,
41 pub exclude_patterns: Vec<String>,
43 pub ignore_ignore_files: bool,
45}
46
47impl Default for AnalysisOptions {
48 fn default() -> Self {
49 Self {
50 parallel: true,
51 max_files: None,
52 fail_fast: false,
53 exclude_patterns: Vec::new(),
54 ignore_ignore_files: false,
55 }
56 }
57}
58
59impl Analyzer {
60 pub fn new(config: GuardianConfig) -> GuardianResult<Self> {
62 let mut pattern_engine = PatternEngine::new();
63
64 for (category_name, category) in &config.patterns {
66 if !category.enabled {
67 continue;
68 }
69
70 for rule in &category.rules {
71 if !rule.enabled {
72 continue;
73 }
74
75 let effective_severity = config.effective_severity(category, rule);
76 pattern_engine.add_rule(rule, effective_severity).map_err(|e| {
77 GuardianError::config(format!(
78 "Failed to add rule '{}' in category '{}': {}",
79 rule.id, category_name, e
80 ))
81 })?;
82 }
83 }
84
85 let ignore_file = if config.paths.ignore_file.as_deref() == Some("") {
87 None
88 } else {
89 config.paths.ignore_file.clone()
90 };
91
92 let path_filter = PathFilter::new(config.paths.patterns.clone(), ignore_file)
93 .map_err(|e| GuardianError::config(format!("Failed to create path filter: {e}")))?;
94
95 Ok(Self { config, pattern_engine, path_filter, rust_analyzer: RustAnalyzer::new() })
96 }
97
98 pub fn with_defaults() -> GuardianResult<Self> {
100 Self::new(GuardianConfig::default())
101 }
102
103 pub fn analyze_file<P: AsRef<Path>>(&self, file_path: P) -> GuardianResult<Vec<Violation>> {
105 let file_path = file_path.as_ref();
106
107 if !self.path_filter.should_analyze(file_path)? {
109 return Ok(Vec::new());
110 }
111
112 let content = fs::read_to_string(file_path).map_err(|e| {
114 GuardianError::analysis(
115 file_path.display().to_string(),
116 format!("Failed to read file: {e}"),
117 )
118 })?;
119
120 let mut all_violations = Vec::new();
121
122 let matches = self.pattern_engine.analyze_file(file_path, &content).map_err(|e| {
124 GuardianError::analysis(
125 file_path.display().to_string(),
126 format!("Pattern analysis failed: {e}"),
127 )
128 })?;
129
130 all_violations.extend(self.pattern_engine.matches_to_violations(matches));
131
132 if self.rust_analyzer.handles_file(file_path) {
134 let rust_violations = self.rust_analyzer.analyze(file_path, &content).map_err(|e| {
135 GuardianError::analysis(
136 file_path.display().to_string(),
137 format!("Rust analysis failed: {e}"),
138 )
139 })?;
140 all_violations.extend(rust_violations);
141 }
142
143 Ok(all_violations)
144 }
145
146 pub fn analyze_paths<P: AsRef<Path>>(
148 &self,
149 paths: &[P],
150 options: &AnalysisOptions,
151 ) -> GuardianResult<ValidationReport> {
152 let start_time = Instant::now();
153 let mut report = ValidationReport::new();
154
155 let mut files_to_analyze = Vec::new();
157
158 for path in paths {
159 let path = path.as_ref();
160
161 if path.is_file() {
162 files_to_analyze.push(path.to_path_buf());
163 } else if path.is_dir() {
164 let discovered_files = self.path_filter.find_files(path)?;
165 files_to_analyze.extend(discovered_files);
166 }
167 }
168
169 if !options.exclude_patterns.is_empty() {
171 let mut temp_filter = self.path_filter.clone();
172 for pattern in &options.exclude_patterns {
173 temp_filter.add_pattern(pattern.clone())?;
174 }
175 files_to_analyze = temp_filter.filter_paths(&files_to_analyze)?;
176 }
177
178 if let Some(max_files) = options.max_files {
180 files_to_analyze.truncate(max_files);
181 }
182
183 let total_files = files_to_analyze.len();
184
185 let violations = if options.parallel && files_to_analyze.len() > 1 {
187 self.analyze_files_parallel(&files_to_analyze, options)?
188 } else {
189 self.analyze_files_sequential(&files_to_analyze, options)?
190 };
191
192 for violation in violations {
194 report.add_violation(violation);
195 }
196
197 report.set_files_analyzed(total_files);
198 report.set_execution_time(start_time.elapsed().as_millis() as u64);
199 report.set_config_fingerprint(self.config.fingerprint());
200 report.sort_violations();
201
202 Ok(report)
203 }
204
205 fn analyze_files_sequential(
207 &self,
208 files: &[PathBuf],
209 options: &AnalysisOptions,
210 ) -> GuardianResult<Vec<Violation>> {
211 let mut all_violations = Vec::new();
212
213 for file_path in files {
214 match self.analyze_file(file_path) {
215 Ok(violations) => {
216 all_violations.extend(violations);
217 }
218 Err(e) => {
219 if options.fail_fast {
220 return Err(e);
221 } else {
222 tracing::warn!("Failed to analyze {}: {}", file_path.display(), e);
224 }
225 }
226 }
227 }
228
229 Ok(all_violations)
230 }
231
232 fn analyze_files_parallel(
234 &self,
235 files: &[PathBuf],
236 options: &AnalysisOptions,
237 ) -> GuardianResult<Vec<Violation>> {
238 let violations = Arc::new(Mutex::new(Vec::new()));
239 let errors = Arc::new(Mutex::new(Vec::new()));
240
241 files.par_iter().for_each(|file_path| match self.analyze_file(file_path) {
242 Ok(file_violations) => {
243 if let Ok(mut v) = violations.lock() {
244 v.extend(file_violations);
245 }
246 }
247 Err(e) => {
248 if let Ok(mut errs) = errors.lock() {
249 errs.push((file_path.clone(), e));
250 }
251 }
252 });
253
254 let errors = Arc::try_unwrap(errors)
256 .map_err(|_| {
257 GuardianError::analysis(
258 "parallel_analysis".to_string(),
259 "Failed to unwrap errors Arc".to_string(),
260 )
261 })?
262 .into_inner()
263 .map_err(|_| {
264 GuardianError::analysis(
265 "parallel_analysis".to_string(),
266 "Failed to lock errors mutex".to_string(),
267 )
268 })?;
269
270 if !errors.is_empty() {
271 if options.fail_fast {
272 if let Some((file_path, error)) = errors.into_iter().next() {
273 return Err(GuardianError::analysis(
274 file_path.display().to_string(),
275 error.to_string(),
276 ));
277 }
278 } else {
279 for (file_path, error) in errors {
281 tracing::warn!("Failed to analyze {}: {}", file_path.display(), error);
282 }
283 }
284 }
285
286 let violations = Arc::try_unwrap(violations)
287 .map_err(|_| {
288 GuardianError::analysis(
289 "parallel_analysis".to_string(),
290 "Failed to unwrap violations Arc".to_string(),
291 )
292 })?
293 .into_inner()
294 .map_err(|_| {
295 GuardianError::analysis(
296 "parallel_analysis".to_string(),
297 "Failed to lock violations mutex".to_string(),
298 )
299 })?;
300 Ok(violations)
301 }
302
303 pub fn analyze_directory<P: AsRef<Path>>(
305 &self,
306 root: P,
307 options: &AnalysisOptions,
308 ) -> GuardianResult<ValidationReport> {
309 self.analyze_paths(&[root.as_ref()], options)
310 }
311
312 pub fn config_fingerprint(&self) -> String {
314 self.config.fingerprint()
315 }
316
317 pub fn pattern_stats(&self) -> PatternStats {
319 let mut stats = PatternStats::default();
320
321 for category in self.config.patterns.values() {
322 if category.enabled {
323 stats.enabled_categories += 1;
324
325 for rule in &category.rules {
326 if rule.enabled {
327 stats.enabled_rules += 1;
328 match rule.rule_type {
329 crate::config::RuleType::Regex => stats.regex_patterns += 1,
330 crate::config::RuleType::Ast => stats.ast_patterns += 1,
331 crate::config::RuleType::Semantic => stats.semantic_patterns += 1,
332 crate::config::RuleType::ImportAnalysis => stats.import_patterns += 1,
333 }
334 } else {
335 stats.disabled_rules += 1;
336 }
337 }
338 } else {
339 stats.disabled_categories += 1;
340 stats.disabled_rules += category.rules.len();
341 }
342 }
343
344 stats
345 }
346}
347
348#[derive(Debug, Default)]
350pub struct PatternStats {
351 pub enabled_categories: usize,
352 pub disabled_categories: usize,
353 pub enabled_rules: usize,
354 pub disabled_rules: usize,
355 pub regex_patterns: usize,
356 pub ast_patterns: usize,
357 pub semantic_patterns: usize,
358 pub import_patterns: usize,
359}
360
361impl PatternStats {
362 pub fn total_categories(&self) -> usize {
363 self.enabled_categories + self.disabled_categories
364 }
365
366 pub fn total_rules(&self) -> usize {
367 self.enabled_rules + self.disabled_rules
368 }
369}
370
371pub trait FileAnalyzer {
373 fn analyze(&self, file_path: &Path, content: &str) -> GuardianResult<Vec<Violation>>;
375
376 fn handles_file(&self, file_path: &Path) -> bool;
378}
379
380#[cfg(test)]
383impl Analyzer {
384 pub fn validate_initialization(&self) -> GuardianResult<()> {
386 let stats = self.pattern_stats();
387
388 if stats.enabled_rules == 0 {
389 return Err(GuardianError::config(
390 "Analyzer must have at least one enabled rule".to_string(),
391 ));
392 }
393
394 if stats.regex_patterns == 0 && stats.ast_patterns == 0 && stats.semantic_patterns == 0 {
395 return Err(GuardianError::config(
396 "Analyzer must have at least one pattern type enabled".to_string(),
397 ));
398 }
399
400 Ok(())
401 }
402
403 pub fn validate_file_analysis(&self, test_content: &str) -> GuardianResult<()> {
405 use std::fs;
406 use tempfile::TempDir;
407
408 let temp_dir = TempDir::new().map_err(|e| {
409 GuardianError::analysis(
410 "validation".to_string(),
411 format!("Failed to create temp dir: {e}"),
412 )
413 })?;
414 let file_path = temp_dir.path().join("validation_test.rs");
415
416 fs::write(&file_path, test_content).map_err(|e| {
417 GuardianError::analysis(
418 "validation".to_string(),
419 format!("Failed to write test file: {e}"),
420 )
421 })?;
422
423 let violations = self.analyze_file(&file_path)?;
424
425 for violation in &violations {
427 if violation.rule_id.is_empty() {
428 return Err(GuardianError::analysis(
429 "validation".to_string(),
430 "Violation missing rule_id".to_string(),
431 ));
432 }
433 if violation.message.is_empty() {
434 return Err(GuardianError::analysis(
435 "validation".to_string(),
436 "Violation missing message".to_string(),
437 ));
438 }
439 }
440
441 Ok(())
442 }
443
444 pub fn validate_directory_analysis(&self) -> GuardianResult<()> {
446 use std::fs;
447 use tempfile::TempDir;
448
449 let temp_dir = TempDir::new().map_err(|e| {
450 GuardianError::analysis(
451 "validation".to_string(),
452 format!("Failed to create temp dir: {e}"),
453 )
454 })?;
455 let root = temp_dir.path();
456
457 fs::create_dir_all(root.join("src")).map_err(|e| {
459 GuardianError::analysis(
460 "validation".to_string(),
461 format!("Failed to create src dir: {e}"),
462 )
463 })?;
464 fs::create_dir_all(root.join("target/debug")).map_err(|e| {
465 GuardianError::analysis(
466 "validation".to_string(),
467 format!("Failed to create target dir: {e}"),
468 )
469 })?;
470
471 fs::write(root.join("src/lib.rs"), "//! Test module\n//!\n//! Code Quality Principle: Self-validation\npub fn test() { /* implementation */ }")
473 .map_err(|e| GuardianError::analysis("validation".to_string(), format!("Failed to write lib.rs: {e}")))?;
474 fs::write(root.join("src/main.rs"), "//! Main module\n//!\n//! Code Quality Principle: Entry point\nfn main() { eprintln!(\"Application starting\"); }")
475 .map_err(|e| GuardianError::analysis("validation".to_string(), format!("Failed to write main.rs: {e}")))?;
476 fs::write(root.join("target/debug/app"), "binary content").map_err(|e| {
477 GuardianError::analysis(
478 "validation".to_string(),
479 format!("Failed to write binary: {e}"),
480 )
481 })?;
482
483 let report = self.analyze_directory(root, &AnalysisOptions::default())?;
484
485 if report.summary.total_files == 0 {
487 return Err(GuardianError::analysis(
488 "validation".to_string(),
489 "Directory analysis should find at least one file".to_string(),
490 ));
491 }
492
493 let target_violations = report
495 .violations
496 .iter()
497 .filter(|v| v.file_path.to_string_lossy().contains("target/"))
498 .count();
499
500 if target_violations > 0 {
501 return Err(GuardianError::analysis(
502 "validation".to_string(),
503 "Target directory should be excluded from analysis".to_string(),
504 ));
505 }
506
507 Ok(())
508 }
509
510 pub fn validate_analysis_options(&self) -> GuardianResult<()> {
512 use std::fs;
513 use tempfile::TempDir;
514
515 let temp_dir = TempDir::new().map_err(|e| {
516 GuardianError::analysis(
517 "validation".to_string(),
518 format!("Failed to create temp dir: {e}"),
519 )
520 })?;
521 let root = temp_dir.path();
522
523 fs::create_dir_all(root.join("src")).map_err(|e| {
524 GuardianError::analysis(
525 "validation".to_string(),
526 format!("Failed to create src dir: {e}"),
527 )
528 })?;
529 fs::write(
530 root.join("src/lib.rs"),
531 "//! Test lib\n//!\n//! Code Quality Principle: Testing\npub fn lib() {}",
532 )
533 .map_err(|e| {
534 GuardianError::analysis(
535 "validation".to_string(),
536 format!("Failed to write lib.rs: {e}"),
537 )
538 })?;
539 fs::write(
540 root.join("src/main.rs"),
541 "//! Test main\n//!\n//! Code Quality Principle: Entry\nfn main() {}",
542 )
543 .map_err(|e| {
544 GuardianError::analysis(
545 "validation".to_string(),
546 format!("Failed to write main.rs: {e}"),
547 )
548 })?;
549
550 let options = AnalysisOptions { max_files: Some(1), ..Default::default() };
552
553 let report = self.analyze_directory(root, &options)?;
554
555 if report.summary.total_files != 1 {
556 return Err(GuardianError::analysis(
557 "validation".to_string(),
558 format!("Expected 1 file with max_files=1, got {}", report.summary.total_files),
559 ));
560 }
561
562 Ok(())
563 }
564}
565
566#[cfg(test)]
569pub fn validate_analyzer_domain() -> GuardianResult<()> {
570 let analyzer = Analyzer::with_defaults()?;
571
572 analyzer.validate_initialization()?;
574 analyzer.validate_file_analysis(
575 "//! Test\n//!\n//! Code Quality Principle: Validation\nfn test() {}",
576 )?;
577 analyzer.validate_directory_analysis()?;
578 analyzer.validate_analysis_options()?;
579
580 let stats = analyzer.pattern_stats();
582 if stats.total_rules() == 0 {
583 return Err(GuardianError::config(
584 "Pattern statistics validation failed: no rules configured".to_string(),
585 ));
586 }
587
588 if stats.total_categories() == 0 {
589 return Err(GuardianError::config(
590 "Pattern statistics validation failed: no categories configured".to_string(),
591 ));
592 }
593
594 Ok(())
595}