use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
use runemark::Verdict;
use crate::manifest::Manifest;
use crate::workspace::Member;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Check {
pub name: &'static str,
pub tool: &'static str,
pub scope: Option<String>,
program: PathBuf,
args: Vec<&'static str>,
dir: PathBuf,
}
impl Check {
pub fn command_line(&self) -> String {
let program = self.program.file_name().map_or_else(
|| self.tool.to_owned(),
|name| name.to_string_lossy().into_owned(),
);
std::iter::once(program)
.chain(self.args.iter().map(|arg| (*arg).to_owned()))
.collect::<Vec<_>>()
.join(" ")
}
pub fn label(&self) -> String {
match &self.scope {
Some(scope) => format!("{} ({scope})", self.name),
None => self.name.to_owned(),
}
}
}
#[derive(Debug, Clone)]
pub struct Report {
pub check: Check,
pub verdict: Verdict,
pub duration: Duration,
pub output: String,
}
impl Report {
pub fn passed(&self) -> bool {
self.verdict == Verdict::Passed
}
pub fn unusable(&self) -> bool {
self.verdict == Verdict::ActionRequired
}
}
struct Candidate {
name: &'static str,
tool: &'static str,
package: &'static str,
args: &'static [&'static str],
}
const CANDIDATES: &[Candidate] = &[
Candidate {
name: "TypeScript",
tool: "astro",
package: "@astrojs/check",
args: &["check"],
},
Candidate {
name: "TypeScript",
tool: "tsc",
package: "typescript",
args: &["--noEmit"],
},
Candidate {
name: "Lint & format",
tool: "biome",
package: "@biomejs/biome",
args: &["check", "."],
},
Candidate {
name: "Lint",
tool: "eslint",
package: "eslint",
args: &["."],
},
Candidate {
name: "Format",
tool: "prettier",
package: "prettier",
args: &["--check", "."],
},
Candidate {
name: "Tests",
tool: "vitest",
package: "vitest",
args: &["run"],
},
Candidate {
name: "Tests",
tool: "jest",
package: "jest",
args: &[],
},
Candidate {
name: "Dead code",
tool: "fallow",
package: "fallow",
args: &["dead-code"],
},
Candidate {
name: "Dead code",
tool: "knip",
package: "knip",
args: &[],
},
Candidate {
name: "Secrets",
tool: "nosecrets",
package: "@casoon/nosecrets",
args: &["scan", "."],
},
Candidate {
name: "Secrets",
tool: "nosecrets",
package: "nosecrets",
args: &["scan", "."],
},
Candidate {
name: "Secrets",
tool: "secretlint",
package: "secretlint",
args: &["**/*"],
},
];
impl Check {
pub fn detect_all(
manifest: &Manifest,
members: &[Member],
rust_root: Option<&Path>,
root: &Path,
) -> Vec<Self> {
let mut checks = Self::detect_in(manifest, root, None);
for member in members {
checks.extend(Self::detect_in(
&member.manifest,
&member.path,
Some(member.name.clone()),
));
}
if let Some(rust_root) = rust_root {
checks.extend(Self::cargo(rust_root));
}
checks
}
fn cargo(root: &Path) -> Vec<Self> {
crate::cargo::CHECKS
.iter()
.map(|(name, args)| Self {
name,
tool: "cargo",
scope: Some("rust".to_owned()),
program: PathBuf::from("cargo"),
args: args.to_vec(),
dir: root.to_path_buf(),
})
.collect()
}
fn detect_in(manifest: &Manifest, dir: &Path, scope: Option<String>) -> Vec<Self> {
let mut checks: Vec<Self> = Vec::new();
for candidate in CANDIDATES {
if checks.iter().any(|check| check.name == candidate.name) {
continue;
}
if !manifest.depends_on(candidate.package) {
continue;
}
let Some(program) = binary(dir, candidate.tool) else {
continue;
};
checks.push(Self {
name: candidate.name,
tool: candidate.tool,
scope: scope.clone(),
program,
args: candidate.args.to_vec(),
dir: dir.to_path_buf(),
});
}
checks
}
pub fn run(self) -> Report {
let started = Instant::now();
let result = Command::new(&self.program)
.args(&self.args)
.current_dir(&self.dir)
.output();
let (verdict, output) = match result {
Ok(output) => {
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
text.push_str(&String::from_utf8_lossy(&output.stderr));
let verdict = if output.status.success() {
Verdict::Passed
} else {
Verdict::Failed
};
(verdict, text)
}
Err(error) => (
Verdict::ActionRequired,
format!("could not run {}: {error}", self.tool),
),
};
Report {
check: self,
verdict,
duration: started.elapsed(),
output: output.trim_end().to_owned(),
}
}
}
fn binary(root: &Path, tool: &str) -> Option<PathBuf> {
root.ancestors()
.map(|dir| dir.join("node_modules").join(".bin").join(tool))
.find(|path| path.exists())
}
pub fn run_all(checks: Vec<Check>, mut on_report: impl FnMut(&Report)) -> Vec<Report> {
use std::sync::mpsc;
let limit = std::thread::available_parallelism()
.map_or(2, std::num::NonZero::get)
.clamp(1, 4);
let mut reports = Vec::with_capacity(checks.len());
let mut queue = checks.into_iter();
let (sender, receiver) = mpsc::channel();
let mut running = 0usize;
std::thread::scope(|scope| {
loop {
while running < limit {
let Some(check) = queue.next() else { break };
let sender = sender.clone();
running += 1;
scope.spawn(move || {
let _ = sender.send(check.run());
});
}
if running == 0 {
break;
}
let Ok(report) = receiver.recv() else { break };
running -= 1;
on_report(&report);
reports.push(report);
}
});
reports.sort_by_key(|report| report.check.label());
reports
}