use super::file::{SkipReason, audit_file_inline, collect_file_facts, process_file};
use super::summary::build_language_summary;
use super::walker::{build_walker, collect_paths};
use crate::audits::traits::FileAudit;
use crate::findings::types::Finding;
use crate::scan::config::ScanConfig;
use crate::scan::facts::ScanFacts;
use rayon::prelude::*;
use std::collections::HashMap;
use std::io;
use std::path::Path;
pub(super) fn collect_and_audit_inline(
path: &Path,
config: &ScanConfig,
file_audits: &[Box<dyn FileAudit>],
) -> io::Result<(ScanFacts, Vec<Finding>)> {
ensure_path_exists(path)?;
let mut facts = ScanFacts {
root_path: path.to_path_buf(),
..ScanFacts::default()
};
if path.is_file() {
facts.files_discovered = 1;
let mut languages: HashMap<String, usize> = HashMap::new();
let mut findings: Vec<Finding> = Vec::new();
audit_file_inline(
path,
&mut facts,
&mut languages,
file_audits,
config,
&mut findings,
)?;
facts.languages = build_language_summary(languages);
return Ok((facts, findings));
}
let (mut file_paths, dirs_count) = collect_paths(path, config)?;
facts.files_discovered = file_paths.len();
if let Some(max) = config.max_files {
file_paths.truncate(max);
}
facts.directories_count = dirs_count;
let results: Vec<io::Result<_>> = file_paths
.par_iter()
.map(|p| process_file(p, file_audits, config))
.collect();
let mut languages: HashMap<String, usize> = HashMap::new();
let mut findings: Vec<Finding> = Vec::new();
for result in results {
let per_file = result?;
if per_file.skip_reason == SkipReason::None {
facts.files_count += 1;
if let Some(ref lang) = per_file.language {
*languages.entry(lang.clone()).or_insert(0) += 1;
}
}
facts.lines_of_code += per_file.file_facts.lines_of_code;
match per_file.skip_reason {
SkipReason::LargeFile => {
facts.skipped_files_count += 1;
facts.skipped_bytes = facts.skipped_bytes.saturating_add(per_file.skipped_bytes);
}
SkipReason::Binary => {
facts.binary_files_skipped += 1;
facts.skipped_bytes = facts.skipped_bytes.saturating_add(per_file.skipped_bytes);
}
SkipReason::LowSignal => {
facts.files_skipped_low_signal += 1;
}
SkipReason::None => {}
}
facts.files.push(per_file.file_facts);
findings.extend(per_file.findings);
}
facts.languages = build_language_summary(languages);
Ok((facts, findings))
}
pub fn collect_scan_facts(path: &Path) -> io::Result<ScanFacts> {
collect_scan_facts_with_config(path, &ScanConfig::default())
}
pub fn collect_scan_facts_with_config(path: &Path, config: &ScanConfig) -> io::Result<ScanFacts> {
ensure_path_exists(path)?;
let mut facts = ScanFacts {
root_path: path.to_path_buf(),
..ScanFacts::default()
};
let mut languages: HashMap<String, usize> = HashMap::new();
if path.is_file() {
facts.files_discovered = 1;
collect_file_facts(path, &mut facts, &mut languages, config)?;
} else {
collect_directory_facts(path, &mut facts, &mut languages, config)?;
}
facts.languages = build_language_summary(languages);
Ok(facts)
}
fn collect_directory_facts(
path: &Path,
facts: &mut ScanFacts,
languages: &mut HashMap<String, usize>,
config: &ScanConfig,
) -> io::Result<()> {
let walker = build_walker(path, config);
let mut files_discovered = 0usize;
for result in walker {
let entry = result.map_err(io::Error::other)?;
let entry_path = entry.path();
if entry_path == path {
continue;
}
let Some(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() {
facts.directories_count += 1;
continue;
}
if file_type.is_file() {
files_discovered += 1;
collect_file_facts(entry_path, facts, languages, config)?;
}
}
facts.files_discovered = files_discovered;
Ok(())
}
fn ensure_path_exists(path: &Path) -> io::Result<()> {
if path.exists() {
return Ok(());
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("path does not exist: {}", path.display()),
))
}