use std::fs;
use std::path::{Path, PathBuf};
use ast_grep_language::SupportLang;
use rayon::prelude::*;
use crate::cache::{file_content_hash, rules_hash, CacheEntry, CacheStore};
use crate::config::Config;
use crate::cross_file::FileSymbols;
use crate::finding::{CacheStats, Finding, ScanResult, ScanStats};
use crate::resolution::FileImports;
use crate::rule::{Rule, Severity};
use crate::test_filter::TestPaths;
use super::{
append_cross_file, append_resolution, build_glob_set, compile_rules, explicit_files, scan_file,
walk_files, CompiledRules, FileScan, ScanError,
};
pub fn count_severities(findings: &[Finding]) -> (usize, usize) {
let mut errors = 0;
let mut warnings = 0;
for finding in findings {
match finding.severity {
Severity::Error => errors += 1,
Severity::Warning => warnings += 1,
}
}
(errors, warnings)
}
fn normalize_findings(findings: &mut Vec<Finding>) {
findings.sort_by(|a, b| (&a.file, a.line, a.column).cmp(&(&b.file, b.line, b.column)));
findings.dedup_by(|a, b| {
a.rule_id == b.rule_id && a.file == b.file && a.line == b.line && a.column == b.column
});
}
fn scan_collected(
files: Vec<(PathBuf, SupportLang)>,
compiled: &CompiledRules,
run_cross_file: bool,
) -> ScanResult {
let (per_file, project) = compiled.engines();
let collect_symbols = project.iter().any(|e| e.needs_symbols());
let collect_imports = !compiled.resolution.is_empty();
let scans: Vec<(PathBuf, FileScan)> = files
.par_iter()
.map(|(path, lang)| {
let scan = scan_file(
path,
*lang,
&per_file,
&compiled.test_paths,
collect_symbols,
collect_imports,
);
(path.clone(), scan)
})
.collect();
let mut findings: Vec<Finding> = Vec::new();
let mut contributions: Vec<(PathBuf, FileSymbols)> = Vec::new();
let mut import_contributions: Vec<(PathBuf, FileImports)> = Vec::new();
for (path, scan) in scans {
findings.extend(scan.findings);
if collect_symbols {
contributions.push((path.clone(), scan.symbols));
}
if collect_imports {
import_contributions.push((path, scan.imports));
}
}
append_cross_file(&mut findings, &project, run_cross_file, &contributions);
append_resolution(&mut findings, compiled, &import_contributions);
normalize_findings(&mut findings);
let (errors, warnings) = count_severities(&findings);
ScanResult {
findings,
stats: ScanStats {
errors,
warnings,
total: errors + warnings,
files_scanned: files.len(),
baseline_filtered: 0,
diff_base: None,
files_changed: None,
},
cache_stats: None,
}
}
fn scan_collected_cached(
files: Vec<(PathBuf, SupportLang)>,
compiled: &CompiledRules,
rules: &[Rule],
cache_dir: &Path,
prune: bool,
run_cross_file: bool,
) -> ScanResult {
let (per_file, project) = compiled.engines();
let collect_symbols = project.iter().any(|e| e.needs_symbols());
let collect_imports = !compiled.resolution.is_empty();
let current_rules_hash = rules_hash(rules);
let store = CacheStore::with_dir(cache_dir.to_path_buf()).scoped_to_rules(¤t_rules_hash);
let rules_changed = store
.check_rules_changed(¤t_rules_hash)
.unwrap_or(true);
let file_contents: Vec<(PathBuf, SupportLang, Vec<u8>, String)> = files
.into_iter()
.filter_map(|(path, lang)| {
let content = fs::read(&path).ok()?;
let hash = file_content_hash(&content);
Some((path, lang, content, hash))
})
.collect();
let mut cached_count = 0usize;
let mut changed_count = 0usize;
let mut all_findings: Vec<Finding> = Vec::new();
let mut contributions: Vec<(PathBuf, FileSymbols)> = Vec::new();
let mut import_contributions: Vec<(PathBuf, FileImports)> = Vec::new();
struct FileWork {
path: PathBuf,
lang: SupportLang,
hash: String,
}
let mut to_scan: Vec<FileWork> = Vec::new();
for (path, lang, _content, hash) in file_contents.iter() {
if !rules_changed {
if let Some(entry) = store.get(hash) {
cached_count += 1;
all_findings.extend(entry.findings);
if collect_symbols {
contributions.push((path.clone(), entry.symbols));
}
if collect_imports {
import_contributions.push((path.clone(), entry.imports));
}
continue;
}
}
changed_count += 1;
to_scan.push(FileWork {
path: path.clone(),
lang: *lang,
hash: hash.clone(),
});
}
let scanned: Vec<(PathBuf, String, FileScan)> = to_scan
.par_iter()
.map(|work| {
let scan = scan_file(
&work.path,
work.lang,
&per_file,
&compiled.test_paths,
collect_symbols,
collect_imports,
);
(work.path.clone(), work.hash.clone(), scan)
})
.collect();
for (path, hash, scan) in scanned {
let entry = CacheEntry {
findings: scan.findings,
symbols: scan.symbols,
imports: scan.imports,
};
store.put(&hash, &entry).ok();
all_findings.extend(entry.findings);
if collect_symbols {
contributions.push((path.clone(), entry.symbols));
}
if collect_imports {
import_contributions.push((path, entry.imports));
}
}
append_cross_file(&mut all_findings, &project, run_cross_file, &contributions);
append_resolution(&mut all_findings, compiled, &import_contributions);
if prune {
let current_hashes: Vec<String> =
file_contents.iter().map(|(_, _, _, h)| h.clone()).collect();
store.cleanup(¤t_hashes).ok();
}
normalize_findings(&mut all_findings);
let (errors, warnings) = count_severities(&all_findings);
let files_scanned = cached_count + changed_count;
ScanResult {
findings: all_findings,
stats: ScanStats {
errors,
warnings,
total: errors + warnings,
files_scanned,
baseline_filtered: 0,
diff_base: None,
files_changed: None,
},
cache_stats: Some(CacheStats {
cached: cached_count,
changed: changed_count,
}),
}
}
pub fn scan(paths: &[PathBuf], rules: &[Rule], config: &Config) -> Result<ScanResult, ScanError> {
let test_paths = TestPaths::new(&config.scan.test_paths)?;
let compiled = compile_rules(rules, test_paths)?;
let ignores = build_glob_set(&config.scan.ignores)?;
Ok(scan_collected(walk_files(paths, &ignores), &compiled, true))
}
pub fn scan_cached(
paths: &[PathBuf],
rules: &[Rule],
config: &Config,
cache_dir: &Path,
) -> Result<ScanResult, ScanError> {
let test_paths = TestPaths::new(&config.scan.test_paths)?;
let compiled = compile_rules(rules, test_paths)?;
let ignores = build_glob_set(&config.scan.ignores)?;
Ok(scan_collected_cached(
walk_files(paths, &ignores),
&compiled,
rules,
cache_dir,
true,
true,
))
}
pub fn scan_files(
files: &[PathBuf],
rules: &[Rule],
config: &Config,
) -> Result<ScanResult, ScanError> {
let test_paths = TestPaths::new(&config.scan.test_paths)?;
let compiled = compile_rules(rules, test_paths)?;
let ignores = build_glob_set(&config.scan.ignores)?;
Ok(scan_collected(
explicit_files(files, &ignores),
&compiled,
false,
))
}
pub fn scan_files_cached(
files: &[PathBuf],
rules: &[Rule],
config: &Config,
cache_dir: &Path,
) -> Result<ScanResult, ScanError> {
let test_paths = TestPaths::new(&config.scan.test_paths)?;
let compiled = compile_rules(rules, test_paths)?;
let ignores = build_glob_set(&config.scan.ignores)?;
Ok(scan_collected_cached(
explicit_files(files, &ignores),
&compiled,
rules,
cache_dir,
false,
false,
))
}