use std::path::{Path, PathBuf};
fn nix_files(root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(root) else {
return;
};
for e in entries.flatten() {
let p = e.path();
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
let Ok(ft) = e.file_type() else { continue };
if ft.is_symlink() {
continue;
}
if ft.is_dir() {
if matches!(name, ".git" | "target" | "result" | "node_modules") {
continue;
}
nix_files(&p, out);
} else if p.extension().and_then(|s| s.to_str()) == Some("nix") {
out.push(p);
}
}
}
fn main() {
let roots: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
if roots.is_empty() {
eprintln!("usage: scan <dir> [<dir>…]");
std::process::exit(2);
}
let mut files = Vec::new();
for r in &roots {
nix_files(r, &mut files);
}
let (mut unparseable, mut clean, mut planned, mut rejected) = (0usize, 0usize, 0usize, 0usize);
let mut groups = 0usize;
let mut rejects: Vec<(PathBuf, String)> = Vec::new();
for f in &files {
let Ok(src) = std::fs::read_to_string(f) else {
continue;
};
let parse = rnix::Root::parse(&src);
if !parse.errors().is_empty() {
unparseable += 1;
continue;
}
match sui_normalize::normalize(&parse.tree()) {
Ok(table) if table.is_empty() => clean += 1,
Ok(table) => {
planned += 1;
groups += table.len();
}
Err(e) => {
rejected += 1;
rejects.push((f.clone(), e.to_string()));
}
}
}
println!("scanned {}", files.len());
println!(" clean {clean} (no duplicate key, no dotted path — untouched)");
println!(" planned {planned} ({groups} binding groups — THE BLAST RADIUS)");
println!(" rejected {rejected} (each MUST be one `nix-instantiate --parse` also refuses)");
println!(" unparseable {unparseable} (rnix could not read; not this pass's business)");
for (p, e) in rejects.iter().take(25) {
println!(" REJECT {}: {e}", p.display());
}
if rejects.len() > 25 {
println!(" … and {} more", rejects.len() - 25);
}
}