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 (per_file, project) = compiled.engines();
54 let collect_symbols = project.iter().any(|e| e.needs_symbols());
55 let collect_imports = !compiled.resolution.is_empty();
56 let scans: Vec<(PathBuf, FileScan)> = files
57 .par_iter()
58 .map(|(path, lang)| {
59 let scan = scan_file(
60 path,
61 *lang,
62 &per_file,
63 &compiled.test_paths,
64 collect_symbols,
65 collect_imports,
66 );
67 (path.clone(), scan)
68 })
69 .collect();
70
71 let mut findings: Vec<Finding> = Vec::new();
72 let mut contributions: Vec<(PathBuf, FileSymbols)> = Vec::new();
73 let mut import_contributions: Vec<(PathBuf, FileImports)> = Vec::new();
74 for (path, scan) in scans {
75 findings.extend(scan.findings);
76 if collect_symbols {
77 contributions.push((path.clone(), scan.symbols));
78 }
79 if collect_imports {
80 import_contributions.push((path, scan.imports));
81 }
82 }
83 append_cross_file(&mut findings, &project, run_cross_file, &contributions);
84 append_resolution(&mut findings, compiled, &import_contributions);
85 normalize_findings(&mut findings);
86
87 let (errors, warnings) = count_severities(&findings);
88
89 ScanResult {
90 findings,
91 stats: ScanStats {
92 errors,
93 warnings,
94 total: errors + warnings,
95 files_scanned: files.len(),
96 baseline_filtered: 0,
97 diff_base: None,
98 files_changed: None,
99 },
100 cache_stats: None,
101 }
102}
103
104fn scan_collected_cached(
111 files: Vec<(PathBuf, SupportLang)>,
112 compiled: &CompiledRules,
113 rules: &[Rule],
114 cache_dir: &Path,
115 prune: bool,
116 run_cross_file: bool,
117) -> ScanResult {
118 let (per_file, project) = compiled.engines();
119 let collect_symbols = project.iter().any(|e| e.needs_symbols());
120 let collect_imports = !compiled.resolution.is_empty();
121 let current_rules_hash = rules_hash(rules);
122 let store = CacheStore::with_dir(cache_dir.to_path_buf()).scoped_to_rules(¤t_rules_hash);
123 let rules_changed = store
124 .check_rules_changed(¤t_rules_hash)
125 .unwrap_or(true);
126
127 let file_contents: Vec<(PathBuf, SupportLang, Vec<u8>, String)> = files
128 .into_iter()
129 .filter_map(|(path, lang)| {
130 let content = fs::read(&path).ok()?;
131 let hash = file_content_hash(&content);
132 Some((path, lang, content, hash))
133 })
134 .collect();
135
136 let mut cached_count = 0usize;
137 let mut changed_count = 0usize;
138 let mut all_findings: Vec<Finding> = Vec::new();
139 let mut contributions: Vec<(PathBuf, FileSymbols)> = Vec::new();
140 let mut import_contributions: Vec<(PathBuf, FileImports)> = Vec::new();
141
142 struct FileWork {
143 path: PathBuf,
144 lang: SupportLang,
145 hash: String,
146 }
147
148 let mut to_scan: Vec<FileWork> = Vec::new();
149
150 for (path, lang, _content, hash) in file_contents.iter() {
151 if !rules_changed {
152 if let Some(entry) = store.get(hash) {
153 cached_count += 1;
154 all_findings.extend(entry.findings);
155 if collect_symbols {
156 contributions.push((path.clone(), entry.symbols));
157 }
158 if collect_imports {
159 import_contributions.push((path.clone(), entry.imports));
160 }
161 continue;
162 }
163 }
164 changed_count += 1;
165 to_scan.push(FileWork {
166 path: path.clone(),
167 lang: *lang,
168 hash: hash.clone(),
169 });
170 }
171
172 let scanned: Vec<(PathBuf, String, FileScan)> = to_scan
173 .par_iter()
174 .map(|work| {
175 let scan = scan_file(
176 &work.path,
177 work.lang,
178 &per_file,
179 &compiled.test_paths,
180 collect_symbols,
181 collect_imports,
182 );
183 (work.path.clone(), work.hash.clone(), scan)
184 })
185 .collect();
186
187 for (path, hash, scan) in scanned {
188 let entry = CacheEntry {
189 findings: scan.findings,
190 symbols: scan.symbols,
191 imports: scan.imports,
192 };
193 store.put(&hash, &entry).ok();
194 all_findings.extend(entry.findings);
195 if collect_symbols {
196 contributions.push((path.clone(), entry.symbols));
197 }
198 if collect_imports {
199 import_contributions.push((path, entry.imports));
200 }
201 }
202
203 append_cross_file(&mut all_findings, &project, run_cross_file, &contributions);
207 append_resolution(&mut all_findings, compiled, &import_contributions);
208
209 if prune {
210 let current_hashes: Vec<String> =
211 file_contents.iter().map(|(_, _, _, h)| h.clone()).collect();
212 store.cleanup(¤t_hashes).ok();
213 }
214
215 normalize_findings(&mut all_findings);
216
217 let (errors, warnings) = count_severities(&all_findings);
218
219 let files_scanned = cached_count + changed_count;
220
221 ScanResult {
222 findings: all_findings,
223 stats: ScanStats {
224 errors,
225 warnings,
226 total: errors + warnings,
227 files_scanned,
228 baseline_filtered: 0,
229 diff_base: None,
230 files_changed: None,
231 },
232 cache_stats: Some(CacheStats {
233 cached: cached_count,
234 changed: changed_count,
235 }),
236 }
237}
238
239pub fn scan(paths: &[PathBuf], rules: &[Rule], config: &Config) -> Result<ScanResult, ScanError> {
241 let test_paths = TestPaths::new(&config.scan.test_paths)?;
242 let compiled = compile_rules(rules, test_paths)?;
243 let ignores = build_glob_set(&config.scan.ignores)?;
244 Ok(scan_collected(walk_files(paths, &ignores), &compiled, true))
245}
246
247pub fn scan_cached(
256 paths: &[PathBuf],
257 rules: &[Rule],
258 config: &Config,
259 cache_dir: &Path,
260) -> Result<ScanResult, ScanError> {
261 let test_paths = TestPaths::new(&config.scan.test_paths)?;
262 let compiled = compile_rules(rules, test_paths)?;
263 let ignores = build_glob_set(&config.scan.ignores)?;
264 Ok(scan_collected_cached(
265 walk_files(paths, &ignores),
266 &compiled,
267 rules,
268 cache_dir,
269 true,
270 true,
271 ))
272}
273
274pub fn scan_files(
284 files: &[PathBuf],
285 rules: &[Rule],
286 config: &Config,
287) -> Result<ScanResult, ScanError> {
288 let test_paths = TestPaths::new(&config.scan.test_paths)?;
289 let compiled = compile_rules(rules, test_paths)?;
290 let ignores = build_glob_set(&config.scan.ignores)?;
291 Ok(scan_collected(
292 explicit_files(files, &ignores),
293 &compiled,
294 false,
295 ))
296}
297
298pub fn scan_files_cached(
306 files: &[PathBuf],
307 rules: &[Rule],
308 config: &Config,
309 cache_dir: &Path,
310) -> Result<ScanResult, ScanError> {
311 let test_paths = TestPaths::new(&config.scan.test_paths)?;
312 let compiled = compile_rules(rules, test_paths)?;
313 let ignores = build_glob_set(&config.scan.ignores)?;
314 Ok(scan_collected_cached(
315 explicit_files(files, &ignores),
316 &compiled,
317 rules,
318 cache_dir,
319 false,
320 false,
321 ))
322}