use super::ImportScopes;
use std::collections::BTreeMap;
use weavatrix_graph::NodeId;
pub(super) fn add_module_scopes(scopes: &mut ImportScopes, files: &BTreeMap<String, NodeId>) {
let roots = project_roots(files);
let swift = files
.keys()
.filter(|path| super::paths::has_extension(path, "swift"))
.map(|path| (path.clone(), unit(path, &roots)))
.collect::<Vec<_>>();
for (source, source_unit) in &swift {
let visible = scopes.files.entry(source.clone()).or_default();
for (peer, peer_unit) in &swift {
if peer != source && same_module(source_unit, peer_unit) {
visible.insert(peer.clone());
}
}
}
}
fn project_roots(files: &BTreeMap<String, NodeId>) -> Vec<String> {
let mut roots = files
.keys()
.filter_map(|path| {
let path = path.replace('\\', "/");
let (directory, name) = path.rsplit_once('/').unwrap_or(("", &path));
matches!(name, "project.yml" | "Project.swift" | "Package.swift")
.then(|| directory.to_owned())
})
.collect::<Vec<_>>();
roots.sort_by(|left, right| right.len().cmp(&left.len()).then_with(|| left.cmp(right)));
roots.dedup();
roots
}
fn same_module(left: &str, right: &str) -> bool {
if left == right {
return true;
}
let left_prod = production_unit(left);
let right_prod = production_unit(right);
if left_prod == right_prod {
return true;
}
shared_sibling(left, right) || shared_sibling(right, left)
}
fn unit(path: &str, roots: &[String]) -> String {
let path = path.replace('\\', "/");
if let Some(root) = roots.iter().find(|root| under_root(&path, root)) {
let below = if root.is_empty() {
path.as_str()
} else {
&path[root.len() + 1..]
};
let components = below.split('/').collect::<Vec<_>>();
let depth = if matches!(components.first().copied(), Some("Sources" | "Tests"))
&& components.len() > 2
{
2
} else {
1
};
let target = components[..depth.min(components.len())].join("/");
return if root.is_empty() {
target
} else {
format!("{root}/{target}")
};
}
if let Some(index) = path.find("/Features/") {
return path[..index].to_owned();
}
if let Some(index) = path.find("/Shared/") {
return path[..index + "/Shared".len()].to_owned();
}
path.rsplit_once('/')
.map(|(directory, _)| directory.to_owned())
.unwrap_or(path)
}
fn under_root(path: &str, root: &str) -> bool {
root.is_empty()
|| path
.strip_prefix(root)
.is_some_and(|rest| rest.starts_with('/'))
}
fn production_unit(unit: &str) -> String {
let unit = unit.strip_suffix("Tests").unwrap_or(unit);
if let Some(rest) = unit.strip_prefix("Tests/") {
return format!("Sources/{rest}");
}
match unit.rsplit_once("/Tests/") {
Some((parent, target)) => format!("{parent}/Sources/{target}"),
None => unit.to_owned(),
}
}
fn shared_sibling(shared: &str, other: &str) -> bool {
let Some(parent) = shared.strip_suffix("/Shared") else {
return false;
};
other == parent || other.starts_with(&format!("{parent}/"))
}