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