(mut context: AnalysisContext)
| 31 | } |
| 32 | |
| 33 | pub fn run_analysis(mut context: AnalysisContext) -> Vec<Issue> { |
| 34 | // Apply disabled_rule_ids from [defaults] before scanning |
| 35 | if !context.ruleset.defaults.disabled_rule_ids.is_empty() { |
| 36 | let disabled: std::collections::HashSet<&str> = context.ruleset.defaults |
| 37 | .disabled_rule_ids.iter().map(|s| s.as_str()).collect(); |
| 38 | let before = context.ruleset.rules.len(); |
| 39 | context.ruleset.rules.retain(|r| !disabled.contains(r.id.as_str())); |
| 40 | let removed = before - context.ruleset.rules.len(); |
| 41 | if removed > 0 { |
| 42 | println!("[*] Disabled {} rules via [defaults].disabled_rule_ids", removed); |
| 43 | } |
| 44 | } |
| 45 | println!("[*] Starting analysis with {} rules", context.ruleset.rules.len()); |
| 46 | |
| 47 | let root_path = Path::new(&context.root_path); |
| 48 | let mut files_to_scan: Vec<String> = Vec::new(); |
| 49 | |
| 50 | // Add common test fixture patterns to exclusions |
| 51 | let mut enhanced_exclusions = context.exclusions.clone(); |
| 52 | enhanced_exclusions.extend(vec![ |
| 53 | "*/tests/fixtures/*".to_string(), |
| 54 | "*/test/fixtures/*".to_string(), |
| 55 | "*_test.py".to_string(), |
| 56 | "*/test_*.py".to_string(), |
| 57 | ]); |
| 58 | // Precompile each exclusion pattern once — is_excluded() is called once per |
| 59 | // file in the walk below and once per file again in the AST pass, so |
| 60 | // re-parsing every glob pattern with WildMatch::new() on every call (and |
| 61 | // twice per call, at that) adds up on large codebases. |
| 62 | let compiled_exclusions = compile_exclusions(&enhanced_exclusions); |
| 63 | |
| 64 | for entry in WalkDir::new(root_path).into_iter().filter_map(|e| e.ok()) { |
| 65 | let path = entry.path(); |
| 66 | // Collect all files (not just .py) for regex scanning |
| 67 | if path.is_file() && !is_excluded(path, &compiled_exclusions) { |
| 68 | if let Some(s) = path.to_str() { |
| 69 | files_to_scan.push(s.to_string()); |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | println!("[+] Found {} files to scan ({} non-Python)", files_to_scan.len(), |
| 75 | files_to_scan.iter().filter(|f| !f.ends_with(".py")).count()); |
| 76 | |
| 77 | // Scan all files with regex patterns |
| 78 | let t_config = std::time::Instant::now(); |
| 79 | let mut issues: Vec<Issue> = files_to_scan |
| 80 | .par_iter() |
| 81 | .flat_map(|file_path| { |
| 82 | if let Ok(content) = fs::read_to_string(file_path) { |
| 83 | config_analysis::scan_file(file_path, &content, &context.ruleset) |
| 84 | } else { |
| 85 | Vec::new() |
| 86 | } |
| 87 | }) |
| 88 | .collect(); |
| 89 | println!("[*] Pattern/config scan: {:.2}s → {} issues", t_config.elapsed().as_secs_f64(), issues.len()); |
| 90 |
no test coverage detected