use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
use harn_parser::analysis::{AnalysisDatabase, SourceId, SourceVersion};
use crate::package;
use super::check_cmd::{check_file_report_inner, CheckFileReport, CheckTextOutput};
pub(crate) const CHECK_JOBS_ENV: &str = "HARN_CHECK_JOBS";
#[derive(Debug, Clone, Default)]
pub(crate) struct CheckCliOverrides {
pub host_capabilities: Option<String>,
pub bundle_root: Option<String>,
pub strict_types: bool,
pub preflight: Option<String>,
pub invariants: bool,
}
pub(crate) struct CheckedFile {
pub report: CheckFileReport,
pub strict: bool,
pub text: CheckTextOutput,
}
fn worker_count(files: usize) -> usize {
let configured = std::env::var(CHECK_JOBS_ENV)
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|&jobs| jobs > 0);
let default = std::thread::available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(1);
configured.unwrap_or(default).min(files.max(1))
}
pub(crate) fn check_files(
files: &[PathBuf],
module_graph: &harn_modules::ModuleGraph,
parsed_sources: HashMap<PathBuf, harn_modules::ParsedModuleSource>,
cross_file_imports: &HashSet<String>,
overrides: &CheckCliOverrides,
want_text: bool,
) -> Vec<CheckedFile> {
let workers = worker_count(files.len());
let parsed_sources = Mutex::new(parsed_sources);
let next = AtomicUsize::new(0);
let run_worker = || {
let mut analysis = AnalysisDatabase::new();
let mut produced: Vec<(usize, CheckedFile)> = Vec::new();
loop {
let index = next.fetch_add(1, Ordering::Relaxed);
let Some(file) = files.get(index) else {
break;
};
let checked = check_one(
&mut analysis,
file,
module_graph,
&parsed_sources,
cross_file_imports,
overrides,
want_text,
);
produced.push((index, checked));
}
produced
};
let mut merged: Vec<Option<CheckedFile>> = Vec::with_capacity(files.len());
merged.resize_with(files.len(), || None);
if workers <= 1 {
for (index, checked) in run_worker() {
merged[index] = Some(checked);
}
} else {
let produced = std::thread::scope(|scope| {
let handles: Vec<_> = (0..workers).map(|_| scope.spawn(run_worker)).collect();
handles
.into_iter()
.flat_map(|handle| match handle.join() {
Ok(produced) => produced,
Err(panic) => std::panic::resume_unwind(panic),
})
.collect::<Vec<_>>()
});
for (index, checked) in produced {
merged[index] = Some(checked);
}
}
merged
.into_iter()
.map(|slot| slot.expect("every input file produces exactly one check result"))
.collect()
}
fn check_one(
analysis: &mut AnalysisDatabase,
file: &Path,
module_graph: &harn_modules::ModuleGraph,
parsed_sources: &Mutex<HashMap<PathBuf, harn_modules::ParsedModuleSource>>,
cross_file_imports: &HashSet<String>,
overrides: &CheckCliOverrides,
want_text: bool,
) -> CheckedFile {
if let Some(parsed) = take_parsed_source(parsed_sources, file) {
analysis.set_parsed_source(
SourceId::path(file),
parsed.source,
SourceVersion(1),
parsed.program,
);
}
let mut config = package::load_check_config(Some(file));
if let Some(path) = overrides.host_capabilities.as_ref() {
config.host_capabilities_path = Some(path.clone());
}
if let Some(path) = overrides.bundle_root.as_ref() {
config.bundle_root = Some(path.clone());
}
if overrides.strict_types {
config.strict_types = true;
}
if let Some(severity) = overrides.preflight.as_deref() {
config.preflight_severity = Some(severity.to_string());
}
let mut text = want_text.then(CheckTextOutput::default);
let report = check_file_report_inner(
analysis,
file,
&config,
cross_file_imports,
module_graph,
overrides.invariants,
text.as_mut(),
);
CheckedFile {
report,
strict: config.strict,
text: text.unwrap_or_default(),
}
}
fn take_parsed_source(
parsed_sources: &Mutex<HashMap<PathBuf, harn_modules::ParsedModuleSource>>,
file: &Path,
) -> Option<harn_modules::ParsedModuleSource> {
let canonical = std::fs::canonicalize(file).ok()?;
parsed_sources
.lock()
.expect("parsed-source map lock poisoned")
.remove(&canonical)
}