use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use futures::stream::{self, StreamExt};
use crate::analysis::findings::Finding;
use crate::analysis::result::{FailureReason, union_failures};
use crate::cli::check::input::Work;
use crate::languages;
use crate::languages::runner::{self};
use crate::languages::spec::ToolSpec;
const TOOL_PROCESS_CONCURRENCY: usize = 4;
pub async fn run(
work: &Work,
root: &Path,
) -> (
Vec<Finding>,
BTreeMap<PathBuf, FailureReason>,
BTreeSet<PathBuf>,
) {
let mut failures: BTreeMap<PathBuf, FailureReason> = BTreeMap::new();
let tasks = plan_tasks(work, root);
let (serial, parallel): (Vec<_>, Vec<_>) = tasks
.into_iter()
.partition(|task| task.spec.serial_in_repository);
let parallel = stream::iter(parallel)
.map(|task| run_one(task, root))
.buffer_unordered(TOOL_PROCESS_CONCURRENCY)
.collect::<Vec<_>>();
let serial = async {
let mut outcomes = Vec::with_capacity(serial.len());
for task in serial {
outcomes.push(run_one(task, root).await);
}
outcomes
};
let (mut outcomes, serial_outcomes) = tokio::join!(parallel, serial);
outcomes.extend(serial_outcomes);
let mut findings = Vec::new();
let mut compiled = BTreeSet::new();
for (outcome, files) in outcomes {
merge_outcome(outcome, files, &mut failures, &mut findings, &mut compiled);
}
(findings, failures, compiled)
}
struct PlannedTask {
spec: &'static ToolSpec,
workspace_root: PathBuf,
files: Vec<PlannedFile>,
}
struct PlannedFile {
original: PathBuf,
absolute: PathBuf,
argument: String,
}
async fn run_one(task: PlannedTask, root: &Path) -> (runner::ToolOutcome, Vec<PathBuf>) {
let PlannedTask {
spec,
workspace_root,
files,
} = task;
let mut arguments = Vec::with_capacity(files.len());
let mut originals = Vec::with_capacity(files.len());
let mut original_by_absolute = BTreeMap::new();
for file in files {
arguments.push(file.argument);
originals.push(file.original.clone());
original_by_absolute.insert(file.absolute, file.original);
}
let mut outcome = runner::run_tool_at(spec, root, &workspace_root, &arguments).await;
for finding in &mut outcome.findings {
let reported = Path::new(&finding.file_path)
.strip_prefix(".")
.unwrap_or_else(|_| Path::new(&finding.file_path));
let absolute = if reported.is_absolute() {
reported.to_path_buf()
} else {
workspace_root.join(reported)
};
if let Some(original) = original_by_absolute.get(&absolute) {
finding.file_path = original.to_string_lossy().into_owned();
}
}
(outcome, originals)
}
fn plan_tasks(work: &Work, root: &Path) -> Vec<PlannedTask> {
let paths: Vec<&Path> = work
.by_file
.iter()
.filter_map(|hunks| hunks.first())
.map(|hunk| hunk.file_path.as_path())
.chain(work.lint_only.iter().map(PathBuf::as_path))
.collect();
let repository_root = runner::absolute(root);
languages::group_by_language(&paths)
.into_iter()
.flat_map(|(language, files)| {
language.tools.iter().flat_map({
let repository_root = repository_root.clone();
move |spec| {
let mut workspaces: BTreeMap<PathBuf, Vec<PlannedFile>> = BTreeMap::new();
for file in &files {
let Some(workspace_root) =
runner::configuration_root(spec, &repository_root, file)
else {
continue;
};
let absolute = if file.is_absolute() {
(*file).to_path_buf()
} else {
repository_root.join(file)
};
let Ok(relative) = absolute.strip_prefix(&workspace_root) else {
continue;
};
let argument = relative.to_string_lossy().into_owned();
workspaces
.entry(workspace_root)
.or_default()
.push(PlannedFile {
original: (*file).to_path_buf(),
absolute,
argument,
});
}
workspaces
.into_iter()
.map(move |(workspace_root, files)| PlannedTask {
spec,
workspace_root,
files,
})
}
})
})
.collect()
}
fn merge_outcome(
outcome: runner::ToolOutcome,
files: Vec<PathBuf>,
failures: &mut BTreeMap<PathBuf, FailureReason>,
findings: &mut Vec<Finding>,
compiled: &mut BTreeSet<PathBuf>,
) {
if outcome.compilation_succeeded {
compiled.extend(files.iter().cloned());
}
match outcome.status {
runner::ToolStatus::Ok => {
findings.extend(outcome.findings);
}
runner::ToolStatus::Skipped => {
}
runner::ToolStatus::Unavailable => {
let reason = FailureReason::ToolUnavailable {
tool: outcome.tool.to_owned(),
detail: outcome.detail,
};
let batch: BTreeMap<PathBuf, FailureReason> = files
.into_iter()
.map(|file| (file, reason.clone()))
.collect();
union_failures(failures, batch);
}
}
}