use std::path::Path;
pub fn declared_modules(main_text: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for line in main_text.lines() {
let t = line.trim();
let rest = t.strip_prefix("pub mod ").or_else(|| t.strip_prefix("mod "));
if let Some(rest) = rest {
if let Some(name) = rest.strip_suffix(';') {
let name = name.trim();
if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
out.push(name.to_string());
}
}
}
}
out
}
pub fn check(main_text: &str, file_stems: &[String]) -> (Vec<String>, Vec<String>) {
let declared = declared_modules(main_text);
let mut orphaned: Vec<String> = file_stems
.iter()
.filter(|s| s.as_str() != "main" && !declared.contains(s))
.cloned()
.collect();
let mut missing: Vec<String> = declared
.iter()
.filter(|d| !file_stems.contains(d))
.cloned()
.collect();
orphaned.sort();
missing.sort();
(orphaned, missing)
}
pub fn check_dir(src_dir: &Path) -> Result<(Vec<String>, Vec<String>, usize), String> {
let main_path = src_dir.join("main.rs");
let main_text = std::fs::read_to_string(&main_path)
.map_err(|e| format!("{}: {}", main_path.display(), e))?;
let mut stems: Vec<String> = Vec::new();
let rd = std::fs::read_dir(src_dir).map_err(|e| format!("{}: {}", src_dir.display(), e))?;
for ent in rd {
let ent = ent.map_err(|e| format!("{}: {}", src_dir.display(), e))?;
let p = ent.path();
if p.extension().and_then(|x| x.to_str()) == Some("rs") {
if let Some(stem) = p.file_stem().and_then(|x| x.to_str()) {
stems.push(stem.to_string());
}
}
}
let n = stems.len();
let (orphaned, missing) = check(&main_text, &stems);
Ok((orphaned, missing, n))
}
#[cfg(test)]
mod tests {
use super::*;
fn stems(names: &[&str]) -> Vec<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn exact_wiring_is_clean() {
let main = "mod cli;\nmod core;\nmod plan;\nfn main() {}\n";
let (o, m) = check(main, &stems(&["main", "cli", "core", "plan"]));
assert!(o.is_empty() && m.is_empty());
}
#[test]
fn standalone_root_orphans_the_v3_modules() {
let main = "mod plan;\nfn main() {}\n";
let files = stems(&[
"main", "cli", "coord", "core", "crypto", "hash", "http", "mcp", "personas",
"phext", "plan", "state", "store", "tools", "vol", "xz",
]);
let (o, m) = check(main, &files);
assert_eq!(o.len(), 14);
assert!(o.contains(&"core".to_string()));
assert!(o.contains(&"tools".to_string()));
assert!(!o.contains(&"plan".to_string()));
assert!(m.is_empty());
}
#[test]
fn declaration_without_file_is_missing() {
let main = "mod cli;\nmod ghost;\n";
let (o, m) = check(main, &stems(&["main", "cli"]));
assert!(o.is_empty());
assert_eq!(m, vec!["ghost".to_string()]);
}
#[test]
fn declared_modules_parsing() {
let main = "// mod commented;\nmod cli;\npub mod core;\n mod plan; \nmod not_a_decl\nlet x = \"mod fake;\";\n";
assert_eq!(declared_modules(main), vec!["cli".to_string(), "core".to_string(), "plan".to_string()]);
}
}