1use std::path::{Path, PathBuf};
21
22fn nix_files(root: &Path, out: &mut Vec<PathBuf>) {
23 let Ok(entries) = std::fs::read_dir(root) else {
24 return;
25 };
26 for e in entries.flatten() {
27 let p = e.path();
28 let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
29 let Ok(ft) = e.file_type() else { continue };
34 if ft.is_symlink() {
35 continue;
36 }
37 if ft.is_dir() {
38 if matches!(name, ".git" | "target" | "result" | "node_modules") {
40 continue;
41 }
42 nix_files(&p, out);
43 } else if p.extension().and_then(|s| s.to_str()) == Some("nix") {
44 out.push(p);
45 }
46 }
47}
48
49fn main() {
50 let roots: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
51 if roots.is_empty() {
52 eprintln!("usage: scan <dir> [<dir>…]");
53 std::process::exit(2);
54 }
55
56 let mut files = Vec::new();
57 for r in &roots {
58 nix_files(r, &mut files);
59 }
60
61 let (mut unparseable, mut clean, mut planned, mut rejected) = (0usize, 0usize, 0usize, 0usize);
62 let mut groups = 0usize;
63 let mut rejects: Vec<(PathBuf, String)> = Vec::new();
64
65 for f in &files {
66 let Ok(src) = std::fs::read_to_string(f) else {
67 continue;
68 };
69 let parse = rnix::Root::parse(&src);
70 if !parse.errors().is_empty() {
71 unparseable += 1;
74 continue;
75 }
76 match sui_normalize::normalize(&parse.tree()) {
77 Ok(table) if table.is_empty() => clean += 1,
78 Ok(table) => {
79 planned += 1;
80 groups += table.len();
81 }
82 Err(e) => {
83 rejected += 1;
84 rejects.push((f.clone(), e.to_string()));
85 }
86 }
87 }
88
89 println!("scanned {}", files.len());
90 println!(" clean {clean} (no duplicate key, no dotted path — untouched)");
91 println!(" planned {planned} ({groups} binding groups — THE BLAST RADIUS)");
92 println!(" rejected {rejected} (must be 0 before the rejection tier flips)");
93 println!(" unparseable {unparseable} (rnix could not read; not this pass's business)");
94
95 for (p, e) in rejects.iter().take(25) {
96 println!(" REJECT {}: {e}", p.display());
97 }
98 if rejects.len() > 25 {
99 println!(" … and {} more", rejects.len() - 25);
100 }
101}