diff --git a/.github/mlc_config.json b/.github/mlc_config.json index d74c272..345b853 100644 --- a/.github/mlc_config.json +++ b/.github/mlc_config.json @@ -5,6 +5,18 @@ }, { "pattern": "^https://localhost" + }, + { + "pattern": "^https://crates\\.io/crates/rust-guardian" + }, + { + "pattern": "^https://crates\\.io/me" + }, + { + "pattern": "^https://crates\\.io/$" + }, + { + "pattern": "^CONTRIBUTING\\.md$" } ], "replacementPatterns": [], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea55fce..24accc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,9 @@ jobs: uses: dtolnay/rust-toolchain@nightly - name: Install minimal-versions - run: cargo install cargo-minimal-versions + run: | + cargo install cargo-minimal-versions + cargo install cargo-hack - name: Check minimal versions run: cargo minimal-versions check \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e35f542 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to Rust Guardian + +Thank you for your interest in contributing to Rust Guardian! + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/rust-guardian.git` +3. Create a new branch: `git checkout -b feature/your-feature-name` + +## Development Setup + +### Prerequisites + +- Rust 1.70 or higher +- Cargo (comes with Rust) + +### Building + +```bash +cargo build +``` + +### Running Tests + +```bash +cargo test +``` + +### Running Locally + +```bash +cargo run -- check src/ +``` + +## Code Quality + +Before submitting a pull request, make sure your code: + +1. Passes all tests: `cargo test` +2. Passes clippy checks: `cargo clippy --all-targets --all-features -- -D warnings` +3. Is properly formatted: `cargo fmt --all -- --check` + +## Submitting Changes + +1. Commit your changes with clear, descriptive commit messages +2. Push to your fork +3. Create a pull request against the `main` branch +4. Describe your changes in the PR description + +## Questions? + +If you have questions, please open an issue on GitHub. + +## License + +By contributing to Rust Guardian, you agree that your contributions will be licensed under the MIT License. diff --git a/src/analyzer/mod.rs b/src/analyzer/mod.rs index 66f011a..6450a1e 100644 --- a/src/analyzer/mod.rs +++ b/src/analyzer/mod.rs @@ -73,12 +73,14 @@ impl Analyzer { } 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 + )) + })?; } } @@ -92,7 +94,12 @@ impl Analyzer { 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, path_filter, rust_analyzer: RustAnalyzer::new() }) + Ok(Self { + config, + pattern_engine, + path_filter, + rust_analyzer: RustAnalyzer::new(), + }) } /// Create an analyzer with default configuration @@ -120,23 +127,29 @@ impl Analyzer { 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); } @@ -238,18 +251,20 @@ impl Analyzer { 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) { - Ok(file_violations) => { - if let Ok(mut v) = violations.lock() { - v.extend(file_violations); + 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); + } } - } - Err(e) => { - if let Ok(mut errs) = errors.lock() { - errs.push((file_path.clone(), e)); + Err(e) => { + if let Ok(mut errs) = errors.lock() { + errs.push((file_path.clone(), e)); + } } - } - }); + }); // Handle errors let errors = Arc::try_unwrap(errors) @@ -548,14 +563,20 @@ impl Analyzer { })?; // Test max_files limitation - let options = AnalysisOptions { max_files: Some(1), ..Default::default() }; + let options = AnalysisOptions { + max_files: Some(1), + ..Default::default() + }; let report = self.analyze_directory(root, &options)?; if report.summary.total_files != 1 { return Err(GuardianError::analysis( "validation".to_string(), - format!("Expected 1 file with max_files=1, got {}", report.summary.total_files), + format!( + "Expected 1 file with max_files=1, got {}", + report.summary.total_files + ), )); } diff --git a/src/analyzer/rust.rs b/src/analyzer/rust.rs index 0c0f968..1774c77 100644 --- a/src/analyzer/rust.rs +++ b/src/analyzer/rust.rs @@ -27,12 +27,18 @@ pub struct RustAnalyzer { impl RustAnalyzer { /// Create a new Rust analyzer with default settings pub fn new() -> Self { - Self { analyze_tests: false, check_quality_headers: true } + Self { + analyze_tests: false, + check_quality_headers: true, + } } /// Create a Rust analyzer that also analyzes test files pub fn with_tests() -> Self { - Self { analyze_tests: true, check_quality_headers: true } + Self { + analyze_tests: true, + check_quality_headers: true, + } } /// Find all unimplemented macros in the file @@ -157,7 +163,11 @@ impl FileAnalyzer for RustAnalyzer { } fn handles_file(&self, file_path: &Path) -> bool { - file_path.extension().and_then(|ext| ext.to_str()).map(|ext| ext == "rs").unwrap_or(false) + file_path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext == "rs") + .unwrap_or(false) } } @@ -276,18 +286,24 @@ impl EmptyOkReturnVisitor { 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) - } + 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) - } + syn::Type::Path(type_path) => type_path + .path + .segments + .last() + .map(|seg| seg.ident == "Option") + .unwrap_or(false), _ => false, } } @@ -309,7 +325,13 @@ impl EmptyOkReturnVisitor { if let syn::Expr::Call(call) = expr { // 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; @@ -338,7 +360,13 @@ impl EmptyOkReturnVisitor { if let syn::Expr::Call(call) = expr { // 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] { @@ -408,8 +436,10 @@ fn another_function() { let violations = self.analyze(Path::new("test.rs"), content)?; - let unimplemented_violations: Vec<_> = - violations.iter().filter(|v| v.rule_id.contains("unimplemented")).collect(); + let unimplemented_violations: Vec<_> = violations + .iter() + .filter(|v| v.rule_id.contains("unimplemented")) + .collect(); if unimplemented_violations.is_empty() { return Err(GuardianError::analysis( @@ -446,8 +476,10 @@ fn perform_operation() -> i32 { let violations = self.analyze(Path::new("test.rs"), content)?; - let empty_violations: Vec<_> = - violations.iter().filter(|v| v.rule_id == "empty_ok_return").collect(); + let empty_violations: Vec<_> = violations + .iter() + .filter(|v| v.rule_id == "empty_ok_return") + .collect(); if empty_violations.is_empty() { return Err(GuardianError::analysis( @@ -457,8 +489,9 @@ fn perform_operation() -> i32 { } // Verify it caught the empty function - let has_empty_function = - empty_violations.iter().any(|v| v.message.contains("empty_function")); + let has_empty_function = empty_violations + .iter() + .any(|v| v.message.contains("empty_function")); if !has_empty_function { return Err(GuardianError::analysis( @@ -502,8 +535,10 @@ mod tests { let violations = self.analyze(Path::new("test.rs"), content)?; - let unimplemented_violations: Vec<_> = - violations.iter().filter(|v| v.rule_id.contains("unimplemented")).collect(); + let unimplemented_violations: Vec<_> = violations + .iter() + .filter(|v| v.rule_id.contains("unimplemented")) + .collect(); // Should find exactly one violation (from regular_function) if unimplemented_violations.len() != 1 { @@ -534,8 +569,10 @@ fn main() { "#; let violations = self.analyze(Path::new("src/main.rs"), content_without_header)?; - let missing_header_violations: Vec<_> = - violations.iter().filter(|v| v.rule_id == "quality_header_missing").collect(); + let missing_header_violations: Vec<_> = violations + .iter() + .filter(|v| v.rule_id == "quality_header_missing") + .collect(); if missing_header_violations.is_empty() { return Err(GuardianError::analysis( @@ -559,8 +596,10 @@ fn main() { "#; let violations = self.analyze(Path::new("src/main.rs"), content_with_header)?; - let header_violations: Vec<_> = - violations.iter().filter(|v| v.rule_id == "quality_header_missing").collect(); + let header_violations: Vec<_> = violations + .iter() + .filter(|v| v.rule_id == "quality_header_missing") + .collect(); if !header_violations.is_empty() { return Err(GuardianError::analysis( @@ -583,7 +622,10 @@ fn main() { // This is acceptable behavior - the file would fail to compile anyway if !violations.is_empty() { // Log this as interesting but don't fail - pattern matching might still work - tracing::debug!("Found {} violations in invalid syntax file", violations.len()); + tracing::debug!( + "Found {} violations in invalid syntax file", + violations.len() + ); } Ok(()) diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 8de562d..f5d49a6 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -362,7 +362,12 @@ impl FileCache { impl Default for CacheMetadata { fn default() -> Self { let now = current_timestamp(); - Self { created_at: now, updated_at: now, hits: 0, misses: 0 } + Self { + created_at: now, + updated_at: now, + hits: 0, + misses: 0, + } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index bfdf889..03279bf 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -294,7 +294,9 @@ impl GuardianConfig { if rule.case_sensitive { regex::Regex::new(&rule.pattern) } else { - regex::RegexBuilder::new(&rule.pattern).case_insensitive(true).build() + regex::RegexBuilder::new(&rule.pattern) + .case_insensitive(true) + .build() } .map_err(|e| { GuardianError::config(format!( @@ -311,15 +313,16 @@ impl GuardianConfig { /// Get all enabled rules across all categories pub fn enabled_rules(&self) -> impl Iterator { - self.patterns.iter().filter(|(_, category)| category.enabled).flat_map( - |(name, category)| { + self.patterns + .iter() + .filter(|(_, category)| category.enabled) + .flat_map(|(name, category)| { category .rules .iter() .filter(|rule| rule.enabled) .map(move |rule| (name, category, rule)) - }, - ) + }) } /// Get effective severity for a rule (rule override or category default) @@ -412,7 +415,9 @@ pub struct ConfigBuilder { impl ConfigBuilder { /// Create a new builder with default configuration pub fn new() -> Self { - Self { config: GuardianConfig::default() } + Self { + config: GuardianConfig::default(), + } } /// Add a path pattern @@ -483,7 +488,11 @@ impl GuardianConfig { .ignore_file(".guardian_ignore") .build()?; - if !evolved_config.paths.patterns.contains(&"guardian/evolution/**".to_string()) { + if !evolved_config + .paths + .patterns + .contains(&"guardian/evolution/**".to_string()) + { return Err(GuardianError::config( "Configuration evolution failed to integrate new patterns".to_string(), )); diff --git a/src/domain/violations.rs b/src/domain/violations.rs index 4ca6661..3d437f2 100644 --- a/src/domain/violations.rs +++ b/src/domain/violations.rs @@ -181,7 +181,10 @@ impl ValidationReport { pub fn new() -> Self { Self { violations: Vec::new(), - summary: ValidationSummary { validated_at: Utc::now(), ..Default::default() }, + summary: ValidationSummary { + validated_at: Utc::now(), + ..Default::default() + }, config_fingerprint: None, } } @@ -204,7 +207,9 @@ impl ValidationReport { /// 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 @@ -281,27 +286,38 @@ 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(), + } } /// Create a validation error pub fn validation(message: impl Into) -> Self { - Self::Validation { message: message.into() } + Self::Validation { + message: message.into(), + } } } @@ -342,7 +358,10 @@ mod tests { assert_eq!(violation.line_number, Some(42)); assert_eq!(violation.column_number, Some(15)); - assert_eq!(violation.context, Some("let x = unimplemented!();".to_string())); + assert_eq!( + violation.context, + Some("let x = unimplemented!();".to_string()) + ); assert!(!violation.is_blocking()); } diff --git a/src/lib.rs b/src/lib.rs index a974c8e..5ccbed1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,7 +70,11 @@ impl GuardianValidator { let analyzer = Analyzer::new(config)?; let report_formatter = ReportFormatter::default(); - Ok(Self { analyzer, cache: None, report_formatter }) + Ok(Self { + analyzer, + cache: None, + report_formatter, + }) } /// Create a validator with default configuration @@ -104,7 +108,8 @@ impl GuardianValidator { &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 @@ -118,7 +123,8 @@ impl GuardianValidator { // 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::>(), @@ -188,7 +194,11 @@ impl GuardianValidator { /// Cleanup cache by removing entries for non-existent files pub fn cleanup_cache(&mut self) -> GuardianResult> { - if let Some(cache) = &mut self.cache { Ok(Some(cache.cleanup()?)) } else { Ok(None) } + if let Some(cache) = &mut self.cache { + Ok(Some(cache.cleanup()?)) + } else { + Ok(None) + } } /// Cache-aware analysis that skips files that haven't changed @@ -213,8 +223,11 @@ impl GuardianValidator { // 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().map(|v| v.file_path.clone()).collect(); + let discovered_files: std::collections::HashSet = temp_report + .violations + .iter() + .map(|v| v.file_path.clone()) + .collect(); all_files.extend(discovered_files); } } @@ -247,7 +260,10 @@ 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::>(), + &files_to_analyze + .iter() + .map(|p| p.as_path()) + .collect::>(), options, )?; @@ -256,8 +272,10 @@ impl GuardianValidator { // Update cache with new results for file_path in &files_to_analyze { - let file_violations: Vec<_> = - all_violations.iter().filter(|v| v.file_path == *file_path).collect(); + 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) @@ -452,7 +470,9 @@ mod tests { 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); @@ -469,10 +489,14 @@ mod tests { 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()); } diff --git a/src/main.rs b/src/main.rs index 552e545..f410edf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -246,13 +246,18 @@ async fn run_command(cli: Cli) -> GuardianResult { ) .await } - Commands::Watch { path, pattern, delay } => run_watch(path, pattern, delay).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) - } + Commands::Rules { + enabled_only, + category, + } => run_list_rules(cli.config, enabled_only, category), } } @@ -300,7 +305,11 @@ async fn run_check( } // Use current directory if no paths specified - let paths = if paths.is_empty() { vec![PathBuf::from(".")] } else { paths }; + let paths = if paths.is_empty() { + vec![PathBuf::from(".")] + } else { + paths + }; // Set up validation options let validation_options = ValidationOptions { @@ -323,7 +332,9 @@ async fn run_check( }; // 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())?; @@ -368,7 +379,11 @@ async fn run_watch( 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 }; + let watch_patterns = if patterns.is_empty() { + vec!["**/*.rs".to_string()] + } else { + patterns + }; println!("🎯 Patterns: {}", watch_patterns.join(", ")); println!("⏱️ Debounce delay: {delay_ms}ms"); @@ -389,9 +404,15 @@ async fn run_watch( .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(); @@ -526,7 +547,11 @@ 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() } @@ -575,7 +600,10 @@ async fn run_watch_analysis_with_config( }; // 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)?; @@ -670,7 +698,10 @@ fn run_explain(rule_id: String) -> GuardianResult { 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!(); @@ -805,7 +836,12 @@ fn run_list_rules( } 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 @@ -816,7 +852,13 @@ fn run_list_rules( let rule_status = if rule.enabled { "✅" } else { "❌" }; let severity = rule.severity.unwrap_or(category.severity); - println!(" {}🔍 {} [{}] - {}", rule_status, rule.id, severity.as_str(), rule.message); + println!( + " {}🔍 {} [{}] - {}", + rule_status, + rule.id, + severity.as_str(), + rule.message + ); } println!(); } @@ -825,15 +867,25 @@ fn run_list_rules( } fn init_logging(verbose: bool) { - let level = if verbose { tracing::Level::DEBUG } else { tracing::Level::WARN }; + let level = if verbose { + tracing::Level::DEBUG + } else { + tracing::Level::WARN + }; - tracing_subscriber::fmt().with_max_level(level).with_target(false).init(); + tracing_subscriber::fmt() + .with_max_level(level) + .with_target(false) + .init(); } fn format_timestamp(timestamp: u64) -> String { use chrono::{TimeZone, Utc}; - let dt = Utc.timestamp_opt(timestamp as i64, 0).single().unwrap_or_else(Utc::now); + 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() } @@ -851,9 +903,15 @@ mod tests { fs::write(&test_file, "// TODO: implement this\nfn main() {}").unwrap(); - // Test basic check + // Create a test config that will detect TODO comments + let config_file = temp_dir.path().join("test_config.yaml"); + let config = GuardianConfig::default(); + let yaml = serde_yaml::to_string(&config).unwrap(); + fs::write(&config_file, yaml).unwrap(); + + // Test basic check with explicit config let result = run_check( - None, + Some(config_file), vec![test_file], OutputFormatArg::Json, None, diff --git a/src/patterns/mod.rs b/src/patterns/mod.rs index 72f2f95..adcf21f 100644 --- a/src/patterns/mod.rs +++ b/src/patterns/mod.rs @@ -92,7 +92,10 @@ pub struct PatternMatch { impl PatternEngine { /// Create a new pattern engine pub fn new() -> Self { - Self { regex_patterns: HashMap::new(), ast_patterns: HashMap::new() } + Self { + regex_patterns: HashMap::new(), + ast_patterns: HashMap::new(), + } } /// Add a pattern rule to the engine @@ -119,7 +122,9 @@ impl PatternEngine { let regex = if rule.case_sensitive { Regex::new(&rule.pattern) } else { - RegexBuilder::new(&rule.pattern).case_insensitive(true).build() + RegexBuilder::new(&rule.pattern) + .case_insensitive(true) + .build() } .map_err(|e| { GuardianError::pattern(format!("Invalid regex '{}': {}", rule.pattern, e)) @@ -408,7 +413,9 @@ impl PatternEngine { 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(), @@ -434,8 +441,9 @@ impl PatternEngine { continue; } - let message = - pattern.message_template.replace("{value}", &complexity.to_string()); + let message = pattern + .message_template + .replace("{value}", &complexity.to_string()); matches.push(PatternMatch { rule_id: pattern.rule_id.clone(), @@ -485,8 +493,9 @@ impl PatternEngine { continue; } - let message = - pattern.message_template.replace("{lines}", &line_count.to_string()); + let message = pattern + .message_template + .replace("{lines}", &line_count.to_string()); matches.push(PatternMatch { rule_id: pattern.rule_id.clone(), @@ -512,7 +521,9 @@ impl PatternEngine { continue; } - let message = pattern.message_template.replace("{depth}", &depth.to_string()); + let message = pattern + .message_template + .replace("{depth}", &depth.to_string()); matches.push(PatternMatch { rule_id: pattern.rule_id.clone(), @@ -538,8 +549,9 @@ impl PatternEngine { continue; } - let message = - pattern.message_template.replace("{count}", &arg_count.to_string()); + let message = pattern + .message_template + .replace("{count}", &arg_count.to_string()); matches.push(PatternMatch { rule_id: pattern.rule_id.clone(), @@ -798,7 +810,9 @@ impl PatternEngine { continue; } - let message = pattern.message_template.replace("{function_name}", &fn_name); + let message = pattern + .message_template + .replace("{function_name}", &fn_name); matches.push(PatternMatch { rule_id: pattern.rule_id.clone(), @@ -898,7 +912,10 @@ impl PatternEngine { } } - let mut visitor = MacroVisitor { target_macros, matches: Vec::new() }; + let mut visitor = MacroVisitor { + target_macros, + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -959,7 +976,13 @@ impl PatternEngine { if let syn::Expr::Call(call) = expr { // 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] { @@ -973,7 +996,9 @@ impl PatternEngine { } } - let mut visitor = EmptyOkVisitor { matches: Vec::new() }; + let mut visitor = EmptyOkVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1014,7 +1039,9 @@ impl PatternEngine { } } - let mut visitor = EmptyBodyVisitor { matches: Vec::new() }; + let mut visitor = EmptyBodyVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1039,14 +1066,16 @@ impl PatternEngine { "unwrap" => { // unwrap() calls are always problematic let (line, col, context) = (1, 1, ".unwrap()".to_string()); - self.matches.push((line, col, "unwrap".to_string(), context)); + self.matches + .push((line, col, "unwrap".to_string(), context)); } "expect" => { // Check if expect() has a meaningful message if method_call.args.is_empty() { // expect() without any message let (line, col, context) = (1, 1, ".expect()".to_string()); - self.matches.push((line, col, "expect".to_string(), context)); + self.matches + .push((line, col, "expect".to_string(), context)); } else if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. @@ -1060,7 +1089,8 @@ impl PatternEngine { { let (line, col, context) = (1, 1, format!(".expect(\"{}\")", message)); - self.matches.push((line, col, "expect".to_string(), context)); + self.matches + .push((line, col, "expect".to_string(), context)); } } } @@ -1071,7 +1101,9 @@ impl PatternEngine { } } - let mut visitor = UnwrapVisitor { matches: Vec::new() }; + let mut visitor = UnwrapVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1096,7 +1128,9 @@ impl PatternEngine { // Convert the use statement back to string for regex matching let use_string = format!( "use {};", - quote::quote!(#use_item).to_string().trim_start_matches("use ") + quote::quote!(#use_item) + .to_string() + .trim_start_matches("use ") ); if self.regex.is_match(&use_string) { @@ -1111,7 +1145,10 @@ impl PatternEngine { } } - let mut visitor = ImportVisitor { regex, matches: Vec::new() }; + let mut visitor = ImportVisitor { + regex, + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1137,8 +1174,10 @@ impl PatternEngine { } // Extract context line - let line_end = - content[line_start..].find('\n').map(|pos| line_start + pos).unwrap_or(content.len()); + 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(); @@ -1222,7 +1261,11 @@ impl PatternEngine { /// 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().map(|s| s == "tests" || s == "test").unwrap_or(false) + component + .as_os_str() + .to_str() + .map(|s| s == "tests" || s == "test") + .unwrap_or(false) }) || file_path .file_name() .and_then(|name| name.to_str()) @@ -1307,7 +1350,10 @@ impl PatternEngine { } } - let mut visitor = ComplexityVisitor { threshold, matches: Vec::new() }; + let mut visitor = ComplexityVisitor { + threshold, + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1328,7 +1374,8 @@ impl PatternEngine { { let fn_name = func.sig.ident.to_string(); let (line, col, context) = (1, 1, format!("pub fn {}", fn_name)); - self.matches.push((line, col, format!("fn {}", fn_name), context)); + self.matches + .push((line, col, format!("fn {}", fn_name), context)); } syn::visit::visit_item_fn(self, func); } @@ -1339,7 +1386,8 @@ impl PatternEngine { { let struct_name = item_struct.ident.to_string(); let (line, col, context) = (1, 1, format!("pub struct {}", struct_name)); - self.matches.push((line, col, format!("struct {}", struct_name), context)); + self.matches + .push((line, col, format!("struct {}", struct_name), context)); } syn::visit::visit_item_struct(self, item_struct); } @@ -1350,7 +1398,8 @@ impl PatternEngine { { let enum_name = item_enum.ident.to_string(); let (line, col, context) = (1, 1, format!("pub enum {}", enum_name)); - self.matches.push((line, col, format!("enum {}", enum_name), context)); + self.matches + .push((line, col, format!("enum {}", enum_name), context)); } syn::visit::visit_item_enum(self, item_enum); } @@ -1361,7 +1410,8 @@ impl PatternEngine { { let trait_name = item_trait.ident.to_string(); let (line, col, context) = (1, 1, format!("pub trait {}", trait_name)); - self.matches.push((line, col, format!("trait {}", trait_name), context)); + self.matches + .push((line, col, format!("trait {}", trait_name), context)); } syn::visit::visit_item_trait(self, item_trait); } @@ -1383,7 +1433,9 @@ impl PatternEngine { } } - let mut visitor = PublicDocsVisitor { matches: Vec::new() }; + let mut visitor = PublicDocsVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1431,7 +1483,10 @@ impl PatternEngine { } } - let mut visitor = LongFunctionVisitor { threshold, matches: Vec::new() }; + let mut visitor = LongFunctionVisitor { + threshold, + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1456,8 +1511,11 @@ impl PatternEngine { self.current_depth += 1; if self.current_depth > self.threshold { - let (line, col, context) = - (1, 1, format!("nested block at depth {}", self.current_depth)); + let (line, col, context) = ( + 1, + 1, + format!("nested block at depth {}", self.current_depth), + ); self.matches.push((line, col, self.current_depth, context)); } @@ -1469,8 +1527,11 @@ impl PatternEngine { self.current_depth += 1; if self.current_depth > self.threshold { - let (line, col, context) = - (1, 1, format!("if statement at depth {}", self.current_depth)); + let (line, col, context) = ( + 1, + 1, + format!("if statement at depth {}", self.current_depth), + ); self.matches.push((line, col, self.current_depth, context)); } @@ -1482,8 +1543,11 @@ impl PatternEngine { self.current_depth += 1; if self.current_depth > self.threshold { - let (line, col, context) = - (1, 1, format!("match statement at depth {}", self.current_depth)); + let (line, col, context) = ( + 1, + 1, + format!("match statement at depth {}", self.current_depth), + ); self.matches.push((line, col, self.current_depth, context)); } @@ -1492,7 +1556,11 @@ impl PatternEngine { } } - let mut visitor = NestingVisitor { threshold, current_depth: 0, matches: Vec::new() }; + let mut visitor = NestingVisitor { + threshold, + current_depth: 0, + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1526,7 +1594,10 @@ impl PatternEngine { } } - let mut visitor = ManyArgsVisitor { threshold, matches: Vec::new() }; + let mut visitor = ManyArgsVisitor { + threshold, + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1597,7 +1668,10 @@ impl PatternEngine { } } - let mut visitor = BlockingInAsyncVisitor { in_async_fn: false, matches: Vec::new() }; + let mut visitor = BlockingInAsyncVisitor { + in_async_fn: false, + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1624,7 +1698,8 @@ impl PatternEngine { .contains(&fn_name.as_str()) { let (line, col, context) = (1, 1, format!("{}() not awaited", fn_name)); - self.matches.push((line, col, format!("{}()", fn_name), context)); + self.matches + .push((line, col, format!("{}()", fn_name), context)); } } } @@ -1633,7 +1708,9 @@ impl PatternEngine { } } - let mut visitor = FutureNotAwaitedVisitor { matches: Vec::new() }; + let mut visitor = FutureNotAwaitedVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1664,7 +1741,9 @@ impl PatternEngine { } } - let mut visitor = SelectVisitor { matches: Vec::new() }; + let mut visitor = SelectVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1701,8 +1780,11 @@ impl PatternEngine { if let syn::GenericParam::Type(type_param) = param { if type_param.bounds.is_empty() { let generic_name = type_param.ident.to_string(); - let (line, col, context) = - (1, 1, format!("struct {}<{}>", item_struct.ident, generic_name)); + let (line, col, context) = ( + 1, + 1, + format!("struct {}<{}>", item_struct.ident, generic_name), + ); self.matches.push((line, col, generic_name, context)); } } @@ -1712,7 +1794,9 @@ impl PatternEngine { } } - let mut visitor = GenericBoundsVisitor { matches: Vec::new() }; + let mut visitor = GenericBoundsVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1786,7 +1870,9 @@ impl PatternEngine { } } - let mut visitor = TestAssertionVisitor { matches: Vec::new() }; + let mut visitor = TestAssertionVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1822,7 +1908,9 @@ impl PatternEngine { } } - let mut visitor = ImplTraitVisitor { matches: Vec::new() }; + let mut visitor = ImplTraitVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1855,7 +1943,9 @@ impl PatternEngine { } } - let mut visitor = UnsafeVisitor { matches: Vec::new() }; + let mut visitor = UnsafeVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1885,7 +1975,9 @@ impl PatternEngine { } } - let mut visitor = IgnoredTestVisitor { matches: Vec::new() }; + let mut visitor = IgnoredTestVisitor { + matches: Vec::new(), + }; visitor.visit_file(syntax_tree); visitor.matches @@ -1991,8 +2083,8 @@ pub mod validation { } /// Validate exclude conditions functionality - designed for integration testing - pub fn validate_exclude_conditions_functionality() - -> crate::domain::violations::GuardianResult<()> { + pub fn validate_exclude_conditions_functionality( + ) -> crate::domain::violations::GuardianResult<()> { let mut engine = PatternEngine::new(); let rule = PatternRule { diff --git a/src/patterns/path_filter.rs b/src/patterns/path_filter.rs index 7df2204..9dcd039 100644 --- a/src/patterns/path_filter.rs +++ b/src/patterns/path_filter.rs @@ -48,7 +48,11 @@ impl PathFilter { GuardianError::pattern(format!("Invalid pattern '{pattern_str}': {e}")) })?; - filter_patterns.push(FilterPattern { pattern, is_include, original: pattern_str }); + filter_patterns.push(FilterPattern { + pattern, + is_include, + original: pattern_str, + }); } Ok(Self { @@ -167,7 +171,11 @@ impl PathFilter { match glob::Pattern::new(&pattern_str) { Ok(pattern) => { - patterns.push(FilterPattern { pattern, is_include, original: pattern_str }); + patterns.push(FilterPattern { + pattern, + is_include, + original: pattern_str, + }); } Err(e) => { // Log warning but don't fail - just skip invalid patterns @@ -190,22 +198,25 @@ impl PathFilter { let mut files = Vec::new(); // OPTIMIZATION: Use filter_entry to skip massive directories BEFORE entering them - let walker = WalkDir::new(root).follow_links(false).into_iter().filter_entry(|e| { - let name = e.file_name().to_string_lossy(); - - // SKIP common massive directories to prevent IO floods - if name == ".git" - || name == "target" - || name == "node_modules" - || name == ".venv" - || name == "venv" - || name == ".idea" - || name == ".vscode" - { - return false; - } - true - }); + let walker = WalkDir::new(root) + .follow_links(false) + .into_iter() + .filter_entry(|e| { + let name = e.file_name().to_string_lossy(); + + // SKIP common massive directories to prevent IO floods + if name == ".git" + || name == "target" + || name == "node_modules" + || name == ".venv" + || name == "venv" + || name == ".idea" + || name == ".vscode" + { + return false; + } + true + }); for entry in walker.filter_map(|e| e.ok()) { let path = entry.path(); @@ -285,12 +296,17 @@ impl PathFilter { } // Remove trailing slash and match let dir_pattern = pattern.original.trim_end_matches('/'); - return glob::Pattern::new(dir_pattern).map(|p| p.matches(&path_str)).unwrap_or(false); + return glob::Pattern::new(dir_pattern) + .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); @@ -417,7 +433,10 @@ pub mod validation { fs::create_dir_all(root.join("tests"))?; // Create .guardianignore file - fs::write(root.join(".guardianignore"), "*.tmp\ntests/**\n!tests/important.rs\n")?; + fs::write( + root.join(".guardianignore"), + "*.tmp\ntests/**\n!tests/important.rs\n", + )?; // Create test files fs::write(root.join("src/lib.rs"), "")?; diff --git a/src/report/mod.rs b/src/report/mod.rs index 7b89d8a..34c6c23 100644 --- a/src/report/mod.rs +++ b/src/report/mod.rs @@ -171,12 +171,16 @@ impl ReportOptions { show_suggestions: false, ..Self::default() }, - OutputFormat::Junit => { - Self { use_colors: false, show_suggestions: false, ..Self::default() } - } - OutputFormat::GitHub => { - Self { use_colors: false, show_suggestions: false, ..Self::default() } - } + OutputFormat::Junit => Self { + use_colors: false, + show_suggestions: false, + ..Self::default() + }, + OutputFormat::GitHub => Self { + use_colors: false, + show_suggestions: false, + ..Self::default() + }, OutputFormat::Agent => Self { use_colors: false, show_context: false, @@ -246,9 +250,18 @@ impl ReportFormatter { /// Check if the current environment supports ANSI color codes fn supports_ansi_colors() -> bool { - // Basic heuristic - in production this would check terminal capabilities - std::env::var("NO_COLOR").is_err() - && (std::env::var("TERM").is_ok_and(|term| term != "dumb")) + // Check if colors are explicitly disabled + if std::env::var("NO_COLOR").is_ok() { + return false; + } + + // GitHub Actions and other CI systems support ANSI colors + if std::env::var("GITHUB_ACTIONS").is_ok() || std::env::var("CI").is_ok() { + return true; + } + + // Check terminal capabilities + std::env::var("TERM").is_ok_and(|term| term != "dumb") } /// Validate that a format operation produces expected structure @@ -327,7 +340,11 @@ impl ReportFormatter { )); } - if json.get("runs").and_then(|r| r.as_array()).is_none_or(|arr| arr.is_empty()) { + if json + .get("runs") + .and_then(|r| r.as_array()) + .is_none_or(|arr| arr.is_empty()) + { return Err(crate::domain::violations::GuardianError::config( "SARIF output must contain at least one run", )); @@ -434,7 +451,10 @@ impl ReportFormatter { 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 @@ -563,7 +583,10 @@ impl ReportFormatter { 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; // Currently using failures count for both failures and errors let execution_time = (report.summary.execution_time_ms as f64) / 1000.0; @@ -686,8 +709,11 @@ impl ReportFormatter { _ => 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", @@ -714,7 +740,10 @@ impl ReportFormatter { let line_number = violation.line_number.unwrap_or(1); let path = violation.file_path.display(); - output.push_str(&format!("[{}:{}]\n{}\n\n", line_number, path, violation.message)); + output.push_str(&format!( + "[{}:{}]\n{}\n\n", + line_number, path, violation.message + )); } Ok(output) @@ -752,7 +781,11 @@ impl ReportFormatter { 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{text}\x1b[0m")); @@ -765,7 +798,11 @@ impl ReportFormatter { 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{text}\x1b[0m")); @@ -839,7 +876,10 @@ mod tests { #[test] fn test_human_format() { - let options = ReportOptions { use_colors: false, ..Default::default() }; + let options = ReportOptions { + use_colors: false, + ..Default::default() + }; // Demonstrate self-validation options.validate().expect("Test options should be valid"); @@ -878,7 +918,13 @@ mod tests { let json: JsonValue = serde_json::from_str(&output).expect("JSON output should be valid JSON"); assert!(json["violations"].is_array()); - assert_eq!(json["violations"].as_array().expect("violations should be an array").len(), 1); + assert_eq!( + json["violations"] + .as_array() + .expect("violations should be an array") + .len(), + 1 + ); assert_eq!(json["violations"][0]["rule_id"], "test_rule"); assert_eq!(json["summary"]["total_files"], 10); } @@ -923,7 +969,10 @@ mod tests { #[test] fn test_empty_report() { - let options = ReportOptions { use_colors: false, ..Default::default() }; + let options = ReportOptions { + use_colors: false, + ..Default::default() + }; // Demonstrate self-validation options.validate().expect("Test options should be valid"); @@ -945,10 +994,15 @@ mod tests { #[test] fn test_severity_filtering() { - let options = ReportOptions { min_severity: Some(Severity::Error), ..Default::default() }; + let options = ReportOptions { + min_severity: Some(Severity::Error), + ..Default::default() + }; // Demonstrate self-validation of filtering options - options.validate().expect("Severity filtering options should be valid"); + options + .validate() + .expect("Severity filtering options should be valid"); let formatter = ReportFormatter::with_options(options); @@ -980,7 +1034,10 @@ mod tests { // Should only include the error, not the warning assert_eq!( - json["violations"].as_array().expect("filtered violations should be an array").len(), + json["violations"] + .as_array() + .expect("filtered violations should be an array") + .len(), 1 ); assert_eq!(json["violations"][0]["rule_id"], "error_rule");