1use std::collections::{HashMap, HashSet};
5use std::fs;
6use std::path::{Path, PathBuf};
7
8use ast_grep_language::SupportLang;
9use rayon::prelude::*;
10
11use crate::cache::{file_content_hash, rules_hash, CacheEntry, CacheStore};
12use crate::config::Config;
13use crate::cross_file::FileSymbols;
14use crate::disable::unused_disable_findings;
15use crate::finding::{CacheStats, Finding, ScanResult, ScanStats};
16use crate::resolution::FileImports;
17use crate::rule::{Rule, Severity};
18
19use super::project::{append_cross_file, index_only_files};
20use super::{
21 append_resolution, build_glob_set, compile_rules, explicit_files, scan_file, walk_files,
22 CompiledRules, FileScan, ScanError,
23};
24
25pub fn count_severities(findings: &[Finding]) -> (usize, usize) {
27 let mut errors = 0;
28 let mut warnings = 0;
29 for finding in findings {
30 match finding.severity {
31 Severity::Error => errors += 1,
32 Severity::Warning => warnings += 1,
33 }
34 }
35 (errors, warnings)
36}
37
38fn normalize_findings(findings: &mut Vec<Finding>) {
40 findings.sort_by(|a, b| (&a.file, a.line, a.column).cmp(&(&b.file, b.line, b.column)));
41 findings.dedup_by(|a, b| {
42 a.rule_id == b.rule_id && a.file == b.file && a.line == b.line && a.column == b.column
43 });
44}
45
46struct ScanSet {
49 files: Vec<(PathBuf, SupportLang)>,
50 index_only: Vec<(PathBuf, SupportLang)>,
51}
52
53impl ScanSet {
54 fn walk(
57 paths: &[PathBuf],
58 compiled: &CompiledRules,
59 config: &Config,
60 ) -> Result<Self, ScanError> {
61 let ignores = build_glob_set(&config.scan.ignores)?;
62 let files = walk_files(paths, &ignores);
63 let index_only = index_only_files(compiled, paths, &files, &ignores);
64 Ok(Self { files, index_only })
65 }
66
67 fn explicit(
70 targets: &[PathBuf],
71 compiled: &CompiledRules,
72 config: &Config,
73 ) -> Result<Self, ScanError> {
74 let ignores = build_glob_set(&config.scan.ignores)?;
75 let files = explicit_files(targets, &ignores);
76 let index_only = index_only_files(compiled, targets, &files, &ignores);
77 Ok(Self { files, index_only })
78 }
79
80 fn reported(&self) -> HashSet<&Path> {
82 self.files.iter().map(|(path, _)| path.as_path()).collect()
83 }
84}
85
86fn scan_collected(set: &ScanSet, compiled: &CompiledRules, apply_disable: bool) -> ScanResult {
91 let (per_file, project) = compiled.engines(apply_disable);
92 let collect_symbols = project.iter().any(|e| e.needs_symbols());
93 let collect_imports = !compiled.resolution.is_empty();
94 let scans: Vec<(PathBuf, FileScan)> = set
95 .files
96 .par_iter()
97 .map(|(path, lang)| {
98 let scan = scan_file(
99 path,
100 *lang,
101 &per_file,
102 &compiled.test_paths,
103 collect_symbols,
104 collect_imports,
105 apply_disable,
106 );
107 (path.clone(), scan)
108 })
109 .collect();
110
111 let mut findings: Vec<Finding> = Vec::new();
112 let mut contributions: Vec<(PathBuf, FileSymbols)> = Vec::new();
113 let mut import_contributions: Vec<(PathBuf, FileImports)> = Vec::new();
114 for (path, scan) in scans {
115 findings.extend(scan.findings);
116 if collect_symbols {
117 contributions.push((path.clone(), scan.symbols));
118 }
119 if collect_imports {
120 import_contributions.push((path, scan.imports));
121 }
122 }
123 if collect_symbols {
124 let test_paths = &compiled.test_paths;
125 let extra: Vec<(PathBuf, FileSymbols)> = set
126 .index_only
127 .par_iter()
128 .map(|(path, lang)| {
129 let scan = scan_file(path, *lang, &[], test_paths, true, false, apply_disable);
130 (path.clone(), scan.symbols)
131 })
132 .collect();
133 contributions.extend(extra);
134 }
135 append_cross_file(&mut findings, &project, &contributions, &set.reported());
136 append_resolution(
137 &mut findings,
138 compiled,
139 &import_contributions,
140 apply_disable,
141 );
142 normalize_findings(&mut findings);
143
144 let (errors, warnings) = count_severities(&findings);
145
146 ScanResult {
147 findings,
148 stats: ScanStats {
149 errors,
150 warnings,
151 total: errors + warnings,
152 files_scanned: set.files.len(),
153 baseline_filtered: 0,
154 diff_base: None,
155 files_changed: None,
156 },
157 cache_stats: None,
158 }
159}
160
161fn scan_collected_cached(
172 set: &ScanSet,
173 compiled: &CompiledRules,
174 rules: &[Rule],
175 cache_dir: &Path,
176 prune: bool,
177) -> ScanResult {
178 let (per_file, project) = compiled.engines(true);
182 let collect_symbols = project.iter().any(|e| e.needs_symbols());
183 let collect_imports = !compiled.resolution.is_empty();
184 let current_rules_hash = rules_hash(rules);
185 let store = CacheStore::with_dir(cache_dir.to_path_buf()).scoped_to_rules(¤t_rules_hash);
186 let rules_changed = store
187 .check_rules_changed(¤t_rules_hash)
188 .unwrap_or(true);
189
190 struct FileWork<'a> {
191 path: &'a Path,
192 lang: SupportLang,
193 hash: String,
194 reported: bool,
196 }
197
198 let reported = set.files.iter().map(|file| (file, true));
199 let index_only = set.index_only.iter().map(|file| (file, false));
200 let work: Vec<FileWork> = reported
201 .chain(index_only)
202 .filter_map(|((path, lang), reported)| {
203 let content = fs::read(path).ok()?;
204 Some(FileWork {
205 path,
206 lang: *lang,
207 hash: file_content_hash(&content),
208 reported,
209 })
210 })
211 .collect();
212
213 let mut cached_count = 0usize;
214 let mut changed_count = 0usize;
215 let mut all_findings: Vec<Finding> = Vec::new();
216 let mut contributions: Vec<(PathBuf, FileSymbols)> = Vec::new();
217 let mut import_contributions: Vec<(PathBuf, FileImports)> = Vec::new();
218 let mut take = |file: &FileWork, entry: CacheEntry| {
219 if file.reported {
220 all_findings.extend(entry.findings);
221 if collect_imports {
222 import_contributions.push((file.path.to_path_buf(), entry.imports));
223 }
224 }
225 if collect_symbols {
226 contributions.push((file.path.to_path_buf(), entry.symbols));
227 }
228 };
229
230 let mut to_scan: Vec<&FileWork> = Vec::new();
231 for file in &work {
232 match (!rules_changed).then(|| store.get(&file.hash)).flatten() {
233 Some(entry) => {
234 cached_count += usize::from(file.reported);
235 take(file, entry);
236 }
237 None => {
238 changed_count += usize::from(file.reported);
239 to_scan.push(file);
240 }
241 }
242 }
243
244 let scanned: Vec<(&FileWork, FileScan)> = to_scan
245 .into_par_iter()
246 .map(|file| {
247 let scan = scan_file(
248 file.path,
249 file.lang,
250 &per_file,
251 &compiled.test_paths,
252 collect_symbols,
253 collect_imports,
254 true,
255 );
256 (file, scan)
257 })
258 .collect();
259
260 for (file, scan) in scanned {
261 let entry = CacheEntry {
262 findings: scan.findings,
263 symbols: scan.symbols,
264 imports: scan.imports,
265 };
266 store.put(&file.hash, &entry).ok();
267 take(file, entry);
268 }
269
270 append_cross_file(&mut all_findings, &project, &contributions, &set.reported());
274 append_resolution(&mut all_findings, compiled, &import_contributions, true);
275
276 if prune {
277 let current_hashes: Vec<String> = work.iter().map(|file| file.hash.clone()).collect();
278 store.cleanup(¤t_hashes).ok();
279 }
280
281 normalize_findings(&mut all_findings);
282
283 let (errors, warnings) = count_severities(&all_findings);
284
285 let files_scanned = cached_count + changed_count;
286
287 ScanResult {
288 findings: all_findings,
289 stats: ScanStats {
290 errors,
291 warnings,
292 total: errors + warnings,
293 files_scanned,
294 baseline_filtered: 0,
295 diff_base: None,
296 files_changed: None,
297 },
298 cache_stats: Some(CacheStats {
299 cached: cached_count,
300 changed: changed_count,
301 }),
302 }
303}
304
305pub fn scan(paths: &[PathBuf], rules: &[Rule], config: &Config) -> Result<ScanResult, ScanError> {
310 let compiled = compile_rules(rules, config)?;
311 let set = ScanSet::walk(paths, &compiled, config)?;
312 Ok(scan_collected(&set, &compiled, true))
313}
314
315pub fn scan_cached(
327 paths: &[PathBuf],
328 rules: &[Rule],
329 config: &Config,
330 cache_dir: &Path,
331) -> Result<ScanResult, ScanError> {
332 let compiled = compile_rules(rules, config)?;
333 let set = ScanSet::walk(paths, &compiled, config)?;
334 let prune = paths.iter().all(|path| path.is_dir());
335 Ok(scan_collected_cached(
336 &set, &compiled, rules, cache_dir, prune,
337 ))
338}
339
340pub fn scan_files(
349 files: &[PathBuf],
350 rules: &[Rule],
351 config: &Config,
352) -> Result<ScanResult, ScanError> {
353 let compiled = compile_rules(rules, config)?;
354 let set = ScanSet::explicit(files, &compiled, config)?;
355 Ok(scan_collected(&set, &compiled, true))
356}
357
358pub fn scan_unused_disables(
366 paths: &[PathBuf],
367 rules: &[Rule],
368 config: &Config,
369) -> Result<Vec<Finding>, ScanError> {
370 let compiled = compile_rules(rules, config)?;
371 let set = ScanSet::walk(paths, &compiled, config)?;
372 let raw = scan_collected(&set, &compiled, false);
373 Ok(unused_from_raw(&set.files, &raw.findings))
374}
375
376pub fn scan_files_unused_disables(
380 files: &[PathBuf],
381 rules: &[Rule],
382 config: &Config,
383) -> Result<Vec<Finding>, ScanError> {
384 let compiled = compile_rules(rules, config)?;
385 let set = ScanSet::explicit(files, &compiled, config)?;
386 let raw = scan_collected(&set, &compiled, false);
387 Ok(unused_from_raw(&set.files, &raw.findings))
388}
389
390fn unused_from_raw(files: &[(PathBuf, SupportLang)], raw: &[Finding]) -> Vec<Finding> {
393 let mut by_file: HashMap<&Path, Vec<Finding>> = HashMap::new();
394 for finding in raw {
395 by_file
396 .entry(finding.file.as_path())
397 .or_default()
398 .push(finding.clone());
399 }
400 let empty: Vec<Finding> = Vec::new();
401 let mut out = Vec::new();
402 for (path, _lang) in files {
403 let Ok(source) = fs::read_to_string(path) else {
404 continue;
405 };
406 let file_raw = by_file.get(path.as_path()).unwrap_or(&empty);
407 out.extend(unused_disable_findings(path, &source, file_raw));
408 }
409 out
410}
411
412pub fn scan_files_cached(
415 files: &[PathBuf],
416 rules: &[Rule],
417 config: &Config,
418 cache_dir: &Path,
419) -> Result<ScanResult, ScanError> {
420 let compiled = compile_rules(rules, config)?;
421 let set = ScanSet::explicit(files, &compiled, config)?;
422 Ok(scan_collected_cached(
423 &set, &compiled, rules, cache_dir, false,
424 ))
425}