1use std::fs;
5use std::path::{Path, PathBuf};
6
7use ast_grep_language::SupportLang;
8use rayon::prelude::*;
9
10use crate::cache::{file_content_hash, rules_hash, CacheEntry, CacheStore};
11use crate::config::Config;
12use crate::cross_file::FileSymbols;
13use crate::finding::{CacheStats, Finding, ScanResult, ScanStats};
14use crate::resolution::FileImports;
15use crate::rule::{Rule, Severity};
16use crate::test_filter::TestPaths;
17
18use super::{
19 append_cross_file, append_resolution, build_glob_set, compile_rules, explicit_files, scan_file,
20 walk_files, CompiledRules, FileScan, ScanError,
21};
22
23pub fn count_severities(findings: &[Finding]) -> (usize, usize) {
25 let mut errors = 0;
26 let mut warnings = 0;
27 for finding in findings {
28 match finding.severity {
29 Severity::Error => errors += 1,
30 Severity::Warning => warnings += 1,
31 }
32 }
33 (errors, warnings)
34}
35
36fn normalize_findings(findings: &mut Vec<Finding>) {
38 findings.sort_by(|a, b| (&a.file, a.line, a.column).cmp(&(&b.file, b.line, b.column)));
39 findings.dedup_by(|a, b| {
40 a.rule_id == b.rule_id && a.file == b.file && a.line == b.line && a.column == b.column
41 });
42}
43
44fn scan_collected(
49 files: Vec<(PathBuf, SupportLang)>,
50 compiled: &CompiledRules,
51 run_cross_file: bool,
52) -> ScanResult {
53 let collect_symbols = !compiled.cross_file.is_empty();
54 let collect_imports = !compiled.resolution.is_empty();
55 let scans: Vec<(PathBuf, FileScan)> = files
56 .par_iter()
57 .map(|(path, lang)| {
58 let scan = scan_file(path, *lang, compiled, collect_symbols, collect_imports);
59 (path.clone(), scan)
60 })
61 .collect();
62
63 let mut findings: Vec<Finding> = Vec::new();
64 let mut contributions: Vec<(PathBuf, FileSymbols)> = Vec::new();
65 let mut import_contributions: Vec<(PathBuf, FileImports)> = Vec::new();
66 for (path, scan) in scans {
67 findings.extend(scan.findings);
68 if collect_symbols {
69 contributions.push((path.clone(), scan.symbols));
70 }
71 if collect_imports {
72 import_contributions.push((path, scan.imports));
73 }
74 }
75 append_cross_file(&mut findings, compiled, run_cross_file, &contributions);
76 append_resolution(&mut findings, compiled, &import_contributions);
77 normalize_findings(&mut findings);
78
79 let (errors, warnings) = count_severities(&findings);
80
81 ScanResult {
82 findings,
83 stats: ScanStats {
84 errors,
85 warnings,
86 total: errors + warnings,
87 files_scanned: files.len(),
88 baseline_filtered: 0,
89 diff_base: None,
90 files_changed: None,
91 },
92 cache_stats: None,
93 }
94}
95
96fn scan_collected_cached(
103 files: Vec<(PathBuf, SupportLang)>,
104 compiled: &CompiledRules,
105 rules: &[Rule],
106 cache_dir: &Path,
107 prune: bool,
108 run_cross_file: bool,
109) -> ScanResult {
110 let collect_symbols = !compiled.cross_file.is_empty();
111 let collect_imports = !compiled.resolution.is_empty();
112 let current_rules_hash = rules_hash(rules);
113 let store = CacheStore::with_dir(cache_dir.to_path_buf()).scoped_to_rules(¤t_rules_hash);
114 let rules_changed = store
115 .check_rules_changed(¤t_rules_hash)
116 .unwrap_or(true);
117
118 let file_contents: Vec<(PathBuf, SupportLang, Vec<u8>, String)> = files
119 .into_iter()
120 .filter_map(|(path, lang)| {
121 let content = fs::read(&path).ok()?;
122 let hash = file_content_hash(&content);
123 Some((path, lang, content, hash))
124 })
125 .collect();
126
127 let mut cached_count = 0usize;
128 let mut changed_count = 0usize;
129 let mut all_findings: Vec<Finding> = Vec::new();
130 let mut contributions: Vec<(PathBuf, FileSymbols)> = Vec::new();
131 let mut import_contributions: Vec<(PathBuf, FileImports)> = Vec::new();
132
133 struct FileWork {
134 path: PathBuf,
135 lang: SupportLang,
136 hash: String,
137 }
138
139 let mut to_scan: Vec<FileWork> = Vec::new();
140
141 for (path, lang, _content, hash) in file_contents.iter() {
142 if !rules_changed {
143 if let Some(entry) = store.get(hash) {
144 cached_count += 1;
145 all_findings.extend(entry.findings);
146 if collect_symbols {
147 contributions.push((path.clone(), entry.symbols));
148 }
149 if collect_imports {
150 import_contributions.push((path.clone(), entry.imports));
151 }
152 continue;
153 }
154 }
155 changed_count += 1;
156 to_scan.push(FileWork {
157 path: path.clone(),
158 lang: *lang,
159 hash: hash.clone(),
160 });
161 }
162
163 let scanned: Vec<(PathBuf, String, FileScan)> = to_scan
164 .par_iter()
165 .map(|work| {
166 let scan = scan_file(
167 &work.path,
168 work.lang,
169 compiled,
170 collect_symbols,
171 collect_imports,
172 );
173 (work.path.clone(), work.hash.clone(), scan)
174 })
175 .collect();
176
177 for (path, hash, scan) in scanned {
178 let entry = CacheEntry {
179 findings: scan.findings,
180 symbols: scan.symbols,
181 imports: scan.imports,
182 };
183 store.put(&hash, &entry).ok();
184 all_findings.extend(entry.findings);
185 if collect_symbols {
186 contributions.push((path.clone(), entry.symbols));
187 }
188 if collect_imports {
189 import_contributions.push((path, entry.imports));
190 }
191 }
192
193 append_cross_file(&mut all_findings, compiled, run_cross_file, &contributions);
197 append_resolution(&mut all_findings, compiled, &import_contributions);
198
199 if prune {
200 let current_hashes: Vec<String> =
201 file_contents.iter().map(|(_, _, _, h)| h.clone()).collect();
202 store.cleanup(¤t_hashes).ok();
203 }
204
205 normalize_findings(&mut all_findings);
206
207 let (errors, warnings) = count_severities(&all_findings);
208
209 let files_scanned = cached_count + changed_count;
210
211 ScanResult {
212 findings: all_findings,
213 stats: ScanStats {
214 errors,
215 warnings,
216 total: errors + warnings,
217 files_scanned,
218 baseline_filtered: 0,
219 diff_base: None,
220 files_changed: None,
221 },
222 cache_stats: Some(CacheStats {
223 cached: cached_count,
224 changed: changed_count,
225 }),
226 }
227}
228
229pub fn scan(paths: &[PathBuf], rules: &[Rule], config: &Config) -> Result<ScanResult, ScanError> {
231 let test_paths = TestPaths::new(&config.scan.test_paths)?;
232 let compiled = compile_rules(rules, test_paths)?;
233 let ignores = build_glob_set(&config.scan.ignores)?;
234 Ok(scan_collected(walk_files(paths, &ignores), &compiled, true))
235}
236
237pub fn scan_cached(
246 paths: &[PathBuf],
247 rules: &[Rule],
248 config: &Config,
249 cache_dir: &Path,
250) -> Result<ScanResult, ScanError> {
251 let test_paths = TestPaths::new(&config.scan.test_paths)?;
252 let compiled = compile_rules(rules, test_paths)?;
253 let ignores = build_glob_set(&config.scan.ignores)?;
254 Ok(scan_collected_cached(
255 walk_files(paths, &ignores),
256 &compiled,
257 rules,
258 cache_dir,
259 true,
260 true,
261 ))
262}
263
264pub fn scan_files(
274 files: &[PathBuf],
275 rules: &[Rule],
276 config: &Config,
277) -> Result<ScanResult, ScanError> {
278 let test_paths = TestPaths::new(&config.scan.test_paths)?;
279 let compiled = compile_rules(rules, test_paths)?;
280 let ignores = build_glob_set(&config.scan.ignores)?;
281 Ok(scan_collected(
282 explicit_files(files, &ignores),
283 &compiled,
284 false,
285 ))
286}
287
288pub fn scan_files_cached(
296 files: &[PathBuf],
297 rules: &[Rule],
298 config: &Config,
299 cache_dir: &Path,
300) -> Result<ScanResult, ScanError> {
301 let test_paths = TestPaths::new(&config.scan.test_paths)?;
302 let compiled = compile_rules(rules, test_paths)?;
303 let ignores = build_glob_set(&config.scan.ignores)?;
304 Ok(scan_collected_cached(
305 explicit_files(files, &ignores),
306 &compiled,
307 rules,
308 cache_dir,
309 false,
310 false,
311 ))
312}