use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct Module {
pub name: String,
pub files: Vec<String>,
}
pub fn infer_modules(file_imports: &[(String, String)], all_files: &[String]) -> Vec<Module> {
let mut fan_in: HashMap<&str, HashSet<&str>> = HashMap::new();
for (from, to) in file_imports {
fan_in.entry(to.as_str()).or_default().insert(from.as_str());
}
let mut parent: HashMap<String, String> =
all_files.iter().map(|f| (f.clone(), f.clone())).collect();
merge_exclusive_deps(file_imports, &fan_in, &mut parent);
let mut groups: HashMap<String, Vec<String>> = HashMap::new();
for file in all_files {
groups
.entry(find(&parent, file))
.or_default()
.push(file.clone());
}
let mut dir_groups: HashMap<String, Vec<String>> = HashMap::new();
for (_, members) in groups {
dir_groups
.entry(common_prefix(&members))
.or_default()
.extend(members);
}
dir_groups
.into_values()
.map(|files| Module {
name: common_prefix(&files),
files,
})
.collect()
}
fn merge_exclusive_deps(
file_imports: &[(String, String)],
fan_in: &HashMap<&str, HashSet<&str>>,
parent: &mut HashMap<String, String>,
) {
loop {
let mut changed = false;
for (_, to) in file_imports {
let importers = match fan_in.get(to.as_str()) {
Some(s) if s.len() == 1 => s,
_ => continue,
};
let sole_importer = importers.iter().next().unwrap();
let root_a = find(parent, sole_importer);
let root_b = find(parent, to);
if root_a != root_b {
parent.insert(root_b, root_a.clone());
changed = true;
}
}
if !changed {
break;
}
}
}
fn find(parent: &HashMap<String, String>, x: &str) -> String {
let mut cur = x.to_string();
while let Some(p) = parent.get(&cur) {
if p == &cur {
break;
}
cur = p.clone();
}
cur
}
fn common_prefix(paths: &[String]) -> String {
if paths.len() <= 1 {
return paths
.first()
.and_then(|p| std::path::Path::new(p).parent())
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
}
let parts: Vec<Vec<&str>> = paths.iter().map(|p| p.split('/').collect()).collect();
let prefix: Vec<&str> = (0..)
.map_while(|i| {
let first = parts[0].get(i)?;
parts
.iter()
.all(|p| p.get(i) == Some(first))
.then_some(*first)
})
.collect();
if prefix.is_empty() {
"(root)".to_string()
} else {
prefix.join("/")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exclusive_dep_merges() {
let files = vec!["src/a.rs".into(), "src/b.rs".into(), "src/c.rs".into()];
let imports = vec![
("src/a.rs".into(), "src/b.rs".into()),
("src/a.rs".into(), "src/c.rs".into()),
("src/b.rs".into(), "src/c.rs".into()),
];
let modules = infer_modules(&imports, &files);
let ab_mod = modules
.iter()
.find(|m| m.files.contains(&"src/a.rs".to_string()));
assert!(ab_mod.is_some());
assert!(ab_mod.unwrap().files.contains(&"src/b.rs".to_string()));
}
#[test]
fn directory_fallback() {
let files = vec![
"src/core/x.rs".into(),
"src/core/y.rs".into(),
"src/util/z.rs".into(),
];
let imports = vec![];
let modules = infer_modules(&imports, &files);
let core_mod = modules
.iter()
.find(|m| m.files.contains(&"src/core/x.rs".to_string()));
assert!(core_mod.is_some());
assert!(
core_mod
.unwrap()
.files
.contains(&"src/core/y.rs".to_string())
);
}
}