From 9c13ae28262656a4ffa48bbaeaad2560658c7683 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 04:08:15 +0000 Subject: [PATCH 1/2] Initial plan From ebe9e6640b327c68a9c7264e53f45cd75cb94ed5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 Aug 2025 04:15:20 +0000 Subject: [PATCH 2/2] Fix: Apply rustfmt formatting to resolve CI pipeline failures Co-authored-by: Theaxiom <57013+Theaxiom@users.noreply.github.com> --- src/analyzer/mod.rs | 233 +++++++++++-------- src/analyzer/rust.rs | 261 ++++++++++++--------- src/cache/mod.rs | 220 +++++++++-------- src/config/mod.rs | 324 ++++++++++++++------------ src/domain/mod.rs | 4 +- src/domain/violations.rs | 103 ++++---- src/lib.rs | 267 ++++++++++----------- src/main.rs | 453 ++++++++++++++++++++---------------- src/patterns/mod.rs | 352 +++++++++++++++++----------- src/patterns/path_filter.rs | 282 ++++++++++++---------- src/report/mod.rs | 403 +++++++++++++++++++------------- 11 files changed, 1635 insertions(+), 1267 deletions(-) diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 4f6eb6a..573c979 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -1,5 +1,5 @@ //! Main analysis orchestrator for Rust Guardian -//! +//! //! CDD Principle: Domain Services - Analyzer orchestrates complex validation workflows //! - Coordinates path filtering, pattern matching, and result aggregation //! - Provides clean interface for validating single files or directory trees @@ -7,15 +7,15 @@ pub mod rust; -use crate::config::GuardianConfig; -use crate::domain::violations::{ValidationReport, Violation, GuardianError, GuardianResult}; -use crate::patterns::{PatternEngine, PathFilter}; use crate::analyzer::rust::RustAnalyzer; +use crate::config::GuardianConfig; +use crate::domain::violations::{GuardianError, GuardianResult, ValidationReport, Violation}; +use crate::patterns::{PathFilter, PatternEngine}; use rayon::prelude::*; +use std::fs; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::Instant; -use std::fs; /// Main analyzer that orchestrates the entire validation process pub struct Analyzer { @@ -60,37 +60,40 @@ impl Analyzer { /// Create a new analyzer with the given configuration pub fn new(config: GuardianConfig) -> GuardianResult { let mut pattern_engine = PatternEngine::new(); - + // Load all enabled rules into the pattern engine for (category_name, category) in &config.patterns { if !category.enabled { continue; } - + for rule in &category.rules { if !rule.enabled { continue; } - + let effective_severity = config.effective_severity(category, rule); - pattern_engine.add_rule(rule, effective_severity) - .map_err(|e| GuardianError::config(format!( - "Failed to add rule '{}' in category '{}': {}", - rule.id, category_name, e - )))?; + pattern_engine + .add_rule(rule, effective_severity) + .map_err(|e| { + GuardianError::config(format!( + "Failed to add rule '{}' in category '{}': {}", + rule.id, category_name, e + )) + })?; } } - + // Create path filter let ignore_file = if config.paths.ignore_file.as_deref() == Some("") { None } else { config.paths.ignore_file.clone() }; - + let path_filter = PathFilter::new(config.paths.patterns.clone(), ignore_file) .map_err(|e| GuardianError::config(format!("Failed to create path filter: {}", e)))?; - + Ok(Self { config, pattern_engine, @@ -98,63 +101,76 @@ impl Analyzer { rust_analyzer: RustAnalyzer::new(), }) } - + /// Create an analyzer with default configuration pub fn with_defaults() -> GuardianResult { Self::new(GuardianConfig::default()) } - + /// Analyze a single file and return violations pub fn analyze_file>(&self, file_path: P) -> GuardianResult> { let file_path = file_path.as_ref(); - + // Check if file should be analyzed if !self.path_filter.should_analyze(file_path)? { return Ok(Vec::new()); } - + // Read file content - let content = fs::read_to_string(file_path) - .map_err(|e| GuardianError::analysis( + let content = fs::read_to_string(file_path).map_err(|e| { + GuardianError::analysis( file_path.display().to_string(), - format!("Failed to read file: {}", e) - ))?; - + format!("Failed to read file: {}", e), + ) + })?; + let mut all_violations = Vec::new(); - + // Apply pattern matching - let matches = self.pattern_engine.analyze_file(file_path, &content) - .map_err(|e| GuardianError::analysis( - file_path.display().to_string(), - format!("Pattern analysis failed: {}", e) - ))?; - + let matches = self + .pattern_engine + .analyze_file(file_path, &content) + .map_err(|e| { + GuardianError::analysis( + file_path.display().to_string(), + format!("Pattern analysis failed: {}", e), + ) + })?; + all_violations.extend(self.pattern_engine.matches_to_violations(matches)); - + // Apply Rust-specific analysis for .rs files if self.rust_analyzer.handles_file(file_path) { - let rust_violations = self.rust_analyzer.analyze(file_path, &content) - .map_err(|e| GuardianError::analysis( - file_path.display().to_string(), - format!("Rust analysis failed: {}", e) - ))?; + let rust_violations = self + .rust_analyzer + .analyze(file_path, &content) + .map_err(|e| { + GuardianError::analysis( + file_path.display().to_string(), + format!("Rust analysis failed: {}", e), + ) + })?; all_violations.extend(rust_violations); } - + Ok(all_violations) } - + /// Analyze multiple files and return a complete validation report - pub fn analyze_paths>(&self, paths: &[P], options: &AnalysisOptions) -> GuardianResult { + pub fn analyze_paths>( + &self, + paths: &[P], + options: &AnalysisOptions, + ) -> GuardianResult { let start_time = Instant::now(); let mut report = ValidationReport::new(); - + // Collect all files to analyze let mut files_to_analyze = Vec::new(); - + for path in paths { let path = path.as_ref(); - + if path.is_file() { files_to_analyze.push(path.to_path_buf()); } else if path.is_dir() { @@ -162,7 +178,7 @@ impl Analyzer { files_to_analyze.extend(discovered_files); } } - + // Apply additional exclusions if specified if !options.exclude_patterns.is_empty() { let mut temp_filter = self.path_filter.clone(); @@ -171,38 +187,42 @@ impl Analyzer { } files_to_analyze = temp_filter.filter_paths(&files_to_analyze)?; } - + // Limit number of files if requested if let Some(max_files) = options.max_files { files_to_analyze.truncate(max_files); } - + let total_files = files_to_analyze.len(); - + // Analyze files (parallel or sequential) let violations = if options.parallel && files_to_analyze.len() > 1 { self.analyze_files_parallel(&files_to_analyze, options)? } else { self.analyze_files_sequential(&files_to_analyze, options)? }; - + // Build final report for violation in violations { report.add_violation(violation); } - + report.set_files_analyzed(total_files); report.set_execution_time(start_time.elapsed().as_millis() as u64); report.set_config_fingerprint(self.config.fingerprint()); report.sort_violations(); - + Ok(report) } - + /// Analyze files sequentially - fn analyze_files_sequential(&self, files: &[PathBuf], options: &AnalysisOptions) -> GuardianResult> { + fn analyze_files_sequential( + &self, + files: &[PathBuf], + options: &AnalysisOptions, + ) -> GuardianResult> { let mut all_violations = Vec::new(); - + for file_path in files { match self.analyze_file(file_path) { Ok(violations) => { @@ -218,17 +238,22 @@ impl Analyzer { } } } - + Ok(all_violations) } - + /// Analyze files in parallel - fn analyze_files_parallel(&self, files: &[PathBuf], options: &AnalysisOptions) -> GuardianResult> { + fn analyze_files_parallel( + &self, + files: &[PathBuf], + options: &AnalysisOptions, + ) -> GuardianResult> { let violations = Arc::new(Mutex::new(Vec::new())); let errors = Arc::new(Mutex::new(Vec::new())); - - files.par_iter().for_each(|file_path| { - match self.analyze_file(file_path) { + + files + .par_iter() + .for_each(|file_path| match self.analyze_file(file_path) { Ok(file_violations) => { if let Ok(mut v) = violations.lock() { v.extend(file_violations); @@ -239,9 +264,8 @@ impl Analyzer { errs.push((file_path.clone(), e)); } } - } - }); - + }); + // Handle errors let errors = Arc::try_unwrap(errors).unwrap().into_inner().unwrap(); if !errors.is_empty() { @@ -249,7 +273,7 @@ impl Analyzer { let (file_path, error) = errors.into_iter().next().unwrap(); return Err(GuardianError::analysis( file_path.display().to_string(), - error.to_string() + error.to_string(), )); } else { // Log all errors @@ -258,29 +282,33 @@ impl Analyzer { } } } - + let violations = Arc::try_unwrap(violations).unwrap().into_inner().unwrap(); Ok(violations) } - + /// Analyze a directory tree and return a validation report - pub fn analyze_directory>(&self, root: P, options: &AnalysisOptions) -> GuardianResult { + pub fn analyze_directory>( + &self, + root: P, + options: &AnalysisOptions, + ) -> GuardianResult { self.analyze_paths(&[root.as_ref()], options) } - + /// Get configuration fingerprint for cache validation pub fn config_fingerprint(&self) -> String { self.config.fingerprint() } - + /// Get statistics about the configured patterns pub fn pattern_stats(&self) -> PatternStats { let mut stats = PatternStats::default(); - + for (_category_name, category) in &self.config.patterns { if category.enabled { stats.enabled_categories += 1; - + for rule in &category.rules { if rule.enabled { stats.enabled_rules += 1; @@ -299,7 +327,7 @@ impl Analyzer { stats.disabled_rules += category.rules.len(); } } - + stats } } @@ -321,7 +349,7 @@ impl PatternStats { pub fn total_categories(&self) -> usize { self.enabled_categories + self.disabled_categories } - + pub fn total_rules(&self) -> usize { self.enabled_rules + self.disabled_rules } @@ -331,7 +359,7 @@ impl PatternStats { pub trait FileAnalyzer { /// Analyze a file and return violations fn analyze(&self, file_path: &Path, content: &str) -> GuardianResult>; - + /// Check if this analyzer handles the given file type fn handles_file(&self, file_path: &Path) -> bool; } @@ -339,96 +367,101 @@ pub trait FileAnalyzer { #[cfg(test)] mod tests { use super::*; - use tempfile::TempDir; use std::fs; - + use tempfile::TempDir; + #[test] fn test_analyzer_creation() { let analyzer = Analyzer::with_defaults().unwrap(); let stats = analyzer.pattern_stats(); - + // Should have default patterns loaded assert!(stats.enabled_rules > 0); assert!(stats.regex_patterns > 0); } - + #[test] fn test_single_file_analysis() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let file_path = temp_dir.path().join("test.rs"); - + fs::write(&file_path, "// TODO: implement this\nfn main() {}")?; - + let analyzer = Analyzer::with_defaults()?; let violations = analyzer.analyze_file(&file_path)?; - + // Should find the TODO comment assert!(!violations.is_empty()); assert!(violations.iter().any(|v| v.rule_id.contains("todo"))); - + Ok(()) } - + #[test] fn test_directory_analysis() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let root = temp_dir.path(); - + // Create directory structure fs::create_dir_all(root.join("src"))?; fs::create_dir_all(root.join("target/debug"))?; - + // Create test files fs::write(root.join("src/lib.rs"), "// TODO: implement\nfn test() {}")?; - fs::write(root.join("src/main.rs"), "fn main() { println!(\"Hello\"); }")?; + fs::write( + root.join("src/main.rs"), + "fn main() { println!(\"Hello\"); }", + )?; fs::write(root.join("target/debug/app"), "binary file")?; // Should be excluded - + let analyzer = Analyzer::with_defaults()?; let report = analyzer.analyze_directory(root, &AnalysisOptions::default())?; - + // Should analyze Rust files but not target directory assert!(report.summary.total_files > 0); assert!(report.has_violations()); - + // Should find the TODO - let todo_violations: Vec<_> = report.violations.iter() + let todo_violations: Vec<_> = report + .violations + .iter() .filter(|v| v.rule_id.contains("todo")) .collect(); assert!(!todo_violations.is_empty()); - + Ok(()) } - + #[test] fn test_analysis_options() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let root = temp_dir.path(); - + fs::create_dir_all(root.join("src"))?; fs::write(root.join("src/lib.rs"), "// TODO: test")?; fs::write(root.join("src/main.rs"), "// TODO: main")?; - + let analyzer = Analyzer::with_defaults()?; - + // Test max_files limitation let options = AnalysisOptions { max_files: Some(1), ..Default::default() }; - + let report = analyzer.analyze_directory(root, &options)?; assert_eq!(report.summary.total_files, 1); - + Ok(()) } - + #[test] fn test_pattern_stats() { let analyzer = Analyzer::with_defaults().unwrap(); let stats = analyzer.pattern_stats(); - + assert!(stats.total_rules() > 0); assert!(stats.total_categories() > 0); assert!(stats.enabled_rules > 0); } -} \ No newline at end of file +} diff --git a/src/analyzer/rust.rs b/src/analyzer/rust.rs index 1e8ce14..5e34459 100644 --- a/src/analyzer/rust.rs +++ b/src/analyzer/rust.rs @@ -1,16 +1,16 @@ //! Rust-specific code analysis using syn for AST parsing -//! +//! //! CDD Principle: Specialized Domain Services - Rust analyzer provides deep syntax understanding //! - Implements FileAnalyzer trait for clean polymorphism //! - Focuses on Rust-specific patterns like macro usage and function signatures //! - Translates syn AST structures to domain violation objects use crate::analyzer::FileAnalyzer; -use crate::domain::violations::{Violation, Severity, GuardianResult}; +use crate::domain::violations::{GuardianResult, Severity, Violation}; +use quote::ToTokens; use std::path::Path; -use syn::visit::Visit; use syn::spanned::Spanned; -use quote::ToTokens; +use syn::visit::Visit; /// Specialized analyzer for Rust source files #[derive(Debug, Default)] @@ -29,7 +29,7 @@ impl RustAnalyzer { check_cdd_compliance: true, } } - + /// Create a Rust analyzer that also analyzes test files pub fn with_tests() -> Self { Self { @@ -37,50 +37,55 @@ impl RustAnalyzer { check_cdd_compliance: true, } } - + /// Find all unimplemented macros in the file fn find_unimplemented_macros(&self, syntax_tree: &syn::File, content: &str) -> Vec { let mut visitor = UnimplementedMacroVisitor { violations: Vec::new(), should_skip_tests: !self.analyze_tests && self.is_test_file_content(content), }; - + visitor.visit_file(syntax_tree); visitor.violations } - + /// Find functions that return Ok(()) with minimal implementation - fn find_empty_ok_returns(&self, syntax_tree: &syn::File, content: &str, file_path: &Path) -> Vec { + fn find_empty_ok_returns( + &self, + syntax_tree: &syn::File, + content: &str, + file_path: &Path, + ) -> Vec { let mut visitor = EmptyOkReturnVisitor { violations: Vec::new(), file_path: file_path.to_path_buf(), should_skip_tests: !self.analyze_tests && self.is_test_file_content(content), }; - + visitor.visit_file(syntax_tree); visitor.violations } - + /// Check if content indicates this is a test file fn is_test_file_content(&self, content: &str) -> bool { - content.contains("#[cfg(test)]") || - content.contains("#[test]") || - content.contains("mod tests") + content.contains("#[cfg(test)]") + || content.contains("#[test]") + || content.contains("mod tests") } - + /// Check for CDD compliance (header comments) fn check_cdd_compliance(&self, content: &str, file_path: &Path) -> Vec { let mut violations = Vec::new(); - + if !self.check_cdd_compliance { return violations; } - + // Skip test files, examples, and benchmarks if self.is_excluded_from_cdd_check(file_path) { return violations; } - + // Look for CDD principle header if !content.contains("CDD Principle:") { violations.push( @@ -91,34 +96,45 @@ impl RustAnalyzer { "File missing CDD principle header comment", ) .with_position(1, 1) - .with_suggestion("Add a header comment explaining which CDD principle this file exemplifies") + .with_suggestion( + "Add a header comment explaining which CDD principle this file exemplifies", + ), ); } - + violations } - + /// Check if file should be excluded from CDD compliance checks fn is_excluded_from_cdd_check(&self, file_path: &Path) -> bool { let path_str = file_path.to_string_lossy(); - - path_str.contains("/tests/") || - path_str.contains("/test/") || - path_str.contains("/benches/") || - path_str.contains("/examples/") || - file_path.file_name() - .and_then(|name| name.to_str()) - .map(|name| name.starts_with("test_") || name.contains("test") || name == "lib.rs" && path_str.contains("/tests/")) - .unwrap_or(false) + + path_str.contains("/tests/") + || path_str.contains("/test/") + || path_str.contains("/benches/") + || path_str.contains("/examples/") + || file_path + .file_name() + .and_then(|name| name.to_str()) + .map(|name| { + name.starts_with("test_") + || name.contains("test") + || name == "lib.rs" && path_str.contains("/tests/") + }) + .unwrap_or(false) } - + /// Find potential architectural violations - fn find_architectural_violations(&self, syntax_tree: &syn::File, file_path: &Path) -> Vec { + fn find_architectural_violations( + &self, + syntax_tree: &syn::File, + file_path: &Path, + ) -> Vec { let mut visitor = ArchitecturalViolationVisitor { violations: Vec::new(), file_path: file_path.to_path_buf(), }; - + visitor.visit_file(syntax_tree); visitor.violations } @@ -127,7 +143,7 @@ impl RustAnalyzer { impl FileAnalyzer for RustAnalyzer { fn analyze(&self, file_path: &Path, content: &str) -> GuardianResult> { let mut violations = Vec::new(); - + // Parse the Rust syntax tree let syntax_tree = match syn::parse_file(content) { Ok(tree) => tree, @@ -137,18 +153,19 @@ impl FileAnalyzer for RustAnalyzer { return Ok(violations); } }; - + // Apply various Rust-specific analyses violations.extend(self.find_unimplemented_macros(&syntax_tree, content)); violations.extend(self.find_empty_ok_returns(&syntax_tree, content, file_path)); violations.extend(self.find_architectural_violations(&syntax_tree, file_path)); violations.extend(self.check_cdd_compliance(content, file_path)); - + Ok(violations) } - + fn handles_file(&self, file_path: &Path) -> bool { - file_path.extension() + file_path + .extension() .and_then(|ext| ext.to_str()) .map(|ext| ext == "rs") .unwrap_or(false) @@ -165,21 +182,23 @@ impl Visit<'_> for UnimplementedMacroVisitor { fn visit_macro(&mut self, mac: &syn::Macro) { if let Some(ident) = mac.path.get_ident() { let macro_name = ident.to_string(); - + // Check for placeholder macros if ["unimplemented", "todo", "panic"].contains(¯o_name.as_str()) { let severity = match macro_name.as_str() { "panic" => Severity::Warning, // panic! might be intentional _ => Severity::Error, }; - + let message = match macro_name.as_str() { - "unimplemented" => "Unimplemented macro found - function needs implementation".to_string(), + "unimplemented" => { + "Unimplemented macro found - function needs implementation".to_string() + } "todo" => "TODO macro found - incomplete implementation".to_string(), "panic" => format!("Panic macro found: {}", macro_name), _ => format!("Placeholder macro found: {}", macro_name), }; - + let violation = Violation::new( format!("{}_macro", macro_name), severity, @@ -188,20 +207,20 @@ impl Visit<'_> for UnimplementedMacroVisitor { ) .with_position(1, 1) .with_context(String::new()); - + self.violations.push(violation); } } - + syn::visit::visit_macro(self, mac); } - + fn visit_item_fn(&mut self, func: &syn::ItemFn) { // If we should skip tests, check if this is a test function if self.should_skip_tests && self.is_test_function(func) { return; // Skip visiting this function } - + syn::visit::visit_item_fn(self, func); } } @@ -209,8 +228,8 @@ impl Visit<'_> for UnimplementedMacroVisitor { impl UnimplementedMacroVisitor { fn is_test_function(&self, func: &syn::ItemFn) -> bool { func.attrs.iter().any(|attr| { - attr.path().is_ident("test") || - attr.path().to_token_stream().to_string().contains("test") + attr.path().is_ident("test") + || attr.path().to_token_stream().to_string().contains("test") }) } } @@ -228,7 +247,7 @@ impl Visit<'_> for EmptyOkReturnVisitor { if self.should_skip_tests && self.is_test_function(func) { return; } - + // Check if function returns Result type if let syn::ReturnType::Type(_, return_type) = &func.sig.output { if self.is_result_type(return_type) || self.is_option_type(return_type) { @@ -238,17 +257,20 @@ impl Visit<'_> for EmptyOkReturnVisitor { "empty_ok_return", Severity::Error, self.file_path.clone(), - format!("Function '{}' returns Ok(()) with no meaningful implementation", func.sig.ident), + format!( + "Function '{}' returns Ok(()) with no meaningful implementation", + func.sig.ident + ), ) .with_position(line, col) .with_context(context) .with_suggestion("Implement the function logic or remove if not needed"); - + self.violations.push(violation); } } } - + syn::visit::visit_item_fn(self, func); } } @@ -256,33 +278,35 @@ impl Visit<'_> for EmptyOkReturnVisitor { impl EmptyOkReturnVisitor { fn is_test_function(&self, func: &syn::ItemFn) -> bool { func.attrs.iter().any(|attr| { - attr.path().is_ident("test") || - attr.path().to_token_stream().to_string().contains("test") + attr.path().is_ident("test") + || attr.path().to_token_stream().to_string().contains("test") }) } - + fn is_result_type(&self, ty: &syn::Type) -> bool { match ty { - syn::Type::Path(type_path) => { - type_path.path.segments.last() - .map(|seg| seg.ident == "Result") - .unwrap_or(false) - } - _ => false + syn::Type::Path(type_path) => type_path + .path + .segments + .last() + .map(|seg| seg.ident == "Result") + .unwrap_or(false), + _ => false, } } - + fn is_option_type(&self, ty: &syn::Type) -> bool { match ty { - syn::Type::Path(type_path) => { - type_path.path.segments.last() - .map(|seg| seg.ident == "Option") - .unwrap_or(false) - } - _ => false + syn::Type::Path(type_path) => type_path + .path + .segments + .last() + .map(|seg| seg.ident == "Option") + .unwrap_or(false), + _ => false, } } - + fn find_trivial_ok_return(&self, block: &syn::Block) -> Option<(u32, u32, String)> { // Look for blocks with only Ok(()) return or similar trivial implementations if block.stmts.len() == 1 { @@ -292,16 +316,22 @@ impl EmptyOkReturnVisitor { } } } - + None } - + fn is_trivial_ok_expr(&self, expr: &syn::Expr) -> bool { match expr { syn::Expr::Call(call) => { // Check if it's Ok(...) with trivial arguments if let syn::Expr::Path(path) = &*call.func { - if path.path.segments.last().map(|seg| seg.ident == "Ok").unwrap_or(false) { + if path + .path + .segments + .last() + .map(|seg| seg.ident == "Ok") + .unwrap_or(false) + { // Ok() with no args is trivial if call.args.is_empty() { return true; @@ -327,13 +357,19 @@ impl EmptyOkReturnVisitor { } false } - + fn is_trivial_some_expr(&self, expr: &syn::Expr) -> bool { match expr { syn::Expr::Call(call) => { // Check if it's Some(...) with trivial arguments if let syn::Expr::Path(path) = &*call.func { - if path.path.segments.last().map(|seg| seg.ident == "Some").unwrap_or(false) { + if path + .path + .segments + .last() + .map(|seg| seg.ident == "Some") + .unwrap_or(false) + { // Some(()) with unit type is trivial if call.args.len() == 1 { if let syn::Expr::Tuple(tuple) = &call.args[0] { @@ -359,11 +395,11 @@ impl Visit<'_> for ArchitecturalViolationVisitor { fn visit_item_use(&mut self, use_item: &syn::ItemUse) { // Check for direct substrate access violations let use_tree_str = quote::quote!(#use_item).to_string(); - + if use_tree_str.contains("substrate") && !self.is_allowed_substrate_access() { let _span = use_item.span(); // Use a simple line-based location since proc_macro2::Span doesn't have start() method - + let violation = Violation::new( "direct_substrate_access", Severity::Warning, @@ -371,11 +407,13 @@ impl Visit<'_> for ArchitecturalViolationVisitor { "Direct substrate access may violate architectural boundaries", ) .with_position(1, 1) - .with_suggestion("Consider accessing substrate through designated services or repositories"); - + .with_suggestion( + "Consider accessing substrate through designated services or repositories", + ); + self.violations.push(violation); } - + syn::visit::visit_item_use(self, use_item); } } @@ -384,9 +422,8 @@ impl ArchitecturalViolationVisitor { fn is_allowed_substrate_access(&self) -> bool { // Allow substrate access in certain contexts let path_str = self.file_path.to_string_lossy(); - - path_str.contains("tests/") || - path_str.contains("examples/") + + path_str.contains("tests/") || path_str.contains("examples/") } } @@ -394,17 +431,17 @@ impl ArchitecturalViolationVisitor { mod tests { use super::*; // Test imports - + #[test] fn test_rust_analyzer_handles_rust_files() { let analyzer = RustAnalyzer::new(); - + assert!(analyzer.handles_file(Path::new("src/lib.rs"))); assert!(analyzer.handles_file(Path::new("main.rs"))); assert!(!analyzer.handles_file(Path::new("README.md"))); assert!(!analyzer.handles_file(Path::new("config.toml"))); } - + #[test] fn test_find_unimplemented_macros() { let analyzer = RustAnalyzer::new(); @@ -417,15 +454,17 @@ fn other() { todo!("implement this") } "#; - + let violations = analyzer.analyze(Path::new("test.rs"), content).unwrap(); - + // Should find both macros assert_eq!(violations.len(), 2); - assert!(violations.iter().any(|v| v.rule_id.contains("unimplemented"))); + assert!(violations + .iter() + .any(|v| v.rule_id.contains("unimplemented"))); assert!(violations.iter().any(|v| v.rule_id.contains("todo"))); } - + #[test] fn test_find_empty_ok_returns() { let analyzer = RustAnalyzer::new(); @@ -439,18 +478,19 @@ fn proper_function() -> Result<(), Box> { Ok(()) } "#; - + let violations = analyzer.analyze(Path::new("test.rs"), content).unwrap(); - + // Should find the empty function but not the proper one - let empty_violations: Vec<_> = violations.iter() + let empty_violations: Vec<_> = violations + .iter() .filter(|v| v.rule_id == "empty_ok_return") .collect(); - + assert_eq!(empty_violations.len(), 1); assert!(empty_violations[0].message.contains("empty_function")); } - + #[test] fn test_skip_test_files() { let analyzer = RustAnalyzer::new(); // analyze_tests = false @@ -464,26 +504,29 @@ fn test_something() { unimplemented!() // This should be ignored } "#; - + let violations = analyzer.analyze(Path::new("test.rs"), content).unwrap(); - + // Should find the macro in regular function but not in test - let macro_violations: Vec<_> = violations.iter() + let macro_violations: Vec<_> = violations + .iter() .filter(|v| v.rule_id.contains("unimplemented")) .collect(); - + assert_eq!(macro_violations.len(), 1); } - + #[test] fn test_cdd_compliance_check() { let analyzer = RustAnalyzer::new(); - + // File without CDD header let content_without_header = "fn main() {}"; - let violations = analyzer.analyze(Path::new("src/main.rs"), content_without_header).unwrap(); + let violations = analyzer + .analyze(Path::new("src/main.rs"), content_without_header) + .unwrap(); assert!(violations.iter().any(|v| v.rule_id == "cdd_header_missing")); - + // File with CDD header let content_with_header = r#" //! This module does something @@ -494,17 +537,21 @@ fn test_something() { fn main() {} "#; - let violations = analyzer.analyze(Path::new("src/main.rs"), content_with_header).unwrap(); + let violations = analyzer + .analyze(Path::new("src/main.rs"), content_with_header) + .unwrap(); assert!(!violations.iter().any(|v| v.rule_id == "cdd_header_missing")); } - + #[test] fn test_invalid_rust_syntax() { let analyzer = RustAnalyzer::new(); let invalid_content = "this is not valid rust syntax {{{"; - + // Should not panic and return empty violations - let violations = analyzer.analyze(Path::new("invalid.rs"), invalid_content).unwrap(); + let violations = analyzer + .analyze(Path::new("invalid.rs"), invalid_content) + .unwrap(); assert!(violations.is_empty()); } -} \ No newline at end of file +} diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 5c154d3..4583a8a 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,5 +1,5 @@ //! File hash caching for incremental checks -//! +//! //! CDD Principle: Infrastructure Layer - Cache provides performance optimization without affecting domain logic //! - FileCache acts as a repository for file metadata and analysis results //! - Hash-based validation ensures cache coherence with minimal overhead @@ -7,12 +7,12 @@ use crate::domain::violations::{GuardianError, GuardianResult}; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::collections::HashMap; -use std::path::{Path, PathBuf}; use std::fs::{self, File}; use std::io::prelude::*; +use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; -use sha2::{Sha256, Digest}; /// Cache for storing file analysis results and metadata #[derive(Debug)] @@ -77,16 +77,16 @@ impl FileCache { dirty: false, } } - + /// Load cache from disk, creating it if it doesn't exist pub fn load(&mut self) -> GuardianResult<()> { if self.cache_path.exists() { let content = fs::read_to_string(&self.cache_path) .map_err(|e| GuardianError::cache(format!("Failed to read cache file: {}", e)))?; - + self.data = serde_json::from_str(&content) .map_err(|e| GuardianError::cache(format!("Failed to parse cache file: {}", e)))?; - + // Migrate cache format if needed self.migrate_if_needed()?; } else { @@ -104,51 +104,62 @@ impl FileCache { }; self.dirty = true; } - + Ok(()) } - + /// Save cache to disk if it has been modified pub fn save(&mut self) -> GuardianResult<()> { if !self.dirty { return Ok(()); } - + // Update metadata self.data.metadata.updated_at = current_timestamp(); - + // Ensure cache directory exists if let Some(parent) = self.cache_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| GuardianError::cache(format!("Failed to create cache directory: {}", e)))?; + fs::create_dir_all(parent).map_err(|e| { + GuardianError::cache(format!("Failed to create cache directory: {}", e)) + })?; } - + // Serialize and write cache let content = serde_json::to_string_pretty(&self.data) .map_err(|e| GuardianError::cache(format!("Failed to serialize cache: {}", e)))?; - + fs::write(&self.cache_path, content) .map_err(|e| GuardianError::cache(format!("Failed to write cache file: {}", e)))?; - + self.dirty = false; Ok(()) } - + /// Check if a file needs to be re-analyzed - pub fn needs_analysis>(&mut self, file_path: P, config_fingerprint: &str) -> GuardianResult { + pub fn needs_analysis>( + &mut self, + file_path: P, + config_fingerprint: &str, + ) -> GuardianResult { let file_path = file_path.as_ref(); - + // Get current file metadata - let metadata = fs::metadata(file_path) - .map_err(|e| GuardianError::cache(format!("Failed to get file metadata for {}: {}", file_path.display(), e)))?; - + let metadata = fs::metadata(file_path).map_err(|e| { + GuardianError::cache(format!( + "Failed to get file metadata for {}: {}", + file_path.display(), + e + )) + })?; + let current_size = metadata.len(); - let current_modified = metadata.modified() + let current_modified = metadata + .modified() .map_err(|e| GuardianError::cache(format!("Failed to get modification time: {}", e)))? .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); - + // Check if we have a cache entry if let Some(entry) = self.data.files.get(file_path) { // Check if file has been modified @@ -157,14 +168,14 @@ impl FileCache { self.dirty = true; return Ok(true); } - + // Check if configuration has changed if entry.config_fingerprint != config_fingerprint { self.data.metadata.misses += 1; self.dirty = true; return Ok(true); } - + // Verify content hash to be absolutely sure let current_hash = self.calculate_file_hash(file_path)?; if entry.content_hash != current_hash { @@ -172,7 +183,7 @@ impl FileCache { self.dirty = true; return Ok(true); } - + // Cache hit! self.data.metadata.hits += 1; self.dirty = true; @@ -184,27 +195,30 @@ impl FileCache { Ok(true) } } - + /// Update cache entry for a file after analysis pub fn update_entry>( - &mut self, - file_path: P, + &mut self, + file_path: P, violation_count: usize, - config_fingerprint: &str + config_fingerprint: &str, ) -> GuardianResult<()> { let file_path = file_path.as_ref(); - + // Get current file metadata let metadata = fs::metadata(file_path) .map_err(|e| GuardianError::cache(format!("Failed to get file metadata: {}", e)))?; - + let content_hash = self.calculate_file_hash(file_path)?; - + let entry = FileEntry { content_hash, size: metadata.len(), - modified_at: metadata.modified() - .map_err(|e| GuardianError::cache(format!("Failed to get modification time: {}", e)))? + modified_at: metadata + .modified() + .map_err(|e| { + GuardianError::cache(format!("Failed to get modification time: {}", e)) + })? .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(), @@ -212,13 +226,13 @@ impl FileCache { analyzed_at: current_timestamp(), config_fingerprint: config_fingerprint.to_string(), }; - + self.data.files.insert(file_path.to_path_buf(), entry); self.dirty = true; - + Ok(()) } - + /// Get cache statistics pub fn statistics(&self) -> CacheStatistics { CacheStatistics { @@ -226,7 +240,8 @@ impl FileCache { cache_hits: self.data.metadata.hits, cache_misses: self.data.metadata.misses, hit_rate: if self.data.metadata.hits + self.data.metadata.misses > 0 { - (self.data.metadata.hits as f64) / ((self.data.metadata.hits + self.data.metadata.misses) as f64) + (self.data.metadata.hits as f64) + / ((self.data.metadata.hits + self.data.metadata.misses) as f64) } else { 0.0 }, @@ -234,7 +249,7 @@ impl FileCache { updated_at: self.data.metadata.updated_at, } } - + /// Clear the entire cache pub fn clear(&mut self) -> GuardianResult<()> { self.data.files.clear(); @@ -242,39 +257,39 @@ impl FileCache { self.data.metadata.misses = 0; self.data.metadata.updated_at = current_timestamp(); self.dirty = true; - + // Remove cache file if it exists if self.cache_path.exists() { fs::remove_file(&self.cache_path) .map_err(|e| GuardianError::cache(format!("Failed to remove cache file: {}", e)))?; } - + Ok(()) } - + /// Remove cache entries for files that no longer exist pub fn cleanup(&mut self) -> GuardianResult { let mut removed = 0; let mut to_remove = Vec::new(); - + for file_path in self.data.files.keys() { if !file_path.exists() { to_remove.push(file_path.clone()); } } - + for file_path in to_remove { self.data.files.remove(&file_path); removed += 1; } - + if removed > 0 { self.dirty = true; } - + Ok(removed) } - + /// Update configuration fingerprint pub fn set_config_fingerprint(&mut self, fingerprint: String) { if self.data.config_fingerprint.as_ref() != Some(&fingerprint) { @@ -282,36 +297,41 @@ impl FileCache { self.dirty = true; } } - + /// Calculate SHA-256 hash of file content fn calculate_file_hash>(&self, file_path: P) -> GuardianResult { let mut file = File::open(&file_path) .map_err(|e| GuardianError::cache(format!("Failed to open file for hashing: {}", e)))?; - + let mut hasher = Sha256::new(); let mut buffer = [0; 8192]; - + loop { - let bytes_read = file.read(&mut buffer) - .map_err(|e| GuardianError::cache(format!("Failed to read file for hashing: {}", e)))?; - + let bytes_read = file.read(&mut buffer).map_err(|e| { + GuardianError::cache(format!("Failed to read file for hashing: {}", e)) + })?; + if bytes_read == 0 { break; } - + hasher.update(&buffer[..bytes_read]); } - + Ok(format!("{:x}", hasher.finalize())) } - + /// Migrate cache format if needed fn migrate_if_needed(&mut self) -> GuardianResult<()> { const CURRENT_VERSION: u32 = 1; - + if self.data.version < CURRENT_VERSION { - tracing::info!("Migrating cache from version {} to {}", self.data.version, CURRENT_VERSION); - + tracing::info!( + "Migrating cache from version {} to {}", + self.data.version, + CURRENT_VERSION + ); + match self.data.version { 0 => { // Migration from version 0 to 1 @@ -327,7 +347,7 @@ impl FileCache { } } } - + Ok(()) } } @@ -379,83 +399,83 @@ fn current_timestamp() -> u64 { #[cfg(test)] mod tests { use super::*; - use tempfile::TempDir; use std::fs; - + use tempfile::TempDir; + #[test] fn test_cache_creation() { let temp_dir = TempDir::new().unwrap(); let cache_path = temp_dir.path().join("cache.json"); - + let mut cache = FileCache::new(&cache_path); cache.load().unwrap(); - + // Should create new cache assert_eq!(cache.data.version, 1); assert_eq!(cache.data.files.len(), 0); } - + #[test] fn test_needs_analysis() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let cache_path = temp_dir.path().join("cache.json"); let test_file = temp_dir.path().join("test.rs"); - + // Create test file fs::write(&test_file, "fn main() {}")?; - + let mut cache = FileCache::new(&cache_path); cache.load()?; - + // First check should need analysis assert!(cache.needs_analysis(&test_file, "config123")?); - + // Update cache entry cache.update_entry(&test_file, 0, "config123")?; - + // Second check should not need analysis assert!(!cache.needs_analysis(&test_file, "config123")?); - + // Change config should need analysis assert!(cache.needs_analysis(&test_file, "config456")?); - + Ok(()) } - + #[test] fn test_file_modification_detection() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let cache_path = temp_dir.path().join("cache.json"); let test_file = temp_dir.path().join("test.rs"); - + // Create test file fs::write(&test_file, "fn main() {}")?; - + let mut cache = FileCache::new(&cache_path); cache.load()?; - + // Add to cache cache.update_entry(&test_file, 0, "config123")?; assert!(!cache.needs_analysis(&test_file, "config123")?); - + // Modify file std::thread::sleep(std::time::Duration::from_millis(10)); // Ensure different timestamp fs::write(&test_file, "fn main() { println!(\"Hello\"); }")?; - + // Should detect modification assert!(cache.needs_analysis(&test_file, "config123")?); - + Ok(()) } - + #[test] fn test_cache_persistence() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let cache_path = temp_dir.path().join("cache.json"); let test_file = temp_dir.path().join("test.rs"); - + fs::write(&test_file, "fn main() {}")?; - + // Create cache and add entry { let mut cache = FileCache::new(&cache_path); @@ -463,71 +483,71 @@ mod tests { cache.update_entry(&test_file, 2, "config123")?; cache.save()?; } - + // Load cache in new instance { let mut cache = FileCache::new(&cache_path); cache.load()?; - + // Should have cached entry assert!(!cache.needs_analysis(&test_file, "config123")?); assert_eq!(cache.data.files.len(), 1); - + let stats = cache.statistics(); assert_eq!(stats.total_files, 1); } - + Ok(()) } - + #[test] fn test_cache_cleanup() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let cache_path = temp_dir.path().join("cache.json"); let test_file1 = temp_dir.path().join("test1.rs"); let test_file2 = temp_dir.path().join("test2.rs"); - + // Create test files fs::write(&test_file1, "fn main() {}")?; fs::write(&test_file2, "fn other() {}")?; - + let mut cache = FileCache::new(&cache_path); cache.load()?; - + // Add both files to cache cache.update_entry(&test_file1, 0, "config123")?; cache.update_entry(&test_file2, 1, "config123")?; assert_eq!(cache.data.files.len(), 2); - + // Remove one file fs::remove_file(&test_file1)?; - + // Cleanup should remove the deleted file let removed = cache.cleanup()?; assert_eq!(removed, 1); assert_eq!(cache.data.files.len(), 1); - + Ok(()) } - + #[test] fn test_statistics() { let temp_dir = TempDir::new().unwrap(); let cache_path = temp_dir.path().join("cache.json"); - + let mut cache = FileCache::new(&cache_path); cache.load().unwrap(); - + // Simulate cache operations cache.data.metadata.hits = 10; cache.data.metadata.misses = 5; - + let stats = cache.statistics(); assert_eq!(stats.cache_hits, 10); assert_eq!(stats.cache_misses, 5); assert_eq!(stats.hit_rate, 10.0 / 15.0); - + let display = stats.format_display(); assert!(display.contains("66.7% hit rate")); } -} \ No newline at end of file +} diff --git a/src/config/mod.rs b/src/config/mod.rs index b674b65..ec4760a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,5 +1,5 @@ //! Configuration loading and management for Rust Guardian -//! +//! //! CDD Principle: Anti-Corruption Layer - Configuration translates external YAML formats //! - Raw YAML structures are converted to clean domain objects //! - Default configurations are embedded in the domain, not infrastructure @@ -8,8 +8,8 @@ use crate::domain::violations::{GuardianError, GuardianResult, Severity}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::path::Path; use std::fs; +use std::path::Path; /// Main configuration structure for Rust Guardian #[derive(Debug, Clone, Serialize, Deserialize)] @@ -95,27 +95,35 @@ pub struct ExcludeConditions { impl GuardianConfig { /// Load configuration from a YAML file pub fn load_from_file>(path: P) -> GuardianResult { - let contents = fs::read_to_string(&path) - .map_err(|e| GuardianError::config(format!("Failed to read config file '{}': {}", - path.as_ref().display(), e)))?; - - let config: Self = serde_yaml::from_str(&contents) - .map_err(|e| GuardianError::config(format!("Failed to parse config file '{}': {}", - path.as_ref().display(), e)))?; - + let contents = fs::read_to_string(&path).map_err(|e| { + GuardianError::config(format!( + "Failed to read config file '{}': {}", + path.as_ref().display(), + e + )) + })?; + + let config: Self = serde_yaml::from_str(&contents).map_err(|e| { + GuardianError::config(format!( + "Failed to parse config file '{}': {}", + path.as_ref().display(), + e + )) + })?; + config.validate()?; Ok(config) } - + /// Load configuration from string content pub fn load_from_str(content: &str) -> GuardianResult { let config: Self = serde_yaml::from_str(content) .map_err(|e| GuardianError::config(format!("Failed to parse config: {}", e)))?; - + config.validate()?; Ok(config) } - + /// Get default configuration with built-in patterns pub fn default() -> Self { Self { @@ -133,63 +141,68 @@ impl GuardianConfig { patterns: Self::default_patterns(), } } - + /// Get default pattern definitions fn default_patterns() -> HashMap { let mut patterns = HashMap::new(); - + // Placeholder detection patterns - patterns.insert("placeholders".to_string(), PatternCategory { - severity: Severity::Error, - enabled: true, - rules: vec![ - PatternRule { - id: "todo_comments".to_string(), - rule_type: RuleType::Regex, - pattern: r"\b(TODO|FIXME|HACK|XXX|BUG|REFACTOR)\b".to_string(), - message: "Placeholder comment detected: {match}".to_string(), - severity: None, - enabled: true, - case_sensitive: false, - exclude_if: None, - }, - PatternRule { - id: "temporary_markers".to_string(), - rule_type: RuleType::Regex, - pattern: r"(?i)\b(for now|temporary|placeholder|stub|dummy|fake)\b".to_string(), - message: "Temporary implementation marker found: {match}".to_string(), - severity: None, - enabled: true, - case_sensitive: false, - exclude_if: Some(ExcludeConditions { - attribute: None, - in_tests: true, - file_patterns: Some(vec!["**/tests/**".to_string()]), - }), - }, - PatternRule { - id: "unimplemented_macros".to_string(), - rule_type: RuleType::Ast, - pattern: "macro_call:unimplemented|todo|panic".to_string(), - message: "Unfinished macro {macro_name}! found".to_string(), - severity: None, - enabled: true, - case_sensitive: true, - exclude_if: Some(ExcludeConditions { - attribute: Some("#[test]".to_string()), - in_tests: true, - file_patterns: None, - }), - }, - ], - }); - + patterns.insert( + "placeholders".to_string(), + PatternCategory { + severity: Severity::Error, + enabled: true, + rules: vec![ + PatternRule { + id: "todo_comments".to_string(), + rule_type: RuleType::Regex, + pattern: r"\b(TODO|FIXME|HACK|XXX|BUG|REFACTOR)\b".to_string(), + message: "Placeholder comment detected: {match}".to_string(), + severity: None, + enabled: true, + case_sensitive: false, + exclude_if: None, + }, + PatternRule { + id: "temporary_markers".to_string(), + rule_type: RuleType::Regex, + pattern: r"(?i)\b(for now|temporary|placeholder|stub|dummy|fake)\b" + .to_string(), + message: "Temporary implementation marker found: {match}".to_string(), + severity: None, + enabled: true, + case_sensitive: false, + exclude_if: Some(ExcludeConditions { + attribute: None, + in_tests: true, + file_patterns: Some(vec!["**/tests/**".to_string()]), + }), + }, + PatternRule { + id: "unimplemented_macros".to_string(), + rule_type: RuleType::Ast, + pattern: "macro_call:unimplemented|todo|panic".to_string(), + message: "Unfinished macro {macro_name}! found".to_string(), + severity: None, + enabled: true, + case_sensitive: true, + exclude_if: Some(ExcludeConditions { + attribute: Some("#[test]".to_string()), + in_tests: true, + file_patterns: None, + }), + }, + ], + }, + ); + // Incomplete implementations - patterns.insert("incomplete_implementations".to_string(), PatternCategory { - severity: Severity::Error, - enabled: true, - rules: vec![ - PatternRule { + patterns.insert( + "incomplete_implementations".to_string(), + PatternCategory { + severity: Severity::Error, + enabled: true, + rules: vec![PatternRule { id: "empty_ok_return".to_string(), rule_type: RuleType::Ast, pattern: "return_ok_unit_with_no_logic".to_string(), @@ -202,77 +215,81 @@ impl GuardianConfig { in_tests: true, file_patterns: None, }), - }, - ], - }); - + }], + }, + ); + // Architectural violations - patterns.insert("architectural_violations".to_string(), PatternCategory { - severity: Severity::Warning, - enabled: true, - rules: vec![ - PatternRule { - id: "hardcoded_paths".to_string(), - rule_type: RuleType::Regex, - pattern: r#"["\'](\./|/|\.\./)?(\.rust/)[^"']*["\']"#.to_string(), - message: "Hardcoded path found - use configuration instead".to_string(), - severity: None, - enabled: true, - case_sensitive: true, - exclude_if: Some(ExcludeConditions { - attribute: None, - in_tests: true, - file_patterns: Some(vec!["**/tests/**".to_string(), "**/examples/**".to_string()]), - }), - }, - PatternRule { - id: "cdd_header_missing".to_string(), - rule_type: RuleType::Regex, - pattern: r"//!\s*(?:.*\n)*?\s*//!\s*CDD Principle:".to_string(), - message: "File missing CDD principle header".to_string(), - severity: Some(Severity::Info), - enabled: false, // Disabled by default, can be enabled per project - case_sensitive: false, - exclude_if: Some(ExcludeConditions { - attribute: None, - in_tests: true, - file_patterns: Some(vec![ - "**/tests/**".to_string(), - "**/benches/**".to_string(), - "**/examples/**".to_string(), - ]), - }), - }, - ], - }); - + patterns.insert( + "architectural_violations".to_string(), + PatternCategory { + severity: Severity::Warning, + enabled: true, + rules: vec![ + PatternRule { + id: "hardcoded_paths".to_string(), + rule_type: RuleType::Regex, + pattern: r#"["\'](\./|/|\.\./)?(\.rust/)[^"']*["\']"#.to_string(), + message: "Hardcoded path found - use configuration instead".to_string(), + severity: None, + enabled: true, + case_sensitive: true, + exclude_if: Some(ExcludeConditions { + attribute: None, + in_tests: true, + file_patterns: Some(vec![ + "**/tests/**".to_string(), + "**/examples/**".to_string(), + ]), + }), + }, + PatternRule { + id: "cdd_header_missing".to_string(), + rule_type: RuleType::Regex, + pattern: r"//!\s*(?:.*\n)*?\s*//!\s*CDD Principle:".to_string(), + message: "File missing CDD principle header".to_string(), + severity: Some(Severity::Info), + enabled: false, // Disabled by default, can be enabled per project + case_sensitive: false, + exclude_if: Some(ExcludeConditions { + attribute: None, + in_tests: true, + file_patterns: Some(vec![ + "**/tests/**".to_string(), + "**/benches/**".to_string(), + "**/examples/**".to_string(), + ]), + }), + }, + ], + }, + ); + patterns } - + /// Validate the configuration for consistency and correctness pub fn validate(&self) -> GuardianResult<()> { // Check version compatibility if !["1.0"].contains(&self.version.as_str()) { return Err(GuardianError::config(format!( - "Unsupported configuration version: {}. Supported versions: 1.0", + "Unsupported configuration version: {}. Supported versions: 1.0", self.version ))); } - + // Validate patterns for (category_name, category) in &self.patterns { for rule in &category.rules { // Validate rule IDs are unique within category - let duplicate_count = category.rules.iter() - .filter(|r| r.id == rule.id) - .count(); + let duplicate_count = category.rules.iter().filter(|r| r.id == rule.id).count(); if duplicate_count > 1 { return Err(GuardianError::config(format!( - "Duplicate rule ID '{}' in category '{}'", + "Duplicate rule ID '{}' in category '{}'", rule.id, category_name ))); } - + // Validate regex patterns can compile if matches!(rule.rule_type, RuleType::Regex) { if rule.case_sensitive { @@ -281,51 +298,57 @@ impl GuardianConfig { regex::RegexBuilder::new(&rule.pattern) .case_insensitive(true) .build() - }.map_err(|e| GuardianError::config(format!( - "Invalid regex pattern in rule '{}': {}", - rule.id, e - )))?; + } + .map_err(|e| { + GuardianError::config(format!( + "Invalid regex pattern in rule '{}': {}", + rule.id, e + )) + })?; } } } - + Ok(()) } - + /// Get all enabled rules across all categories pub fn enabled_rules(&self) -> impl Iterator { - self.patterns.iter() + self.patterns + .iter() .filter(|(_, category)| category.enabled) .flat_map(|(name, category)| { - category.rules.iter() + category + .rules + .iter() .filter(|rule| rule.enabled) .map(move |rule| (name, category, rule)) }) } - + /// Get effective severity for a rule (rule override or category default) pub fn effective_severity(&self, category: &PatternCategory, rule: &PatternRule) -> Severity { rule.severity.unwrap_or(category.severity) } - + /// Convert to JSON for serialization pub fn to_json(&self) -> GuardianResult { serde_json::to_string_pretty(self) .map_err(|e| GuardianError::config(format!("Failed to serialize config: {}", e))) } - + /// Create a fingerprint of the configuration for cache validation pub fn fingerprint(&self) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; - + let mut hasher = DefaultHasher::new(); - + // Create a stable representation for hashing // Sort patterns to ensure consistent ordering let mut sorted_patterns: Vec<_> = self.patterns.iter().collect(); sorted_patterns.sort_by_key(|(name, _)| name.as_str()); - + // Hash version and path config self.version.hash(&mut hasher); self.paths.patterns.len().hash(&mut hasher); @@ -333,17 +356,17 @@ impl GuardianConfig { pattern.hash(&mut hasher); } self.paths.ignore_file.hash(&mut hasher); - + // Hash patterns in sorted order for (category_name, category) in sorted_patterns { category_name.hash(&mut hasher); category.severity.hash(&mut hasher); category.enabled.hash(&mut hasher); - + // Sort rules for consistent ordering let mut sorted_rules = category.rules.clone(); sorted_rules.sort_by_key(|rule| rule.id.clone()); - + for rule in sorted_rules { rule.id.hash(&mut hasher); rule.pattern.hash(&mut hasher); @@ -352,7 +375,7 @@ impl GuardianConfig { rule.case_sensitive.hash(&mut hasher); } } - + format!("{:x}", hasher.finish()) } } @@ -379,25 +402,25 @@ impl ConfigBuilder { config: GuardianConfig::default(), } } - + /// Add a path pattern pub fn add_path_pattern(mut self, pattern: impl Into) -> Self { self.config.paths.patterns.push(pattern.into()); self } - + /// Set the ignore file name pub fn ignore_file(mut self, filename: impl Into) -> Self { self.config.paths.ignore_file = Some(filename.into()); self } - + /// Add a pattern category pub fn add_category(mut self, name: impl Into, category: PatternCategory) -> Self { self.config.patterns.insert(name.into(), category); self } - + /// Build the final configuration pub fn build(self) -> GuardianResult { self.config.validate()?; @@ -414,7 +437,7 @@ impl Default for ConfigBuilder { #[cfg(test)] mod tests { use super::*; - + #[test] fn test_default_config() { let config = GuardianConfig::default(); @@ -422,38 +445,38 @@ mod tests { assert!(config.patterns.contains_key("placeholders")); assert!(config.validate().is_ok()); } - + #[test] fn test_config_validation() { let mut config = GuardianConfig::default(); config.version = "999.0".to_string(); assert!(config.validate().is_err()); } - + #[test] fn test_enabled_rules() { let config = GuardianConfig::default(); let rules: Vec<_> = config.enabled_rules().collect(); assert!(!rules.is_empty()); - + // All returned rules should be enabled for (_, category, rule) in rules { assert!(category.enabled); assert!(rule.enabled); } } - + #[test] fn test_config_fingerprint() { let config1 = GuardianConfig::default(); let config2 = GuardianConfig::default(); assert_eq!(config1.fingerprint(), config2.fingerprint()); - + let mut config3 = GuardianConfig::default(); config3.paths.patterns.push("new_pattern".to_string()); assert_ne!(config1.fingerprint(), config3.fingerprint()); } - + #[test] fn test_config_builder() { let config = ConfigBuilder::new() @@ -461,11 +484,14 @@ mod tests { .ignore_file(".customignore") .build() .unwrap(); - - assert!(config.paths.patterns.contains(&"custom/path/**".to_string())); + + assert!(config + .paths + .patterns + .contains(&"custom/path/**".to_string())); assert_eq!(config.paths.ignore_file, Some(".customignore".to_string())); } - + #[test] fn test_yaml_serialization() { let config = GuardianConfig::default(); @@ -473,4 +499,4 @@ mod tests { let loaded = serde_yaml::from_str::(&yaml).unwrap(); assert_eq!(config.version, loaded.version); } -} \ No newline at end of file +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index edb72cf..cfd1068 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -1,5 +1,5 @@ //! Domain layer for Rust Guardian -//! +//! //! CDD Principle: Domain Model - Pure business logic for code quality enforcement //! - Contains all core entities, value objects, and domain services //! - Independent of infrastructure concerns like databases, file systems, or external APIs @@ -8,4 +8,4 @@ pub mod violations; // Re-export main domain types for convenience -pub use violations::*; \ No newline at end of file +pub use violations::*; diff --git a/src/domain/violations.rs b/src/domain/violations.rs index 30a5590..1f461b1 100644 --- a/src/domain/violations.rs +++ b/src/domain/violations.rs @@ -1,13 +1,13 @@ //! Core domain models for code quality violations and validation results -//! +//! //! CDD Principle: Rich Domain Models - Violations are entities with behavior, not just data //! - Violations can classify themselves, suggest fixes, and maintain context //! - ValidationReport acts as an aggregate root managing collections of violations //! - Domain events can be generated when patterns are detected or when validation completes +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use chrono::{DateTime, Utc}; /// Severity levels for code quality violations #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] @@ -26,12 +26,12 @@ impl Severity { pub fn is_blocking(self) -> bool { matches!(self, Self::Error) } - + /// Convert to string for display pub fn as_str(self) -> &'static str { match self { Self::Info => "info", - Self::Warning => "warning", + Self::Warning => "warning", Self::Error => "error", } } @@ -80,31 +80,31 @@ impl Violation { detected_at: Utc::now(), } } - + /// Set line and column position pub fn with_position(mut self, line: u32, column: u32) -> Self { self.line_number = Some(line); self.column_number = Some(column); self } - + /// Add source code context pub fn with_context(mut self, context: impl Into) -> Self { self.context = Some(context.into()); self } - + /// Add a suggested fix pub fn with_suggestion(mut self, suggestion: impl Into) -> Self { self.suggested_fix = Some(suggestion.into()); self } - + /// Whether this violation is blocking (prevents commits/builds) pub fn is_blocking(&self) -> bool { self.severity.is_blocking() } - + /// Format violation for display pub fn format_display(&self) -> String { let location = match (self.line_number, self.column_number) { @@ -112,7 +112,7 @@ impl Violation { (Some(line), None) => format!(":{}", line), _ => String::new(), }; - + format!( "{}{} [{}] {}", self.file_path.display(), @@ -149,12 +149,12 @@ impl ViolationCounts { pub fn total(&self) -> usize { self.error + self.warning + self.info } - + /// Whether there are any blocking violations pub fn has_blocking(&self) -> bool { self.error > 0 } - + /// Add a violation to the counts pub fn add(&mut self, severity: Severity) { match severity { @@ -188,43 +188,45 @@ impl ValidationReport { config_fingerprint: None, } } - + /// Add a violation to the report pub fn add_violation(&mut self, violation: Violation) { self.summary.violations_by_severity.add(violation.severity); self.violations.push(violation); } - + /// Whether the report contains any violations pub fn has_violations(&self) -> bool { !self.violations.is_empty() } - + /// Whether the report contains blocking violations (errors) pub fn has_errors(&self) -> bool { self.summary.violations_by_severity.has_blocking() } - + /// Get violations of a specific severity pub fn violations_by_severity(&self, severity: Severity) -> impl Iterator { - self.violations.iter().filter(move |v| v.severity == severity) + self.violations + .iter() + .filter(move |v| v.severity == severity) } - + /// Set the number of files analyzed pub fn set_files_analyzed(&mut self, count: usize) { self.summary.total_files = count; } - + /// Set the execution time pub fn set_execution_time(&mut self, duration_ms: u64) { self.summary.execution_time_ms = duration_ms; } - + /// Set the configuration fingerprint pub fn set_config_fingerprint(&mut self, fingerprint: impl Into) { self.config_fingerprint = Some(fingerprint.into()); } - + /// Merge another report into this one pub fn merge(&mut self, other: ValidationReport) { for violation in other.violations { @@ -232,11 +234,12 @@ impl ValidationReport { } self.summary.total_files += other.summary.total_files; } - + /// Sort violations by file path and line number for consistent output pub fn sort_violations(&mut self) { self.violations.sort_by(|a, b| { - a.file_path.cmp(&b.file_path) + a.file_path + .cmp(&b.file_path) .then_with(|| a.line_number.unwrap_or(0).cmp(&b.line_number.unwrap_or(0))) .then_with(|| a.severity.cmp(&b.severity)) }); @@ -255,22 +258,22 @@ pub enum GuardianError { /// Configuration file could not be loaded or parsed #[error("Configuration error: {message}")] Configuration { message: String }, - + /// File could not be read or accessed #[error("IO error: {source}")] Io { #[from] source: std::io::Error, }, - + /// Pattern compilation failed #[error("Pattern error: {message}")] Pattern { message: String }, - + /// Analysis failed for a specific file #[error("Analysis error in {file}: {message}")] Analysis { file: String, message: String }, - + /// Cache operation failed #[error("Cache error: {message}")] Cache { message: String }, @@ -279,25 +282,31 @@ pub enum GuardianError { impl GuardianError { /// Create a configuration error pub fn config(message: impl Into) -> Self { - Self::Configuration { message: message.into() } + Self::Configuration { + message: message.into(), + } } - + /// Create a pattern error pub fn pattern(message: impl Into) -> Self { - Self::Pattern { message: message.into() } + Self::Pattern { + message: message.into(), + } } - + /// Create an analysis error pub fn analysis(file: impl Into, message: impl Into) -> Self { - Self::Analysis { - file: file.into(), - message: message.into() + Self::Analysis { + file: file.into(), + message: message.into(), } } - + /// Create a cache error pub fn cache(message: impl Into) -> Self { - Self::Cache { message: message.into() } + Self::Cache { + message: message.into(), + } } } @@ -308,7 +317,7 @@ pub type GuardianResult = Result; mod tests { use super::*; use std::path::Path; - + #[test] fn test_violation_creation() { let violation = Violation::new( @@ -317,14 +326,14 @@ mod tests { PathBuf::from("src/lib.rs"), "Test message", ); - + assert_eq!(violation.rule_id, "test_rule"); assert_eq!(violation.severity, Severity::Error); assert_eq!(violation.file_path, Path::new("src/lib.rs")); assert_eq!(violation.message, "Test message"); assert!(violation.is_blocking()); } - + #[test] fn test_violation_with_position() { let violation = Violation::new( @@ -335,38 +344,38 @@ mod tests { ) .with_position(42, 15) .with_context("let x = todo!();"); - + assert_eq!(violation.line_number, Some(42)); assert_eq!(violation.column_number, Some(15)); assert_eq!(violation.context, Some("let x = todo!();".to_string())); assert!(!violation.is_blocking()); } - + #[test] fn test_validation_report() { let mut report = ValidationReport::new(); - + report.add_violation(Violation::new( "rule1", Severity::Error, PathBuf::from("src/main.rs"), "Error message", )); - + report.add_violation(Violation::new( - "rule2", + "rule2", Severity::Warning, PathBuf::from("src/lib.rs"), "Warning message", )); - + assert!(report.has_violations()); assert!(report.has_errors()); assert_eq!(report.summary.violations_by_severity.total(), 2); assert_eq!(report.summary.violations_by_severity.error, 1); assert_eq!(report.summary.violations_by_severity.warning, 1); } - + #[test] fn test_severity_ordering() { assert!(Severity::Error > Severity::Warning); @@ -374,4 +383,4 @@ mod tests { assert!(Severity::Error.is_blocking()); assert!(!Severity::Warning.is_blocking()); } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 1875cd4..2e000aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,38 +1,29 @@ //! Rust Guardian - Dynamic code quality enforcement for systems -//! +//! //! CDD Principle: Clean Architecture - Library interface serves as the application layer //! - Pure domain logic separated from infrastructure concerns //! - Clean boundaries between core business logic and external dependencies //! - Agent integration API provides validation workflows -pub mod domain; +pub mod analyzer; +pub mod cache; pub mod config; +pub mod domain; pub mod patterns; -pub mod analyzer; pub mod report; -pub mod cache; // Re-export main types for convenient access pub use domain::violations::{ - Violation, Severity, ValidationReport, ValidationSummary, - GuardianError, GuardianResult + GuardianError, GuardianResult, Severity, ValidationReport, ValidationSummary, Violation, }; -pub use config::{ - GuardianConfig, PatternRule, RuleType, PatternCategory -}; +pub use config::{GuardianConfig, PatternCategory, PatternRule, RuleType}; -pub use analyzer::{ - Analyzer, AnalysisOptions, PatternStats -}; +pub use analyzer::{AnalysisOptions, Analyzer, PatternStats}; -pub use report::{ - ReportFormatter, OutputFormat, ReportOptions -}; +pub use report::{OutputFormat, ReportFormatter, ReportOptions}; -pub use cache::{ - FileCache, CacheStatistics -}; +pub use cache::{CacheStatistics, FileCache}; use std::path::{Path, PathBuf}; @@ -78,25 +69,25 @@ impl GuardianValidator { pub fn new_with_config(config: GuardianConfig) -> GuardianResult { let analyzer = Analyzer::new(config)?; let report_formatter = ReportFormatter::default(); - + Ok(Self { analyzer, cache: None, report_formatter, }) } - + /// Create a validator with default configuration pub fn new() -> GuardianResult { Self::new_with_config(GuardianConfig::default()) } - + /// Create a validator loading configuration from file pub fn from_config_file>(path: P) -> GuardianResult { let config = GuardianConfig::load_from_file(path)?; Self::new_with_config(config) } - + /// Enable caching with the specified cache file pub fn with_cache>(mut self, cache_path: P) -> GuardianResult { let mut cache = FileCache::new(cache_path); @@ -105,84 +96,86 @@ impl GuardianValidator { self.cache = Some(cache); Ok(self) } - + /// Set custom report formatter pub fn with_report_formatter(mut self, formatter: ReportFormatter) -> Self { self.report_formatter = formatter; self } - + /// Validate files for agent workflows - primary API for autonomous agents pub async fn validate_for_agent>( - &mut self, - paths: Vec

+ &mut self, + paths: Vec

, ) -> GuardianResult { - self.validate_with_options(paths, &ValidationOptions::default()).await + self.validate_with_options(paths, &ValidationOptions::default()) + .await } - + /// Validate files with custom options pub async fn validate_with_options>( - &mut self, + &mut self, paths: Vec

, - options: &ValidationOptions + options: &ValidationOptions, ) -> GuardianResult { // Convert paths to PathBuf for consistent handling let paths: Vec = paths.iter().map(|p| p.as_ref().to_path_buf()).collect(); - + // Use cache-aware analysis if enabled let report = if options.use_cache && self.cache.is_some() { - self.analyze_with_cache(&paths, &options.analysis_options).await? + self.analyze_with_cache(&paths, &options.analysis_options) + .await? } else { self.analyzer.analyze_paths( &paths.iter().map(|p| p.as_path()).collect::>(), - &options.analysis_options + &options.analysis_options, )? }; - + Ok(report) } - + /// Validate a single file pub fn validate_file>(&self, file_path: P) -> GuardianResult { let violations = self.analyzer.analyze_file(file_path)?; - + let mut report = ValidationReport::new(); for violation in violations { report.add_violation(violation); } report.set_files_analyzed(1); - + Ok(report) } - + /// Validate entire directory tree pub fn validate_directory>( - &self, - root: P, - options: &AnalysisOptions + &self, + root: P, + options: &AnalysisOptions, ) -> GuardianResult { self.analyzer.analyze_directory(root, options) } - + /// Format a validation report for output pub fn format_report( - &self, - report: &ValidationReport, - format: OutputFormat + &self, + report: &ValidationReport, + format: OutputFormat, ) -> GuardianResult { self.report_formatter.format_report(report, format) } - + /// Get analyzer statistics pub fn pattern_statistics(&self) -> PatternStats { self.analyzer.pattern_stats() } - + /// Get cache statistics (if caching is enabled) pub fn cache_statistics(&self) -> Option { self.cache.as_ref().map(|c| c.statistics()) } - + /// Clear cache (if enabled) pub fn clear_cache(&mut self) -> GuardianResult<()> { if let Some(cache) = &mut self.cache { @@ -190,7 +183,7 @@ impl GuardianValidator { } Ok(()) } - + /// Save cache to disk (if enabled and modified) pub fn save_cache(&mut self) -> GuardianResult<()> { if let Some(cache) = &mut self.cache { @@ -198,7 +191,7 @@ impl GuardianValidator { } Ok(()) } - + /// Cleanup cache by removing entries for non-existent files pub fn cleanup_cache(&mut self) -> GuardianResult> { if let Some(cache) = &mut self.cache { @@ -207,16 +200,20 @@ impl GuardianValidator { Ok(None) } } - + /// Cache-aware analysis that skips files that haven't changed - async fn analyze_with_cache(&mut self, paths: &[PathBuf], options: &AnalysisOptions) -> GuardianResult { + async fn analyze_with_cache( + &mut self, + paths: &[PathBuf], + options: &AnalysisOptions, + ) -> GuardianResult { let mut all_violations = Vec::new(); let files_analyzed: usize; let start_time = std::time::Instant::now(); - + // Get config fingerprint for cache validation let config_fingerprint = self.analyzer.config_fingerprint(); - + // Discover all files to analyze let mut all_files = Vec::new(); for path in paths { @@ -225,19 +222,21 @@ impl GuardianValidator { } else if path.is_dir() { // For directories, just analyze normally to discover files let temp_report = self.analyzer.analyze_directory(path, options)?; - // Extract unique file paths from violations - let discovered_files: std::collections::HashSet = temp_report.violations.iter() + // Extract unique file paths from violations + let discovered_files: std::collections::HashSet = temp_report + .violations + .iter() .map(|v| v.file_path.clone()) .collect(); all_files.extend(discovered_files); } } - + if let Some(cache) = &mut self.cache { // Separate files into those that need analysis and those that don't let mut files_to_analyze = Vec::new(); let mut _cached_violation_count = 0; - + for file_path in &all_files { match cache.needs_analysis(file_path, &config_fingerprint) { Ok(needs_analysis) => { @@ -257,52 +256,58 @@ impl GuardianValidator { } } } - + // Analyze only files that need it if !files_to_analyze.is_empty() { let fresh_report = self.analyzer.analyze_paths( - &files_to_analyze.iter().map(|p| p.as_path()).collect::>(), - options + &files_to_analyze + .iter() + .map(|p| p.as_path()) + .collect::>(), + options, )?; - + all_violations.extend(fresh_report.violations); // Note: files_analyzed will be set to all_files.len() below to include cached files - + // Update cache with new results for file_path in &files_to_analyze { - let file_violations: Vec<_> = all_violations.iter() + let file_violations: Vec<_> = all_violations + .iter() .filter(|v| v.file_path == *file_path) .collect(); - - if let Err(e) = cache.update_entry(file_path, file_violations.len(), &config_fingerprint) { + + if let Err(e) = + cache.update_entry(file_path, file_violations.len(), &config_fingerprint) + { tracing::warn!("Failed to update cache for {}: {}", file_path.display(), e); } } } - + files_analyzed = all_files.len(); // Total files considered } else { // No cache - analyze all files normally let report = self.analyzer.analyze_paths( &all_files.iter().map(|p| p.as_path()).collect::>(), - options + options, )?; - + all_violations.extend(report.violations); files_analyzed = report.summary.total_files; } - + // Build final report let mut report = ValidationReport::new(); for violation in all_violations { report.add_violation(violation); } - + report.set_files_analyzed(files_analyzed); report.set_execution_time(start_time.elapsed().as_millis() as u64); report.set_config_fingerprint(config_fingerprint); report.sort_violations(); - + Ok(report) } } @@ -313,17 +318,13 @@ pub fn create_validator() -> GuardianResult { } /// Convenience function to validate files with default settings -pub async fn validate_files>( - files: Vec

-) -> GuardianResult { +pub async fn validate_files>(files: Vec

) -> GuardianResult { let mut validator = GuardianValidator::new()?; validator.validate_for_agent(files).await } /// Convenience function to validate a directory with default settings -pub fn validate_directory>( - directory: P -) -> GuardianResult { +pub fn validate_directory>(directory: P) -> GuardianResult { let validator = GuardianValidator::new()?; validator.validate_directory(directory, &AnalysisOptions::default()) } @@ -331,18 +332,16 @@ pub fn validate_directory>( /// Agent integration utilities pub mod agent { use super::*; - + /// Pre-commit validation for autonomous agents - /// + /// /// This function provides a simple interface for agents to validate /// code before committing changes. It returns an error if any blocking /// violations are found. - pub async fn pre_commit_check>( - modified_files: Vec

- ) -> GuardianResult<()> { + pub async fn pre_commit_check>(modified_files: Vec

) -> GuardianResult<()> { let mut validator = GuardianValidator::new()?; let report = validator.validate_for_agent(modified_files).await?; - + if report.has_errors() { let error_count = report.summary.violations_by_severity.error; return Err(GuardianError::config(format!( @@ -351,16 +350,16 @@ pub mod agent { if error_count == 1 { "" } else { "s" } ))); } - + Ok(()) } - + /// Quick validation for development workflows - /// + /// /// Validates files with relaxed settings suitable for development, /// only failing on critical errors. pub async fn development_check>( - files: Vec

+ files: Vec

, ) -> GuardianResult { let options = ValidationOptions { analysis_options: AnalysisOptions { @@ -374,17 +373,17 @@ pub mod agent { }, ..Default::default() }; - + let mut validator = GuardianValidator::new()?; validator.validate_with_options(files, &options).await } - + /// Production validation for CI/CD pipelines - /// + /// /// Strict validation suitable for production deployments, /// failing on any errors or warnings. pub async fn production_check>( - files: Vec

+ files: Vec

, ) -> GuardianResult { let options = ValidationOptions { analysis_options: AnalysisOptions { @@ -399,10 +398,10 @@ pub mod agent { }, ..Default::default() }; - + let mut validator = GuardianValidator::new()?; let report = validator.validate_with_options(files, &options).await?; - + // Fail if any warnings or errors found if report.has_violations() { return Err(GuardianError::config(format!( @@ -410,7 +409,7 @@ pub mod agent { report.violations.len() ))); } - + Ok(report) } } @@ -418,131 +417,137 @@ pub mod agent { #[cfg(test)] mod tests { use super::*; - use tempfile::TempDir; use std::fs; - + use tempfile::TempDir; + #[tokio::test] async fn test_validator_creation() { let validator = GuardianValidator::new().unwrap(); let stats = validator.pattern_statistics(); - + // Should have default patterns loaded assert!(stats.enabled_rules > 0); } - + #[tokio::test] async fn test_validate_for_agent() { let temp_dir = TempDir::new().unwrap(); let test_file = temp_dir.path().join("test.rs"); - + // Create a file with violations fs::write(&test_file, "// TODO: implement this\nfn main() {}").unwrap(); - + let mut validator = GuardianValidator::new().unwrap(); let report = validator.validate_for_agent(vec![test_file]).await.unwrap(); - + // Should find the TODO comment assert!(report.has_violations()); assert!(report.violations.iter().any(|v| v.rule_id.contains("todo"))); } - + #[test] fn test_single_file_validation() { let temp_dir = TempDir::new().unwrap(); let test_file = temp_dir.path().join("test.rs"); - + fs::write(&test_file, "fn main() { unimplemented!() }").unwrap(); - + let validator = GuardianValidator::new().unwrap(); let report = validator.validate_file(&test_file).unwrap(); - + assert!(report.has_violations()); assert_eq!(report.summary.total_files, 1); } - + #[test] fn test_directory_validation() { let temp_dir = TempDir::new().unwrap(); let root = temp_dir.path(); - + // Create directory structure fs::create_dir_all(root.join("src")).unwrap(); fs::write(root.join("src/lib.rs"), "// TODO: implement").unwrap(); fs::write(root.join("src/main.rs"), "fn main() {}").unwrap(); - + let validator = GuardianValidator::new().unwrap(); - let report = validator.validate_directory(root, &AnalysisOptions::default()).unwrap(); - + let report = validator + .validate_directory(root, &AnalysisOptions::default()) + .unwrap(); + assert!(report.has_violations()); assert!(report.summary.total_files > 0); } - + #[test] fn test_report_formatting() { let temp_dir = TempDir::new().unwrap(); let test_file = temp_dir.path().join("test.rs"); - + fs::write(&test_file, "// TODO: test").unwrap(); - + let validator = GuardianValidator::new().unwrap(); let report = validator.validate_file(&test_file).unwrap(); - + // Test different formats - let human = validator.format_report(&report, OutputFormat::Human).unwrap(); + let human = validator + .format_report(&report, OutputFormat::Human) + .unwrap(); assert!(human.contains("Code Quality Violations Found")); - - let json = validator.format_report(&report, OutputFormat::Json).unwrap(); + + let json = validator + .format_report(&report, OutputFormat::Json) + .unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); assert!(parsed["violations"].is_array()); } - + #[tokio::test] async fn test_agent_pre_commit_check() { let temp_dir = TempDir::new().unwrap(); let clean_file = temp_dir.path().join("clean.rs"); let dirty_file = temp_dir.path().join("dirty.rs"); - + fs::write(&clean_file, "fn main() { println!(\"Hello\"); }").unwrap(); fs::write(&dirty_file, "fn main() { TODO: implement }").unwrap(); - + // Clean file should pass assert!(agent::pre_commit_check(vec![clean_file]).await.is_ok()); - + // Dirty file should fail assert!(agent::pre_commit_check(vec![dirty_file]).await.is_err()); } - + #[tokio::test] async fn test_development_vs_production_checks() { let temp_dir = TempDir::new().unwrap(); let test_file = temp_dir.path().join("test.rs"); - + // File with warnings but no errors fs::write(&test_file, "fn main() { /* temporary implementation */ }").unwrap(); - + // Development check should be more lenient let dev_result = agent::development_check(vec![&test_file]).await; assert!(dev_result.is_ok()); - + // Production check should be strict let prod_result = agent::production_check(vec![&test_file]).await; // This might pass or fail depending on patterns - main point is they're different let _ = prod_result; // Just ensure it doesn't panic } - + #[test] fn test_convenience_functions() { let temp_dir = TempDir::new().unwrap(); let test_file = temp_dir.path().join("test.rs"); - + fs::write(&test_file, "fn main() {}").unwrap(); - + // Test convenience validator creation let validator = create_validator().unwrap(); assert!(validator.pattern_statistics().enabled_rules > 0); - + // Test convenience directory validation let report = validate_directory(temp_dir.path()).unwrap(); assert_eq!(report.summary.total_files, 1); } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 7eb07bc..c9db9fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,37 +1,38 @@ //! Rust Guardian CLI - Command-line interface for code quality enforcement -//! +//! //! CDD Principle: Application Layer - CLI coordinates user interactions with domain services //! - Translates user commands to domain operations //! - Handles external concerns like file I/O, process exit codes, and terminal output //! - Provides clean separation between user interface and business logic +use clap::{Parser, Subcommand, ValueEnum}; use rust_guardian::{ - GuardianValidator, GuardianConfig, ValidationOptions, AnalysisOptions, - OutputFormat, ReportOptions, Severity, GuardianResult, GuardianError + AnalysisOptions, GuardianConfig, GuardianError, GuardianResult, GuardianValidator, + OutputFormat, ReportOptions, Severity, ValidationOptions, }; -use clap::{Parser, Subcommand, ValueEnum}; use std::path::{Path, PathBuf}; use std::process; - /// Rust Guardian - Dynamic code quality enforcement #[derive(Parser)] #[command(name = "rust-guardian")] #[command(version = "0.1.0")] #[command(about = "Dynamic code quality enforcement preventing incomplete or placeholder code")] -#[command(long_about = "Rust Guardian analyzes code for quality violations, placeholder implementations, and architectural compliance. Designed for autonomous agent workflows and CI/CD integration.")] +#[command( + long_about = "Rust Guardian analyzes code for quality violations, placeholder implementations, and architectural compliance. Designed for autonomous agent workflows and CI/CD integration." +)] struct Cli { #[command(subcommand)] command: Commands, - + /// Enable verbose logging #[arg(short, long, global = true)] verbose: bool, - + /// Configuration file path #[arg(short, long, global = true)] config: Option, - + /// Disable colored output #[arg(long, global = true)] no_color: bool, @@ -43,86 +44,86 @@ enum Commands { Check { /// Paths to analyze (files or directories) paths: Vec, - + /// Output format #[arg(short, long, value_enum, default_value = "human")] format: OutputFormatArg, - + /// Minimum severity level to report #[arg(short, long, value_enum)] severity: Option, - + /// Maximum number of violations to report #[arg(long)] max_violations: Option, - + /// Additional exclude patterns #[arg(long, action = clap::ArgAction::Append)] exclude: Vec, - + /// Ignore .guardianignore files #[arg(long)] no_ignore: bool, - + /// Custom .guardianignore file #[arg(long)] guardianignore: Option, - + /// Disable parallel processing #[arg(long)] no_parallel: bool, - + /// Fail on first error #[arg(long)] fail_fast: bool, - + /// Enable caching for better performance #[arg(long)] cache: bool, - + /// Custom cache file path #[arg(long)] cache_file: Option, }, - + /// Watch for file changes and run checks automatically Watch { /// Path to watch (defaults to current directory) path: Option, - + /// File patterns to watch (glob patterns) #[arg(short, long, action = clap::ArgAction::Append)] pattern: Vec, - + /// Debounce delay in milliseconds #[arg(long, default_value = "500")] delay: u64, }, - + /// Validate configuration file ValidateConfig { /// Configuration file to validate config_file: Option, }, - + /// Explain what a specific rule does Explain { /// Rule ID to explain rule_id: String, }, - + /// Show cache statistics Cache { #[command(subcommand)] action: CacheCommands, }, - + /// List available rules and patterns Rules { /// Show only enabled rules #[arg(long)] enabled_only: bool, - + /// Filter by category #[arg(long)] category: Option, @@ -137,14 +138,14 @@ enum CacheCommands { #[arg(long)] cache_file: Option, }, - + /// Clear the cache Clear { /// Cache file path #[arg(long)] cache_file: Option, }, - + /// Clean up stale cache entries Cleanup { /// Cache file path @@ -194,13 +195,13 @@ impl From for Severity { #[tokio::main] async fn main() { let cli = Cli::parse(); - + // Initialize logging init_logging(cli.verbose); - + // Run the command and handle the result let result = run_command(cli).await; - + match result { Ok(exit_code) => { process::exit(exit_code); @@ -240,23 +241,21 @@ async fn run_command(cli: Cli) -> GuardianResult { cache, cache_file, !cli.no_color, - ).await - } - Commands::Watch { path, pattern, delay } => { - run_watch(path, pattern, delay).await - } - Commands::ValidateConfig { config_file } => { - run_validate_config(config_file.or(cli.config)) - } - Commands::Explain { rule_id } => { - run_explain(rule_id) - } - Commands::Cache { action } => { - run_cache_command(action).await - } - Commands::Rules { enabled_only, category } => { - run_list_rules(cli.config, enabled_only, category) + ) + .await } + Commands::Watch { + path, + pattern, + delay, + } => run_watch(path, pattern, delay).await, + Commands::ValidateConfig { config_file } => run_validate_config(config_file.or(cli.config)), + Commands::Explain { rule_id } => run_explain(rule_id), + Commands::Cache { action } => run_cache_command(action).await, + Commands::Rules { + enabled_only, + category, + } => run_list_rules(cli.config, enabled_only, category), } } @@ -279,37 +278,40 @@ async fn run_check( GuardianConfig::load_from_file(config_path)? } else { // Try to find default config file - let default_configs = ["rust_guardian.yaml", "rust_guardian.yml", ".rust_guardian.yaml"]; + let default_configs = [ + "rust_guardian.yaml", + "rust_guardian.yml", + ".rust_guardian.yaml", + ]; let mut config = None; - + for config_name in &default_configs { if Path::new(config_name).exists() { config = Some(GuardianConfig::load_from_file(config_name)?); break; } } - + config.unwrap_or_else(|| GuardianConfig::default()) }; - + // Create validator let mut validator = GuardianValidator::new_with_config(config)?; - + // Enable cache if requested if use_cache { - let cache_path = cache_file.unwrap_or_else(|| { - PathBuf::from(".rust").join("guardian_cache.json") - }); + let cache_path = + cache_file.unwrap_or_else(|| PathBuf::from(".rust").join("guardian_cache.json")); validator = validator.with_cache(cache_path)?; } - + // Use current directory if no paths specified let paths = if paths.is_empty() { vec![PathBuf::from(".")] } else { paths }; - + // Set up validation options let validation_options = ValidationOptions { use_cache, @@ -329,14 +331,16 @@ async fn run_check( }, ..Default::default() }; - + // Run validation - let report = validator.validate_with_options(paths, &validation_options).await?; - + let report = validator + .validate_with_options(paths, &validation_options) + .await?; + // Format and output results let formatted = validator.format_report(&report, format.into())?; println!("{}", formatted); - + // Print cache statistics if caching is enabled if use_cache { if let Some(stats) = validator.cache_statistics() { @@ -345,12 +349,12 @@ async fn run_check( } } } - + // Save cache if enabled if use_cache { validator.save_cache()?; } - + // Return appropriate exit code if report.has_errors() { Ok(1) // Exit code 1 for errors @@ -364,55 +368,61 @@ async fn run_watch( patterns: Vec, delay_ms: u64, ) -> GuardianResult { - use notify::{Watcher, RecursiveMode, Result as NotifyResult, Event}; + use notify::{Event, RecursiveMode, Result as NotifyResult, Watcher}; + use std::io::{self, Write}; use std::sync::mpsc; - use std::time::Duration; use std::thread; - use std::io::{self, Write}; - + use std::time::Duration; + let watch_path = path.unwrap_or_else(|| PathBuf::from(".")); - + println!("🔍 Starting Rust Guardian watch mode..."); println!("📂 Watching: {}", watch_path.display()); - + // Set up file patterns to watch (default to Rust files if none specified) let watch_patterns = if patterns.is_empty() { vec!["**/*.rs".to_string()] } else { patterns }; - + println!("🎯 Patterns: {}", watch_patterns.join(", ")); println!("⏱️ Debounce delay: {}ms", delay_ms); println!("Press Ctrl+C to stop watching\\n"); - + // Create a channel for file system events let (tx, rx) = mpsc::channel(); - + // Create a watcher - let mut watcher = notify::recommended_watcher(move |res: NotifyResult| { - match res { - Ok(event) => { - if let Err(e) = tx.send(event) { - eprintln!("Error sending event: {}", e); - } + let mut watcher = notify::recommended_watcher(move |res: NotifyResult| match res { + Ok(event) => { + if let Err(e) = tx.send(event) { + eprintln!("Error sending event: {}", e); } - Err(e) => eprintln!("Watch error: {}", e), } - }).map_err(|e| GuardianError::config(format!("Failed to create file watcher: {}", e)))?; - + Err(e) => eprintln!("Watch error: {}", e), + }) + .map_err(|e| GuardianError::config(format!("Failed to create file watcher: {}", e)))?; + // Start watching the path - watcher.watch(&watch_path, RecursiveMode::Recursive) - .map_err(|e| GuardianError::config(format!("Failed to watch path '{}': {}", watch_path.display(), e)))?; - + watcher + .watch(&watch_path, RecursiveMode::Recursive) + .map_err(|e| { + GuardianError::config(format!( + "Failed to watch path '{}': {}", + watch_path.display(), + e + )) + })?; + // Track last run to implement debouncing let mut last_run = std::time::Instant::now(); let debounce_duration = Duration::from_millis(delay_ms); - + // Run initial check println!("🚀 Running initial analysis..."); run_watch_analysis_with_config(&watch_path, &watch_patterns, None).await?; - + // Main event loop loop { match rx.recv_timeout(Duration::from_millis(100)) { @@ -421,12 +431,18 @@ async fn run_watch( if let Some(config_path) = is_config_change(&event) { println!("🔄 Configuration file changed: {}", config_path.display()); println!("📝 Reloading configuration and running analysis..."); - + // Clear terminal and run analysis with new config print!("\\x1B[2J\\x1B[H"); // Clear screen and move cursor to top io::stdout().flush().unwrap(); - - if let Err(e) = run_watch_analysis_with_config(&watch_path, &watch_patterns, Some(&config_path)).await { + + if let Err(e) = run_watch_analysis_with_config( + &watch_path, + &watch_patterns, + Some(&config_path), + ) + .await + { eprintln!("❌ Config reload and analysis failed: {}", e); } last_run = std::time::Instant::now(); @@ -438,9 +454,11 @@ async fn run_watch( // Clear terminal and run analysis print!("\\x1B[2J\\x1B[H"); // Clear screen and move cursor to top io::stdout().flush().unwrap(); - + println!("📝 File changes detected, running analysis..."); - if let Err(e) = run_watch_analysis_with_config(&watch_path, &watch_patterns, None).await { + if let Err(e) = + run_watch_analysis_with_config(&watch_path, &watch_patterns, None).await + { eprintln!("❌ Analysis failed: {}", e); } last_run = now; @@ -456,28 +474,28 @@ async fn run_watch( break; } } - + // Small delay to prevent excessive CPU usage thread::sleep(Duration::from_millis(10)); } - + Ok(0) } /// Check if an event should trigger analysis or config reload fn should_trigger_analysis(event: ¬ify::Event, patterns: &[String]) -> bool { use notify::EventKind; - + // Only trigger on write/create/rename events match event.kind { - EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {}, + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {} _ => return false, } - + // Check if any affected path matches our patterns for path in &event.paths { let path_str = path.to_string_lossy(); - + for pattern in patterns { if let Ok(glob_pattern) = glob::Pattern::new(pattern) { if glob_pattern.matches(&path_str) { @@ -486,43 +504,44 @@ fn should_trigger_analysis(event: ¬ify::Event, patterns: &[String]) -> bool { } } } - + false } /// Check if an event indicates a config file change fn is_config_change(event: ¬ify::Event) -> Option { use notify::EventKind; - + // Only trigger on write/create/rename events match event.kind { - EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {}, + EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {} _ => return None, } - + // Check if any affected path is a config file for path in &event.paths { let file_name = path.file_name()?.to_str()?; - + // Check for common config file names - if matches!(file_name, - "rust_guardian.yaml" | - "rust_guardian.yml" | - ".rust_guardian.yaml" | - ".rust_guardian.yml" + if matches!( + file_name, + "rust_guardian.yaml" + | "rust_guardian.yml" + | ".rust_guardian.yaml" + | ".rust_guardian.yml" ) { return Some(path.clone()); } } - + None } /// Run analysis for watch mode with optional config file async fn run_watch_analysis_with_config( - watch_path: &Path, - _patterns: &[String], - config_path: Option<&Path> + watch_path: &Path, + _patterns: &[String], + config_path: Option<&Path>, ) -> GuardianResult<()> { // Load configuration let config = if let Some(config_path) = config_path { @@ -532,16 +551,24 @@ async fn run_watch_analysis_with_config( config } Err(e) => { - eprintln!("⚠️ Failed to reload config from {}: {}", config_path.display(), e); + eprintln!( + "⚠️ Failed to reload config from {}: {}", + config_path.display(), + e + ); eprintln!(" Using default configuration instead..."); GuardianConfig::default() } } } else { // Try to find default config file - let default_configs = ["rust_guardian.yaml", "rust_guardian.yml", ".rust_guardian.yaml"]; + let default_configs = [ + "rust_guardian.yaml", + "rust_guardian.yml", + ".rust_guardian.yaml", + ]; let mut config = None; - + for config_name in &default_configs { if Path::new(config_name).exists() { match GuardianConfig::load_from_file(config_name) { @@ -556,13 +583,13 @@ async fn run_watch_analysis_with_config( } } } - + config.unwrap_or_else(|| GuardianConfig::default()) }; - + // Create validator let mut validator = GuardianValidator::new_with_config(config)?; - + // Set up validation options for watch mode let validation_options = ValidationOptions { analysis_options: AnalysisOptions { @@ -579,71 +606,91 @@ async fn run_watch_analysis_with_config( output_format: OutputFormat::Human, ..Default::default() }; - + // Run validation - match validator.validate_with_options(vec![watch_path], &validation_options).await { + match validator + .validate_with_options(vec![watch_path], &validation_options) + .await + { Ok(report) => { if report.has_violations() { let formatted = validator.format_report(&report, OutputFormat::Human)?; println!("{}", formatted); - + let error_count = report.summary.violations_by_severity.error; let warning_count = report.summary.violations_by_severity.warning; let info_count = report.summary.violations_by_severity.info; - + if error_count > 0 { - println!("\\n❌ Found {} error{}, {} warning{}, {} info", - error_count, if error_count == 1 { "" } else { "s" }, - warning_count, if warning_count == 1 { "" } else { "s" }, - info_count); + println!( + "\\n❌ Found {} error{}, {} warning{}, {} info", + error_count, + if error_count == 1 { "" } else { "s" }, + warning_count, + if warning_count == 1 { "" } else { "s" }, + info_count + ); } else if warning_count > 0 { - println!("\\n⚠️ Found {} warning{}, {} info", - warning_count, if warning_count == 1 { "" } else { "s" }, - info_count); + println!( + "\\n⚠️ Found {} warning{}, {} info", + warning_count, + if warning_count == 1 { "" } else { "s" }, + info_count + ); } else { - println!("\\n✅ Found {} info message{}", - info_count, if info_count == 1 { "" } else { "s" }); + println!( + "\\n✅ Found {} info message{}", + info_count, + if info_count == 1 { "" } else { "s" } + ); } } else { println!("✅ No code quality violations found"); } - - println!("📊 Analyzed {} files in {:.1}s", - report.summary.total_files, - report.summary.execution_time_ms as f64 / 1000.0); + + println!( + "📊 Analyzed {} files in {:.1}s", + report.summary.total_files, + report.summary.execution_time_ms as f64 / 1000.0 + ); println!("⌚ Watching for changes... (Press Ctrl+C to stop)\\n"); } Err(e) => { eprintln!("❌ Analysis error: {}", e); } } - + Ok(()) } fn run_validate_config(config_path: Option) -> GuardianResult { let config_path = config_path.unwrap_or_else(|| PathBuf::from("rust_guardian.yaml")); - + println!("Validating configuration: {}", config_path.display()); - + match GuardianConfig::load_from_file(&config_path) { Ok(config) => { println!("✅ Configuration is valid"); - + // Show some statistics let total_categories = config.patterns.len(); let enabled_categories = config.patterns.values().filter(|c| c.enabled).count(); let total_rules: usize = config.patterns.values().map(|c| c.rules.len()).sum(); - let enabled_rules: usize = config.patterns.values() + let enabled_rules: usize = config + .patterns + .values() .filter(|c| c.enabled) .map(|c| c.rules.iter().filter(|r| r.enabled).count()) .sum(); - + println!("📊 Configuration summary:"); - println!(" Categories: {} total, {} enabled", total_categories, enabled_categories); + println!( + " Categories: {} total, {} enabled", + total_categories, enabled_categories + ); println!(" Rules: {} total, {} enabled", total_rules, enabled_rules); println!(" Path patterns: {}", config.paths.patterns.len()); - + Ok(0) } Err(e) => { @@ -655,14 +702,17 @@ fn run_validate_config(config_path: Option) -> GuardianResult { fn run_explain(rule_id: String) -> GuardianResult { let config = GuardianConfig::default(); - + // Find the rule in the configuration for (category_name, category) in &config.patterns { for rule in &category.rules { if rule.id == rule_id { println!("📖 Rule: {}", rule.id); println!("📂 Category: {}", category_name); - println!("⚠️ Severity: {:?}", rule.severity.unwrap_or(category.severity)); + println!( + "⚠️ Severity: {:?}", + rule.severity.unwrap_or(category.severity) + ); println!("🔍 Type: {:?}", rule.rule_type); println!("✅ Enabled: {}", rule.enabled); println!(); @@ -671,7 +721,7 @@ fn run_explain(rule_id: String) -> GuardianResult { println!(); println!("🔎 Pattern:"); println!(" {}", rule.pattern); - + if let Some(exclude) = &rule.exclude_if { println!(); println!("🚫 Exclusions:"); @@ -685,77 +735,74 @@ fn run_explain(rule_id: String) -> GuardianResult { println!(" File patterns: {}", patterns.join(", ")); } } - + return Ok(0); } } } - + eprintln!("❌ Rule '{}' not found", rule_id); println!(); println!("Available rules:"); - + for (category_name, category) in &config.patterns { println!(" {}:", category_name); for rule in &category.rules { println!(" - {}", rule.id); } } - + Ok(1) } async fn run_cache_command(action: CacheCommands) -> GuardianResult { match action { CacheCommands::Stats { cache_file } => { - let cache_path = cache_file.unwrap_or_else(|| { - PathBuf::from(".rust").join("guardian_cache.json") - }); - + let cache_path = + cache_file.unwrap_or_else(|| PathBuf::from(".rust").join("guardian_cache.json")); + if !cache_path.exists() { println!("No cache file found at {}", cache_path.display()); return Ok(1); } - + let mut cache = rust_guardian::FileCache::new(&cache_path); cache.load()?; - + let stats = cache.statistics(); println!("📊 Cache Statistics"); println!(" File: {}", cache_path.display()); println!(" {}", stats.format_display()); println!(" Created: {}", format_timestamp(stats.created_at)); println!(" Updated: {}", format_timestamp(stats.updated_at)); - + Ok(0) } CacheCommands::Clear { cache_file } => { - let cache_path = cache_file.unwrap_or_else(|| { - PathBuf::from(".rust").join("guardian_cache.json") - }); - + let cache_path = + cache_file.unwrap_or_else(|| PathBuf::from(".rust").join("guardian_cache.json")); + let mut cache = rust_guardian::FileCache::new(&cache_path); cache.load()?; cache.clear()?; - + println!("✅ Cache cleared: {}", cache_path.display()); Ok(0) } CacheCommands::Cleanup { cache_file } => { - let cache_path = cache_file.unwrap_or_else(|| { - PathBuf::from(".rust").join("guardian_cache.json") - }); - + let cache_path = + cache_file.unwrap_or_else(|| PathBuf::from(".rust").join("guardian_cache.json")); + if !cache_path.exists() { println!("No cache file found at {}", cache_path.display()); return Ok(1); } - + let mut cache = rust_guardian::FileCache::new(&cache_path); cache.load()?; let removed = cache.cleanup()?; cache.save()?; - + println!("✅ Cleaned up {} stale cache entries", removed); Ok(0) } @@ -772,9 +819,9 @@ fn run_list_rules( } else { GuardianConfig::default() }; - + println!("📋 Available Rules\n"); - + for (category_name, category) in &config.patterns { // Apply category filter if let Some(ref filter) = category_filter { @@ -782,34 +829,40 @@ fn run_list_rules( continue; } } - + // Skip disabled categories if enabled_only is true if enabled_only && !category.enabled { continue; } - + let status = if category.enabled { "✅" } else { "❌" }; - println!("{}📂 {} ({})", status, category_name, category.severity.as_str()); - + println!( + "{}📂 {} ({})", + status, + category_name, + category.severity.as_str() + ); + for rule in &category.rules { // Skip disabled rules if enabled_only is true if enabled_only && !rule.enabled { continue; } - + let rule_status = if rule.enabled { "✅" } else { "❌" }; let severity = rule.severity.unwrap_or(category.severity); - - println!(" {}🔍 {} [{}] - {}", - rule_status, - rule.id, + + println!( + " {}🔍 {} [{}] - {}", + rule_status, + rule.id, severity.as_str(), rule.message ); } println!(); } - + Ok(0) } @@ -819,7 +872,7 @@ fn init_logging(verbose: bool) { } else { tracing::Level::WARN }; - + tracing_subscriber::fmt() .with_max_level(level) .with_target(false) @@ -827,28 +880,29 @@ fn init_logging(verbose: bool) { } fn format_timestamp(timestamp: u64) -> String { - use chrono::{Utc, TimeZone}; - - let dt = Utc.timestamp_opt(timestamp as i64, 0) + use chrono::{TimeZone, Utc}; + + let dt = Utc + .timestamp_opt(timestamp as i64, 0) .single() .unwrap_or_else(|| Utc::now()); - + dt.format("%Y-%m-%d %H:%M:%S UTC").to_string() } #[cfg(test)] mod tests { use super::*; - use tempfile::TempDir; use std::fs; - + use tempfile::TempDir; + #[tokio::test] async fn test_check_command() { let temp_dir = TempDir::new().unwrap(); let test_file = temp_dir.path().join("test.rs"); - + fs::write(&test_file, "// TODO: implement this\nfn main() {}").unwrap(); - + // Test basic check let result = run_check( None, @@ -863,41 +917,42 @@ mod tests { false, None, false, - ).await; - + ) + .await; + // Should find violations (exit code 1) assert_eq!(result.unwrap(), 1); } - + #[test] fn test_validate_config() { let temp_dir = TempDir::new().unwrap(); let config_file = temp_dir.path().join("test_config.yaml"); - + // Create a valid config file let config = GuardianConfig::default(); let yaml = serde_yaml::to_string(&config).unwrap(); fs::write(&config_file, yaml).unwrap(); - + let result = run_validate_config(Some(config_file)); assert_eq!(result.unwrap(), 0); } - + #[test] fn test_explain_rule() { let result = run_explain("todo_comments".to_string()); assert_eq!(result.unwrap(), 0); - + let result = run_explain("nonexistent_rule".to_string()); assert_eq!(result.unwrap(), 1); } - + #[test] fn test_list_rules() { let result = run_list_rules(None, false, None); assert_eq!(result.unwrap(), 0); - + let result = run_list_rules(None, true, Some("placeholders".to_string())); assert_eq!(result.unwrap(), 0); } -} \ No newline at end of file +} diff --git a/src/patterns/mod.rs b/src/patterns/mod.rs index 5a57b67..9dec2cd 100644 --- a/src/patterns/mod.rs +++ b/src/patterns/mod.rs @@ -1,5 +1,5 @@ //! Pattern engine for detecting code quality violations -//! +//! //! CDD Principle: Domain Services - Pattern matching orchestrates complex analysis operations //! - PatternEngine coordinates different types of pattern matching (regex, AST, semantic) //! - Each pattern type implements the PatternMatcher trait for clean polymorphism @@ -7,11 +7,11 @@ pub mod path_filter; -use crate::config::{PatternRule, RuleType, ExcludeConditions}; -use crate::domain::violations::{Violation, Severity, GuardianError, GuardianResult}; +use crate::config::{ExcludeConditions, PatternRule, RuleType}; +use crate::domain::violations::{GuardianError, GuardianResult, Severity, Violation}; use regex::{Regex, RegexBuilder}; -use std::path::{Path, PathBuf}; use std::collections::HashMap; +use std::path::{Path, PathBuf}; use syn::spanned::Spanned; pub use path_filter::PathFilter; @@ -77,9 +77,13 @@ impl PatternEngine { ast_patterns: HashMap::new(), } } - + /// Add a pattern rule to the engine - pub fn add_rule(&mut self, rule: &PatternRule, effective_severity: Severity) -> GuardianResult<()> { + pub fn add_rule( + &mut self, + rule: &PatternRule, + effective_severity: Severity, + ) -> GuardianResult<()> { match rule.rule_type { RuleType::Regex => { let regex = if rule.case_sensitive { @@ -88,40 +92,53 @@ impl PatternEngine { RegexBuilder::new(&rule.pattern) .case_insensitive(true) .build() - }.map_err(|e| GuardianError::pattern(format!("Invalid regex '{}': {}", rule.pattern, e)))?; - - self.regex_patterns.insert(rule.id.clone(), CompiledRegex { - regex, - rule_id: rule.id.clone(), - message_template: rule.message.clone(), - severity: effective_severity, - exclude_conditions: rule.exclude_if.clone(), - }); + } + .map_err(|e| { + GuardianError::pattern(format!("Invalid regex '{}': {}", rule.pattern, e)) + })?; + + self.regex_patterns.insert( + rule.id.clone(), + CompiledRegex { + regex, + rule_id: rule.id.clone(), + message_template: rule.message.clone(), + severity: effective_severity, + exclude_conditions: rule.exclude_if.clone(), + }, + ); } RuleType::Ast => { let pattern_type = self.parse_ast_pattern(&rule.pattern, &rule.id)?; - - self.ast_patterns.insert(rule.id.clone(), AstPattern { - pattern_type, - rule_id: rule.id.clone(), - message_template: rule.message.clone(), - severity: effective_severity, - exclude_conditions: rule.exclude_if.clone(), - }); + + self.ast_patterns.insert( + rule.id.clone(), + AstPattern { + pattern_type, + rule_id: rule.id.clone(), + message_template: rule.message.clone(), + severity: effective_severity, + exclude_conditions: rule.exclude_if.clone(), + }, + ); } RuleType::Semantic | RuleType::ImportAnalysis => { // TODO: Implement semantic and import analysis patterns - tracing::warn!("Semantic and import analysis patterns not yet implemented: {}", rule.id); + tracing::warn!( + "Semantic and import analysis patterns not yet implemented: {}", + rule.id + ); } } - + Ok(()) } - + /// Parse AST pattern string into typed pattern fn parse_ast_pattern(&self, pattern: &str, rule_id: &str) -> GuardianResult { if pattern.starts_with("macro_call:") { - let macros = pattern.strip_prefix("macro_call:") + let macros = pattern + .strip_prefix("macro_call:") .unwrap() .split('|') .map(|s| s.trim().to_string()) @@ -132,21 +149,28 @@ impl PatternEngine { } else if pattern.contains("CDD Principle:") { Ok(AstPatternType::MissingCddHeader) } else { - Err(GuardianError::pattern(format!("Unknown AST pattern type in rule '{}': {}", rule_id, pattern))) + Err(GuardianError::pattern(format!( + "Unknown AST pattern type in rule '{}': {}", + rule_id, pattern + ))) } } - + /// Analyze a file and return all pattern matches - pub fn analyze_file>(&self, file_path: P, content: &str) -> GuardianResult> { + pub fn analyze_file>( + &self, + file_path: P, + content: &str, + ) -> GuardianResult> { let file_path = file_path.as_ref(); let mut matches = Vec::new(); - + // Apply regex patterns for pattern in self.regex_patterns.values() { let pattern_matches = self.apply_regex_pattern(pattern, file_path, content)?; matches.extend(pattern_matches); } - + // Apply AST patterns for Rust files if file_path.extension().and_then(|s| s.to_str()) == Some("rs") { for pattern in self.ast_patterns.values() { @@ -154,26 +178,38 @@ impl PatternEngine { matches.extend(pattern_matches); } } - + Ok(matches) } - + /// Apply a regex pattern to file content - fn apply_regex_pattern(&self, pattern: &CompiledRegex, file_path: &Path, content: &str) -> GuardianResult> { + fn apply_regex_pattern( + &self, + pattern: &CompiledRegex, + file_path: &Path, + content: &str, + ) -> GuardianResult> { let mut matches = Vec::new(); - + // Find all matches in the content for regex_match in pattern.regex.find_iter(content) { let matched_text = regex_match.as_str().to_string(); - let (line_num, col_num, context) = self.get_match_location(content, regex_match.start()); - + let (line_num, col_num, context) = + self.get_match_location(content, regex_match.start()); + // Check exclude conditions - if self.should_exclude_match(pattern.exclude_conditions.as_ref(), file_path, &matched_text, content, regex_match.start()) { + if self.should_exclude_match( + pattern.exclude_conditions.as_ref(), + file_path, + &matched_text, + content, + regex_match.start(), + ) { continue; } - + let message = pattern.message_template.replace("{match}", &matched_text); - + matches.push(PatternMatch { rule_id: pattern.rule_id.clone(), file_path: file_path.to_path_buf(), @@ -185,14 +221,19 @@ impl PatternEngine { context: Some(context), }); } - + Ok(matches) } - + /// Apply an AST pattern to Rust source code - fn apply_ast_pattern(&self, pattern: &AstPattern, file_path: &Path, content: &str) -> GuardianResult> { + fn apply_ast_pattern( + &self, + pattern: &AstPattern, + file_path: &Path, + content: &str, + ) -> GuardianResult> { let mut matches = Vec::new(); - + // Parse Rust syntax let syntax_tree = match syn::parse_file(content) { Ok(tree) => tree, @@ -202,18 +243,25 @@ impl PatternEngine { return Ok(matches); } }; - + match &pattern.pattern_type { AstPatternType::MacroCall(macro_names) => { let found_matches = self.find_macro_calls(&syntax_tree, macro_names); for (line, col, macro_name, context) in found_matches { // Check exclude conditions - if self.should_exclude_ast_match(pattern.exclude_conditions.as_ref(), file_path, &syntax_tree, line) { + if self.should_exclude_ast_match( + pattern.exclude_conditions.as_ref(), + file_path, + &syntax_tree, + line, + ) { continue; } - - let message = pattern.message_template.replace("{macro_name}", ¯o_name); - + + let message = pattern + .message_template + .replace("{macro_name}", ¯o_name); + matches.push(PatternMatch { rule_id: pattern.rule_id.clone(), file_path: file_path.to_path_buf(), @@ -230,10 +278,15 @@ impl PatternEngine { let found_matches = self.find_empty_ok_returns(&syntax_tree); for (line, col, context) in found_matches { // Check exclude conditions - if self.should_exclude_ast_match(pattern.exclude_conditions.as_ref(), file_path, &syntax_tree, line) { + if self.should_exclude_ast_match( + pattern.exclude_conditions.as_ref(), + file_path, + &syntax_tree, + line, + ) { continue; } - + matches.push(PatternMatch { rule_id: pattern.rule_id.clone(), file_path: file_path.to_path_buf(), @@ -261,19 +314,23 @@ impl PatternEngine { } } } - + Ok(matches) } - + /// Find macro calls in the syntax tree - fn find_macro_calls(&self, syntax_tree: &syn::File, target_macros: &[String]) -> Vec<(u32, u32, String, String)> { + fn find_macro_calls( + &self, + syntax_tree: &syn::File, + target_macros: &[String], + ) -> Vec<(u32, u32, String, String)> { use syn::visit::Visit; - + struct MacroVisitor<'a> { target_macros: &'a [String], matches: Vec<(u32, u32, String, String)>, } - + impl<'a> Visit<'_> for MacroVisitor<'a> { fn visit_macro(&mut self, mac: &syn::Macro) { if let Some(ident) = mac.path.get_ident() { @@ -282,31 +339,31 @@ impl PatternEngine { let _span = mac.path.span(); // Use a simple line-based location since proc_macro2::Span doesn't have start() method // Use a simple line-based location since proc_macro2::Span doesn't have start() method - let (line, col, context) = (1, 1, String::new()); + let (line, col, context) = (1, 1, String::new()); self.matches.push((line, col, macro_name, context)); } } syn::visit::visit_macro(self, mac); } } - + let mut visitor = MacroVisitor { target_macros, matches: Vec::new(), }; - + visitor.visit_file(syntax_tree); visitor.matches } - + /// Find functions that return empty Ok(()) responses fn find_empty_ok_returns(&self, syntax_tree: &syn::File) -> Vec<(u32, u32, String)> { use syn::visit::Visit; - + struct EmptyOkVisitor { matches: Vec<(u32, u32, String)>, } - + impl Visit<'_> for EmptyOkVisitor { fn visit_item_fn(&mut self, func: &syn::ItemFn) { // Check if function returns Result type @@ -317,7 +374,7 @@ impl PatternEngine { let _span = ok_expr.span(); // Use a simple line-based location since proc_macro2::Span doesn't have start() method // Use a simple line-based location since proc_macro2::Span doesn't have start() method - let (line, col, context) = (1, 1, String::new()); + let (line, col, context) = (1, 1, String::new()); self.matches.push((line, col, context)); } } @@ -325,19 +382,20 @@ impl PatternEngine { syn::visit::visit_item_fn(self, func); } } - + impl EmptyOkVisitor { fn is_result_type(&self, ty: &syn::Type) -> bool { match ty { - syn::Type::Path(type_path) => { - type_path.path.segments.last() - .map(|seg| seg.ident == "Result") - .unwrap_or(false) - } - _ => false + syn::Type::Path(type_path) => type_path + .path + .segments + .last() + .map(|seg| seg.ident == "Result") + .unwrap_or(false), + _ => false, } } - + fn find_ok_unit_return<'b>(&self, block: &'b syn::Block) -> Option<&'b syn::Expr> { // Look for a block with just one statement that returns Ok(()) if block.stmts.len() == 1 { @@ -349,13 +407,19 @@ impl PatternEngine { } None } - + fn is_ok_unit_expr(&self, expr: &syn::Expr) -> bool { match expr { syn::Expr::Call(call) => { // Check if it's Ok(()) if let syn::Expr::Path(path) = &*call.func { - if path.path.segments.last().map(|seg| seg.ident == "Ok").unwrap_or(false) { + if path + .path + .segments + .last() + .map(|seg| seg.ident == "Ok") + .unwrap_or(false) + { // Check if argument is unit type () if call.args.len() == 1 { if let syn::Expr::Tuple(tuple) = &call.args[0] { @@ -370,21 +434,21 @@ impl PatternEngine { false } } - + let mut visitor = EmptyOkVisitor { matches: Vec::new(), }; - + visitor.visit_file(syntax_tree); visitor.matches } - + /// Get line and column number from byte offset in content fn get_match_location(&self, content: &str, byte_offset: usize) -> (u32, u32, String) { let mut line = 1; let mut col = 1; let mut line_start = 0; - + for (i, ch) in content.char_indices() { if i >= byte_offset { break; @@ -397,32 +461,33 @@ impl PatternEngine { col += 1; } } - + // Extract context line - let line_end = content[line_start..].find('\n') + let line_end = content[line_start..] + .find('\n') .map(|pos| line_start + pos) .unwrap_or(content.len()); - + let context = content[line_start..line_end].trim().to_string(); - + (line, col, context) } - + /// Check if a regex match should be excluded based on conditions fn should_exclude_match( - &self, - conditions: Option<&ExcludeConditions>, - file_path: &Path, + &self, + conditions: Option<&ExcludeConditions>, + file_path: &Path, _matched_text: &str, _content: &str, - _offset: usize + _offset: usize, ) -> bool { if let Some(conditions) = conditions { // Check if in test files if conditions.in_tests && self.is_test_file(file_path) { return true; } - + // Check file patterns if let Some(patterns) = &conditions.file_patterns { for pattern in patterns { @@ -433,27 +498,27 @@ impl PatternEngine { } } } - + // TODO: Check attributes and other conditions when we have AST context } - + false } - + /// Check if an AST match should be excluded fn should_exclude_ast_match( - &self, - conditions: Option<&ExcludeConditions>, - file_path: &Path, - _syntax_tree: &syn::File, - _line: u32 + &self, + conditions: Option<&ExcludeConditions>, + file_path: &Path, + _syntax_tree: &syn::File, + _line: u32, ) -> bool { if let Some(conditions) = conditions { // Check if in test files if conditions.in_tests && self.is_test_file(file_path) { return true; } - + // Check file patterns if let Some(patterns) = &conditions.file_patterns { for pattern in patterns { @@ -464,47 +529,48 @@ impl PatternEngine { } } } - + // TODO: Check for specific attributes like #[test] on functions } - + false } - + /// Check if a file path indicates it's a test file fn is_test_file(&self, file_path: &Path) -> bool { file_path.components().any(|component| { - component.as_os_str().to_str() + component + .as_os_str() + .to_str() .map(|s| s == "tests" || s == "test") .unwrap_or(false) - }) || file_path.file_name() + }) || file_path + .file_name() .and_then(|name| name.to_str()) .map(|name| name.contains("test") || name.starts_with("test_")) .unwrap_or(false) } - + /// Convert pattern matches to violations pub fn matches_to_violations(&self, matches: Vec) -> Vec { - matches.into_iter().map(|m| { - let mut violation = Violation::new( - m.rule_id, - m.severity, - m.file_path, - m.message, - ); - - if let Some(line) = m.line_number { - if let Some(col) = m.column_number { - violation = violation.with_position(line, col); + matches + .into_iter() + .map(|m| { + let mut violation = Violation::new(m.rule_id, m.severity, m.file_path, m.message); + + if let Some(line) = m.line_number { + if let Some(col) = m.column_number { + violation = violation.with_position(line, col); + } } - } - - if let Some(context) = m.context { - violation = violation.with_context(context); - } - - violation - }).collect() + + if let Some(context) = m.context { + violation = violation.with_context(context); + } + + violation + }) + .collect() } } @@ -518,11 +584,11 @@ impl Default for PatternEngine { mod tests { use super::*; use crate::config::PatternRule; - + #[test] fn test_regex_pattern() { let mut engine = PatternEngine::new(); - + let rule = PatternRule { id: "todo_test".to_string(), rule_type: RuleType::Regex, @@ -533,22 +599,22 @@ mod tests { case_sensitive: true, exclude_if: None, }; - + engine.add_rule(&rule, Severity::Warning).unwrap(); - + let content = "// TODO: implement this\nlet x = 5;"; let matches = engine.analyze_file(Path::new("test.rs"), content).unwrap(); - + assert_eq!(matches.len(), 1); assert_eq!(matches[0].rule_id, "todo_test"); assert_eq!(matches[0].matched_text, "TODO"); assert_eq!(matches[0].line_number, Some(1)); } - + #[test] fn test_macro_ast_pattern() { let mut engine = PatternEngine::new(); - + let rule = PatternRule { id: "unimplemented_test".to_string(), rule_type: RuleType::Ast, @@ -559,21 +625,21 @@ mod tests { case_sensitive: true, exclude_if: None, }; - + engine.add_rule(&rule, Severity::Error).unwrap(); - + let content = "fn test() {\n unimplemented!()\n}"; let matches = engine.analyze_file(Path::new("test.rs"), content).unwrap(); - + assert_eq!(matches.len(), 1); assert_eq!(matches[0].rule_id, "unimplemented_test"); assert!(matches[0].message.contains("unimplemented")); } - + #[test] fn test_exclude_conditions() { let mut engine = PatternEngine::new(); - + let rule = PatternRule { id: "todo_test".to_string(), rule_type: RuleType::Regex, @@ -588,17 +654,21 @@ mod tests { file_patterns: None, }), }; - + engine.add_rule(&rule, Severity::Warning).unwrap(); - + let content = "// TODO: implement this"; - + // Should match in regular file - let matches = engine.analyze_file(Path::new("src/lib.rs"), content).unwrap(); + let matches = engine + .analyze_file(Path::new("src/lib.rs"), content) + .unwrap(); assert_eq!(matches.len(), 1); - + // Should be excluded in test file - let matches = engine.analyze_file(Path::new("tests/unit.rs"), content).unwrap(); + let matches = engine + .analyze_file(Path::new("tests/unit.rs"), content) + .unwrap(); assert_eq!(matches.len(), 0); } -} \ No newline at end of file +} diff --git a/src/patterns/path_filter.rs b/src/patterns/path_filter.rs index 1b723ac..90ef762 100644 --- a/src/patterns/path_filter.rs +++ b/src/patterns/path_filter.rs @@ -1,13 +1,13 @@ //! Path filtering using .gitignore-style patterns -//! +//! //! CDD Principle: Domain Services - PathFilter orchestrates complex path matching logic //! - Encapsulates the rules for include/exclude pattern evaluation //! - Provides clean interface for determining whether a path should be analyzed //! - Handles .guardianignore file discovery and parsing use crate::domain::violations::{GuardianError, GuardianResult}; -use std::path::{Path, PathBuf}; use std::fs; +use std::path::{Path, PathBuf}; use walkdir::WalkDir; /// Manages path filtering using .gitignore-style patterns @@ -36,66 +36,70 @@ impl PathFilter { /// Create a new path filter with the given patterns pub fn new(patterns: Vec, ignore_filename: Option) -> GuardianResult { let mut filter_patterns = Vec::new(); - + for pattern_str in patterns { let (is_include, pattern_str) = if let Some(stripped) = pattern_str.strip_prefix('!') { (true, stripped.to_string()) } else { (false, pattern_str) }; - - let pattern = glob::Pattern::new(&pattern_str) - .map_err(|e| GuardianError::pattern(format!("Invalid pattern '{}': {}", pattern_str, e)))?; - + + let pattern = glob::Pattern::new(&pattern_str).map_err(|e| { + GuardianError::pattern(format!("Invalid pattern '{}': {}", pattern_str, e)) + })?; + filter_patterns.push(FilterPattern { pattern, is_include, original: pattern_str, }); } - + Ok(Self { patterns: filter_patterns, process_ignore_files: ignore_filename.is_some(), ignore_filename: ignore_filename.unwrap_or_else(|| ".guardianignore".to_string()), }) } - + /// Create a default path filter with sensible exclusions pub fn default() -> GuardianResult { - Self::new(vec![ - // Exclude common build/cache directories - "target/**".to_string(), - "**/node_modules/**".to_string(), - "**/.git/**".to_string(), - "**/*.generated.*".to_string(), - "**/dist/**".to_string(), - "**/build/**".to_string(), - ], Some(".guardianignore".to_string())) + Self::new( + vec![ + // Exclude common build/cache directories + "target/**".to_string(), + "**/node_modules/**".to_string(), + "**/.git/**".to_string(), + "**/*.generated.*".to_string(), + "**/dist/**".to_string(), + "**/build/**".to_string(), + ], + Some(".guardianignore".to_string()), + ) } - + /// Check if a file should be analyzed based on all patterns and ignore files pub fn should_analyze>(&self, path: P) -> GuardianResult { let path = path.as_ref(); let _path_str = path.to_string_lossy(); - + // Start with default: include all files let mut should_include = true; - + // Apply patterns in order (like .gitignore) for pattern in &self.patterns { let matches = self.pattern_matches_path(pattern, path); - + if matches { should_include = pattern.is_include; } } - + // If excluded by configured patterns, return false if !should_include { return Ok(false); } - + // Check .guardianignore files if enabled if self.process_ignore_files { let ignored_by_files = self.is_ignored_by_files(path)?; @@ -103,64 +107,68 @@ impl PathFilter { return Ok(false); } } - + Ok(true) } - + /// Check if path is ignored by .guardianignore files fn is_ignored_by_files>(&self, path: P) -> GuardianResult { let path = path.as_ref(); let mut current_dir = path.parent(); let mut is_ignored = false; - + // Walk up the directory tree looking for .guardianignore files while let Some(dir) = current_dir { let ignore_file = dir.join(&self.ignore_filename); - + if ignore_file.exists() { let patterns = self.load_ignore_file(&ignore_file)?; - + // Check if any pattern in this file matches for pattern in patterns { // Make path relative to the ignore file's directory if let Ok(relative_path) = path.strip_prefix(dir) { let matches = self.pattern_matches_path(&pattern, relative_path); - + if matches { is_ignored = !pattern.is_include; } } } } - + current_dir = dir.parent(); } - + Ok(is_ignored) } - + /// Load patterns from a .guardianignore file fn load_ignore_file>(&self, path: P) -> GuardianResult> { - let content = fs::read_to_string(&path) - .map_err(|e| GuardianError::config(format!("Failed to read ignore file '{}': {}", - path.as_ref().display(), e)))?; - + let content = fs::read_to_string(&path).map_err(|e| { + GuardianError::config(format!( + "Failed to read ignore file '{}': {}", + path.as_ref().display(), + e + )) + })?; + let mut patterns = Vec::new(); - + for line in content.lines() { let line = line.trim(); - + // Skip empty lines and comments if line.is_empty() || line.starts_with('#') { continue; } - + let (is_include, pattern_str) = if let Some(stripped) = line.strip_prefix('!') { (true, stripped.to_string()) } else { (false, line.to_string()) }; - + match glob::Pattern::new(&pattern_str) { Ok(pattern) => { patterns.push(FilterPattern { @@ -172,30 +180,30 @@ impl PathFilter { Err(e) => { // Log warning but don't fail - just skip invalid patterns tracing::warn!( - "Invalid pattern '{}' in {}: {}", - pattern_str, - path.as_ref().display(), + "Invalid pattern '{}' in {}: {}", + pattern_str, + path.as_ref().display(), e ); } } } - + Ok(patterns) } - + /// Get all files that should be analyzed in a directory tree pub fn find_files>(&self, root: P) -> GuardianResult> { let root = root.as_ref(); let mut files = Vec::new(); - + for entry in WalkDir::new(root) .follow_links(false) .into_iter() .filter_map(|e| e.ok()) { let path = entry.path(); - + // Only process files, not directories if path.is_file() { if self.should_analyze(path)? { @@ -203,23 +211,23 @@ impl PathFilter { } } } - + Ok(files) } - + /// Filter a list of paths to only those that should be analyzed pub fn filter_paths>(&self, paths: &[P]) -> GuardianResult> { let mut filtered = Vec::new(); - + for path in paths { if self.should_analyze(path)? { filtered.push(path.as_ref().to_path_buf()); } } - + Ok(filtered) } - + /// Add a pattern to the filter pub fn add_pattern(&mut self, pattern: String) -> GuardianResult<()> { let (is_include, pattern_str) = if let Some(stripped) = pattern.strip_prefix('!') { @@ -227,28 +235,29 @@ impl PathFilter { } else { (false, pattern) }; - - let glob_pattern = glob::Pattern::new(&pattern_str) - .map_err(|e| GuardianError::pattern(format!("Invalid pattern '{}': {}", pattern_str, e)))?; - + + let glob_pattern = glob::Pattern::new(&pattern_str).map_err(|e| { + GuardianError::pattern(format!("Invalid pattern '{}': {}", pattern_str, e)) + })?; + self.patterns.push(FilterPattern { pattern: glob_pattern, is_include, original: pattern_str, }); - + Ok(()) } - + /// Get debug information about patterns and their matches pub fn debug_patterns>(&self, path: P) -> Vec { let path = path.as_ref(); let mut debug_info = Vec::new(); - + for (i, pattern) in self.patterns.iter().enumerate() { let matches = self.pattern_matches_path(pattern, path); let prefix = if pattern.is_include { "!" } else { "" }; - + debug_info.push(format!( "Pattern {}: {}{} -> {}", i, @@ -257,17 +266,16 @@ impl PathFilter { if matches { "MATCH" } else { "no match" } )); } - + debug_info } - + /// Check if a pattern matches a path using .gitignore-style rules fn pattern_matches_path(&self, pattern: &FilterPattern, path: &Path) -> bool { let path_str = path.to_string_lossy(); - + // Handle different pattern types - if pattern.original.ends_with('/') - { + if pattern.original.ends_with('/') { // Directory pattern - only match directories if !path.is_dir() { return false; @@ -278,15 +286,18 @@ impl PathFilter { .map(|p| p.matches(&path_str)) .unwrap_or(false); } - + if pattern.original.starts_with('/') { // Absolute pattern from root - remove leading slash and match from beginning - let absolute_pattern = pattern.original.strip_prefix('/').unwrap_or(&pattern.original); + let absolute_pattern = pattern + .original + .strip_prefix('/') + .unwrap_or(&pattern.original); return glob::Pattern::new(absolute_pattern) .map(|p| p.matches(&path_str)) .unwrap_or(false); } - + if pattern.original.contains('/') { // Pattern contains slash - match full path return pattern.pattern.matches(&path_str); @@ -296,7 +307,7 @@ impl PathFilter { return pattern.pattern.matches(&filename.to_string_lossy()); } } - + false } } @@ -306,119 +317,144 @@ mod tests { use super::*; use std::fs; use tempfile::TempDir; - + #[test] fn test_basic_pattern_matching() { - let filter = PathFilter::new(vec![ - "target/**".to_string(), // Exclude target directory - "*.md".to_string(), // Exclude markdown files - ], None).unwrap(); - + let filter = PathFilter::new( + vec![ + "target/**".to_string(), // Exclude target directory + "*.md".to_string(), // Exclude markdown files + ], + None, + ) + .unwrap(); + assert!(filter.should_analyze(Path::new("src/lib.rs")).unwrap()); - assert!(!filter.should_analyze(Path::new("target/debug/lib.rs")).unwrap()); + assert!(!filter + .should_analyze(Path::new("target/debug/lib.rs")) + .unwrap()); assert!(!filter.should_analyze(Path::new("README.md")).unwrap()); } - + #[test] fn test_include_override() { - let filter = PathFilter::new(vec![ - "target/**".to_string(), // Exclude target - "!target/special/**".to_string(), // But include target/special - ], None).unwrap(); - - assert!(!filter.should_analyze(Path::new("target/debug/lib.rs")).unwrap()); - assert!(filter.should_analyze(Path::new("target/special/lib.rs")).unwrap()); + let filter = PathFilter::new( + vec![ + "target/**".to_string(), // Exclude target + "!target/special/**".to_string(), // But include target/special + ], + None, + ) + .unwrap(); + + assert!(!filter + .should_analyze(Path::new("target/debug/lib.rs")) + .unwrap()); + assert!(filter + .should_analyze(Path::new("target/special/lib.rs")) + .unwrap()); } - + #[test] fn test_pattern_order() { - let filter = PathFilter::new(vec![ - "tests/**".to_string(), // Exclude tests - "!tests/important.rs".to_string(), // But include important test - "!*.rs".to_string(), // And include all .rs files (overrides excludes) - ], None).unwrap(); - + let filter = PathFilter::new( + vec![ + "tests/**".to_string(), // Exclude tests + "!tests/important.rs".to_string(), // But include important test + "!*.rs".to_string(), // And include all .rs files (overrides excludes) + ], + None, + ) + .unwrap(); + assert!(filter.should_analyze(Path::new("src/lib.rs")).unwrap()); assert!(filter.should_analyze(Path::new("tests/unit.rs")).unwrap()); - assert!(filter.should_analyze(Path::new("tests/important.rs")).unwrap()); + assert!(filter + .should_analyze(Path::new("tests/important.rs")) + .unwrap()); } - + #[test] fn test_guardianignore_file() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let root = temp_dir.path(); - + // Create directory structure fs::create_dir_all(root.join("src"))?; fs::create_dir_all(root.join("tests"))?; - + // Create .guardianignore file fs::write( root.join(".guardianignore"), - "*.tmp\ntests/**\n!tests/important.rs\n" + "*.tmp\ntests/**\n!tests/important.rs\n", )?; - + // Create test files fs::write(root.join("src/lib.rs"), "")?; fs::write(root.join("temp.tmp"), "")?; fs::write(root.join("tests/unit.rs"), "")?; fs::write(root.join("tests/important.rs"), "")?; - - let filter = PathFilter::new(vec![], Some(".guardianignore".to_string()))?; // No initial patterns - + + let filter = PathFilter::new(vec![], Some(".guardianignore".to_string()))?; // No initial patterns + assert!(filter.should_analyze(root.join("src/lib.rs"))?); - assert!(!filter.should_analyze(root.join("temp.tmp"))?); // Excluded by ignore file - assert!(!filter.should_analyze(root.join("tests/unit.rs"))?); // Excluded by ignore file - assert!(filter.should_analyze(root.join("tests/important.rs"))?); // Included by override in ignore file - + assert!(!filter.should_analyze(root.join("temp.tmp"))?); // Excluded by ignore file + assert!(!filter.should_analyze(root.join("tests/unit.rs"))?); // Excluded by ignore file + assert!(filter.should_analyze(root.join("tests/important.rs"))?); // Included by override in ignore file + Ok(()) } - + #[test] fn test_find_files() -> GuardianResult<()> { let temp_dir = TempDir::new().unwrap(); let root = temp_dir.path(); - + // Create directory structure fs::create_dir_all(root.join("src"))?; fs::create_dir_all(root.join("target/debug"))?; - + // Create test files fs::write(root.join("src/lib.rs"), "")?; fs::write(root.join("src/main.rs"), "")?; fs::write(root.join("target/debug/app"), "")?; fs::write(root.join("README.md"), "")?; - - let filter = PathFilter::new(vec![ - "target/**".to_string(), // Exclude target directory - "*.md".to_string(), // Exclude markdown files - ], None)?; - + + let filter = PathFilter::new( + vec![ + "target/**".to_string(), // Exclude target directory + "*.md".to_string(), // Exclude markdown files + ], + None, + )?; + let files = filter.find_files(root)?; - - // Should find Rust files but not target or README + + // Should find Rust files but not target or README // The binary file "app" should also be found since it doesn't match our exclude patterns assert_eq!(files.len(), 3); // lib.rs, main.rs, and app assert!(files.iter().any(|p| p.file_name().unwrap() == "lib.rs")); assert!(files.iter().any(|p| p.file_name().unwrap() == "main.rs")); - + Ok(()) } - + #[test] fn test_invalid_pattern() { let result = PathFilter::new(vec!["[invalid".to_string()], None); assert!(result.is_err()); } - + #[test] fn test_default_filter() { let filter = PathFilter::default().unwrap(); - + // Should exclude target directory - assert!(!filter.should_analyze(Path::new("target/debug/lib.rs")).unwrap()); - + assert!(!filter + .should_analyze(Path::new("target/debug/lib.rs")) + .unwrap()); + // Should include regular source files assert!(filter.should_analyze(Path::new("src/lib.rs")).unwrap()); } -} \ No newline at end of file +} diff --git a/src/report/mod.rs b/src/report/mod.rs index 8cd7d7d..98f38a9 100644 --- a/src/report/mod.rs +++ b/src/report/mod.rs @@ -1,11 +1,11 @@ //! Report generation with multiple output formats -//! +//! //! CDD Principle: Anti-Corruption Layer - Formatters translate domain objects to external formats //! - ValidationReport (domain) is converted to various external representations //! - Each formatter encapsulates the rules for its specific output format //! - Domain logic remains pure while supporting multiple presentation needs -use crate::domain::violations::{ValidationReport, Violation, Severity, GuardianResult}; +use crate::domain::violations::{GuardianResult, Severity, ValidationReport, Violation}; use serde_json::Value as JsonValue; use std::io::Write; @@ -36,7 +36,7 @@ impl OutputFormat { _ => None, } } - + /// Get all available format names pub fn all_formats() -> &'static [&'static str] { &["human", "json", "junit", "sarif", "github"] @@ -80,17 +80,21 @@ impl ReportFormatter { pub fn new(options: ReportOptions) -> Self { Self { options } } - + /// Create a formatter with default options pub fn default() -> Self { Self::new(ReportOptions::default()) } - + /// Format a validation report in the specified format - pub fn format_report(&self, report: &ValidationReport, format: OutputFormat) -> GuardianResult { + pub fn format_report( + &self, + report: &ValidationReport, + format: OutputFormat, + ) -> GuardianResult { // Filter violations based on options let filtered_violations = self.filter_violations(&report.violations); - + match format { OutputFormat::Human => self.format_human(report, &filtered_violations), OutputFormat::Json => self.format_json(report, &filtered_violations), @@ -99,23 +103,25 @@ impl ReportFormatter { OutputFormat::GitHub => self.format_github(report, &filtered_violations), } } - + /// Write a formatted report to a writer pub fn write_report( - &self, - report: &ValidationReport, - format: OutputFormat, - mut writer: W + &self, + report: &ValidationReport, + format: OutputFormat, + mut writer: W, ) -> GuardianResult<()> { let formatted = self.format_report(report, format)?; - writer.write_all(formatted.as_bytes()) + writer + .write_all(formatted.as_bytes()) .map_err(|e| crate::domain::violations::GuardianError::Io { source: e })?; Ok(()) } - + /// Filter violations based on report options fn filter_violations<'a>(&self, violations: &'a [Violation]) -> Vec<&'a Violation> { - let mut filtered: Vec<&Violation> = violations.iter() + let mut filtered: Vec<&Violation> = violations + .iter() .filter(|v| { // Filter by minimum severity if let Some(min_severity) = self.options.min_severity { @@ -126,19 +132,23 @@ impl ReportFormatter { true }) .collect(); - + // Limit number of violations if requested if let Some(max) = self.options.max_violations { filtered.truncate(max); } - + filtered } - + /// Format report in human-readable format - fn format_human(&self, report: &ValidationReport, violations: &[&Violation]) -> GuardianResult { + fn format_human( + &self, + report: &ValidationReport, + violations: &[&Violation], + ) -> GuardianResult { let mut output = String::new(); - + if violations.is_empty() { if self.options.use_colors { output.push_str("✅ \x1b[32mNo code quality violations found\x1b[0m\n"); @@ -150,37 +160,43 @@ impl ReportFormatter { let icon = if report.has_errors() { "❌" } else { "⚠️" }; if self.options.use_colors { let color = if report.has_errors() { "31" } else { "33" }; - output.push_str(&format!("{} \x1b[{}mCode Quality Violations Found\x1b[0m\n\n", icon, color)); + output.push_str(&format!( + "{} \x1b[{}mCode Quality Violations Found\x1b[0m\n\n", + icon, color + )); } else { output.push_str(&format!("{} Code Quality Violations Found\n\n", icon)); } - + // Group violations by file - let mut by_file: std::collections::BTreeMap<&std::path::Path, Vec<&Violation>> = + let mut by_file: std::collections::BTreeMap<&std::path::Path, Vec<&Violation>> = std::collections::BTreeMap::new(); - + for violation in violations { - by_file.entry(&violation.file_path).or_default().push(violation); + by_file + .entry(&violation.file_path) + .or_default() + .push(violation); } - + // Display each file's violations for (file_path, file_violations) in by_file { output.push_str(&format!("📁 {}\n", file_path.display())); - + for violation in file_violations { // Format violation with colors let severity_color = match violation.severity { - Severity::Error => "31", // Red + Severity::Error => "31", // Red Severity::Warning => "33", // Yellow - Severity::Info => "36", // Cyan + Severity::Info => "36", // Cyan }; - + let position = match (violation.line_number, violation.column_number) { (Some(line), Some(col)) => format!("{}:{}", line, col), (Some(line), None) => line.to_string(), _ => "?".to_string(), }; - + if self.options.use_colors { output.push_str(&format!( " \x1b[{}m{}:{}\x1b[0m [\x1b[{}m{}\x1b[0m] {}\n", @@ -200,7 +216,7 @@ impl ReportFormatter { violation.message )); } - + // Show context if available and requested if self.options.show_context { if let Some(context) = &violation.context { @@ -211,7 +227,7 @@ impl ReportFormatter { } } } - + // Show suggestions if available and requested if self.options.show_suggestions { if let Some(suggestion) = &violation.suggested_fix { @@ -222,34 +238,41 @@ impl ReportFormatter { } } } - + output.push('\n'); } } } - + // Summary output.push_str(&self.format_summary(report)); - + Ok(output) } - + /// Format report in JSON format - fn format_json(&self, report: &ValidationReport, violations: &[&Violation]) -> GuardianResult { - let json_violations: Vec = violations.iter().map(|v| { - serde_json::json!({ - "rule_id": v.rule_id, - "severity": v.severity.as_str(), - "file_path": v.file_path.display().to_string(), - "line_number": v.line_number, - "column_number": v.column_number, - "message": v.message, - "context": v.context, - "suggested_fix": v.suggested_fix, - "detected_at": v.detected_at.to_rfc3339() + fn format_json( + &self, + report: &ValidationReport, + violations: &[&Violation], + ) -> GuardianResult { + let json_violations: Vec = violations + .iter() + .map(|v| { + serde_json::json!({ + "rule_id": v.rule_id, + "severity": v.severity.as_str(), + "file_path": v.file_path.display().to_string(), + "line_number": v.line_number, + "column_number": v.column_number, + "message": v.message, + "context": v.context, + "suggested_fix": v.suggested_fix, + "detected_at": v.detected_at.to_rfc3339() + }) }) - }).collect(); - + .collect(); + let json_report = serde_json::json!({ "violations": json_violations, "summary": { @@ -264,33 +287,44 @@ impl ReportFormatter { }, "config_fingerprint": report.config_fingerprint }); - - serde_json::to_string_pretty(&json_report) - .map_err(|e| crate::domain::violations::GuardianError::config(format!("JSON serialization failed: {}", e))) + + serde_json::to_string_pretty(&json_report).map_err(|e| { + crate::domain::violations::GuardianError::config(format!( + "JSON serialization failed: {}", + e + )) + }) } - + /// Format report in JUnit XML format - fn format_junit(&self, report: &ValidationReport, violations: &[&Violation]) -> GuardianResult { + fn format_junit( + &self, + report: &ValidationReport, + violations: &[&Violation], + ) -> GuardianResult { let mut xml = String::new(); xml.push_str("\n"); - + let total_tests = violations.len(); - let failures = violations.iter().filter(|v| v.severity == Severity::Error).count(); + let failures = violations + .iter() + .filter(|v| v.severity == Severity::Error) + .count(); let errors = 0; // We don't distinguish between failures and errors for now let execution_time = (report.summary.execution_time_ms as f64) / 1000.0; - + xml.push_str(&format!( "\n", total_tests, failures, errors, execution_time )); - + for violation in violations { xml.push_str(&format!( " \n", violation.rule_id, escape_xml(&violation.file_path.display().to_string()) )); - + if violation.severity == Severity::Error { xml.push_str(&format!( " \n", @@ -307,48 +341,55 @@ impl ReportFormatter { } xml.push_str(" \n"); } - + xml.push_str(" \n"); } - + xml.push_str("\n"); Ok(xml) } - + /// Format report in SARIF format - fn format_sarif(&self, _report: &ValidationReport, violations: &[&Violation]) -> GuardianResult { - let sarif_results: Vec = violations.iter().map(|v| { - let level = match v.severity { - Severity::Error => "error", - Severity::Warning => "warning", - Severity::Info => "note", - }; - - serde_json::json!({ - "ruleId": v.rule_id, - "level": level, - "message": { - "text": v.message - }, - "locations": [{ - "physicalLocation": { - "artifactLocation": { - "uri": v.file_path.display().to_string() - }, - "region": { - "startLine": v.line_number.unwrap_or(1), - "startColumn": v.column_number.unwrap_or(1) - }, - "contextRegion": v.context.as_ref().map(|c| serde_json::json!({ - "snippet": { - "text": c - } - })) - } - }] + fn format_sarif( + &self, + _report: &ValidationReport, + violations: &[&Violation], + ) -> GuardianResult { + let sarif_results: Vec = violations + .iter() + .map(|v| { + let level = match v.severity { + Severity::Error => "error", + Severity::Warning => "warning", + Severity::Info => "note", + }; + + serde_json::json!({ + "ruleId": v.rule_id, + "level": level, + "message": { + "text": v.message + }, + "locations": [{ + "physicalLocation": { + "artifactLocation": { + "uri": v.file_path.display().to_string() + }, + "region": { + "startLine": v.line_number.unwrap_or(1), + "startColumn": v.column_number.unwrap_or(1) + }, + "contextRegion": v.context.as_ref().map(|c| serde_json::json!({ + "snippet": { + "text": c + } + })) + } + }] + }) }) - }).collect(); - + .collect(); + let sarif_report = serde_json::json!({ "version": "2.1.0", "$schema": "https://json.schemastore.org/sarif-2.1.0.json", @@ -363,34 +404,42 @@ impl ReportFormatter { "results": sarif_results }] }); - - serde_json::to_string_pretty(&sarif_report) - .map_err(|e| crate::domain::violations::GuardianError::config(format!("SARIF serialization failed: {}", e))) + + serde_json::to_string_pretty(&sarif_report).map_err(|e| { + crate::domain::violations::GuardianError::config(format!( + "SARIF serialization failed: {}", + e + )) + }) } - + /// Format report for GitHub Actions - fn format_github(&self, _report: &ValidationReport, violations: &[&Violation]) -> GuardianResult { + fn format_github( + &self, + _report: &ValidationReport, + violations: &[&Violation], + ) -> GuardianResult { let mut output = String::new(); - + for violation in violations { let level = match violation.severity { Severity::Error => "error", Severity::Warning => "warning", Severity::Info => "notice", }; - + let position = match (violation.line_number, violation.column_number) { (Some(line), Some(col)) => format!("line={},col={}", line, col), (Some(line), None) => format!("line={}", line), _ => String::new(), }; - - let position_part = if position.is_empty() { - String::new() - } else { - format!(" {}", position) + + let position_part = if position.is_empty() { + String::new() + } else { + format!(" {}", position) }; - + output.push_str(&format!( "::{} file={},title={}{}::{}\n", level, @@ -400,23 +449,23 @@ impl ReportFormatter { violation.message )); } - + Ok(output) } - + /// Format the summary section fn format_summary(&self, report: &ValidationReport) -> String { let mut summary = String::new(); - + let total_violations = report.summary.violations_by_severity.total(); let execution_time = (report.summary.execution_time_ms as f64) / 1000.0; - + if self.options.use_colors { summary.push_str("📊 \x1b[1mSummary:\x1b[0m "); } else { summary.push_str("📊 Summary: "); } - + if total_violations == 0 { if self.options.use_colors { summary.push_str(&format!( @@ -431,11 +480,16 @@ impl ReportFormatter { } } else { let mut parts = Vec::new(); - + if report.summary.violations_by_severity.error > 0 { - let text = format!("{} error{}", + let text = format!( + "{} error{}", report.summary.violations_by_severity.error, - if report.summary.violations_by_severity.error == 1 { "" } else { "s" } + if report.summary.violations_by_severity.error == 1 { + "" + } else { + "s" + } ); if self.options.use_colors { parts.push(format!("\x1b[31m{}\x1b[0m", text)); @@ -443,11 +497,16 @@ impl ReportFormatter { parts.push(text); } } - + if report.summary.violations_by_severity.warning > 0 { - let text = format!("{} warning{}", + let text = format!( + "{} warning{}", report.summary.violations_by_severity.warning, - if report.summary.violations_by_severity.warning == 1 { "" } else { "s" } + if report.summary.violations_by_severity.warning == 1 { + "" + } else { + "s" + } ); if self.options.use_colors { parts.push(format!("\x1b[33m{}\x1b[0m", text)); @@ -455,7 +514,7 @@ impl ReportFormatter { parts.push(text); } } - + if report.summary.violations_by_severity.info > 0 { let text = format!("{} info", report.summary.violations_by_severity.info); if self.options.use_colors { @@ -464,7 +523,7 @@ impl ReportFormatter { parts.push(text); } } - + summary.push_str(&format!( "{} in {} files ({:.1}s)\n", parts.join(", "), @@ -472,7 +531,7 @@ impl ReportFormatter { execution_time )); } - + summary } } @@ -491,123 +550,131 @@ mod tests { use super::*; use std::path::PathBuf; // Test imports - unused - + fn create_test_report() -> ValidationReport { let mut report = ValidationReport::new(); - + report.add_violation( crate::domain::violations::Violation::new( "test_rule", Severity::Error, PathBuf::from("src/main.rs"), - "Test violation" + "Test violation", ) .with_position(42, 15) - .with_context("let x = todo!();") + .with_context("let x = todo!();"), ); - + report.set_files_analyzed(10); report.set_execution_time(1200); - + report } - + #[test] fn test_human_format() { let formatter = ReportFormatter::new(ReportOptions { use_colors: false, ..Default::default() }); - + let report = create_test_report(); - let output = formatter.format_report(&report, OutputFormat::Human).unwrap(); - + let output = formatter + .format_report(&report, OutputFormat::Human) + .unwrap(); + assert!(output.contains("Code Quality Violations Found")); assert!(output.contains("src/main.rs")); assert!(output.contains("Test violation")); assert!(output.contains("Summary:")); } - + #[test] fn test_json_format() { let formatter = ReportFormatter::default(); let report = create_test_report(); - let output = formatter.format_report(&report, OutputFormat::Json).unwrap(); - + let output = formatter + .format_report(&report, OutputFormat::Json) + .unwrap(); + let json: JsonValue = serde_json::from_str(&output).unwrap(); assert!(json["violations"].is_array()); assert_eq!(json["violations"].as_array().unwrap().len(), 1); assert_eq!(json["violations"][0]["rule_id"], "test_rule"); assert_eq!(json["summary"]["total_files"], 10); } - + #[test] fn test_junit_format() { let formatter = ReportFormatter::default(); let report = create_test_report(); - let output = formatter.format_report(&report, OutputFormat::Junit).unwrap(); - + let output = formatter + .format_report(&report, OutputFormat::Junit) + .unwrap(); + assert!(output.contains("