rust_guardian/analyzer/
mod.rs

1//! Main analysis orchestrator for Rust Guardian
2//!
3//! Code Quality Principle: Service Orchestration - Analyzer orchestrates complex validation workflows
4//! - Coordinates path filtering, pattern matching, and result aggregation
5//! - Provides clean interface for validating single files or directory trees
6//! - Handles parallel processing and error recovery gracefully
7
8pub 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
20/// Main analyzer that orchestrates the entire validation process
21pub struct Analyzer {
22    /// Configuration for this analysis
23    config: GuardianConfig,
24    /// Pattern engine for detecting violations
25    pattern_engine: PatternEngine,
26    /// Path filter for determining which files to analyze
27    path_filter: PathFilter,
28    /// Rust-specific analyzer
29    rust_analyzer: RustAnalyzer,
30}
31
32/// Options for customizing analysis behavior
33#[derive(Debug, Clone)]
34pub struct AnalysisOptions {
35    /// Whether to use parallel processing
36    pub parallel: bool,
37    /// Maximum number of files to analyze (-1 for unlimited)
38    pub max_files: Option<usize>,
39    /// Whether to continue on errors or fail fast
40    pub fail_fast: bool,
41    /// Additional paths to exclude for this analysis
42    pub exclude_patterns: Vec<String>,
43    /// Whether to ignore .guardianignore files
44    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    /// Create a new analyzer with the given configuration
61    pub fn new(config: GuardianConfig) -> GuardianResult<Self> {
62        let mut pattern_engine = PatternEngine::new();
63
64        // Load all enabled rules into the pattern engine
65        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        // Create path filter
86        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    /// Create an analyzer with default configuration
99    pub fn with_defaults() -> GuardianResult<Self> {
100        Self::new(GuardianConfig::default())
101    }
102
103    /// Analyze a single file and return violations
104    pub fn analyze_file<P: AsRef<Path>>(&self, file_path: P) -> GuardianResult<Vec<Violation>> {
105        let file_path = file_path.as_ref();
106
107        // Check if file should be analyzed
108        if !self.path_filter.should_analyze(file_path)? {
109            return Ok(Vec::new());
110        }
111
112        // Read file content
113        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        // Apply pattern matching
123        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        // Apply Rust-specific analysis for .rs files
133        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    /// Analyze multiple files and return a complete validation report
147    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        // Collect all files to analyze
156        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        // Apply additional exclusions if specified
170        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        // Limit number of files if requested
179        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        // Analyze files (parallel or sequential)
186        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        // Build final report
193        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    /// Analyze files sequentially
206    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                        // Log error and continue
223                        tracing::warn!("Failed to analyze {}: {}", file_path.display(), e);
224                    }
225                }
226            }
227        }
228
229        Ok(all_violations)
230    }
231
232    /// Analyze files in parallel
233    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        // Handle errors
255        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                // Log all errors
280                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    /// Analyze a directory tree and return a validation report
304    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    /// Get configuration fingerprint for cache validation
313    pub fn config_fingerprint(&self) -> String {
314        self.config.fingerprint()
315    }
316
317    /// Get statistics about the configured patterns
318    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/// Statistics about configured patterns
349#[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
371/// Trait for custom file analyzers
372pub trait FileAnalyzer {
373    /// Analyze a file and return violations
374    fn analyze(&self, file_path: &Path, content: &str) -> GuardianResult<Vec<Violation>>;
375
376    /// Check if this analyzer handles the given file type
377    fn handles_file(&self, file_path: &Path) -> bool;
378}
379
380/// Self-validation methods for analyzer functionality
381/// Following code quality principle: Components should be self-validating
382#[cfg(test)]
383impl Analyzer {
384    /// Validate analyzer creation and configuration
385    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    /// Validate single file analysis capabilities
404    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        // Validate that violations are properly formatted
426        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    /// Validate directory analysis capabilities
445    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        // Create realistic directory structure
458        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        // Create test files with known patterns
472        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        // Validate report structure
486        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        // Validate that target directory is excluded
494        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    /// Validate analysis options functionality
511    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        // Test max_files limitation
551        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/// Comprehensive validation entry point for the analyzer
567/// This replaces traditional unit tests with domain self-validation
568#[cfg(test)]
569pub fn validate_analyzer_domain() -> GuardianResult<()> {
570    let analyzer = Analyzer::with_defaults()?;
571
572    // Validate core functionality
573    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    // Validate pattern statistics
581    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}