use std::collections::HashSet;
use std::path::Path;
pub(crate) fn collect_cfg_test_file_paths(
parsed: &[(String, String, syn::File)],
) -> HashSet<String> {
let resolver = ChildPathResolver::from_parsed(parsed);
let mut set = direct_cfg_test_files(parsed, &resolver);
set.extend(inner_cfg_test_files(parsed));
set.extend(integration_test_files(parsed));
propagate_cfg_test_through_plain_mods(parsed, &resolver, &mut set);
set
}
fn inner_cfg_test_files(parsed: &[(String, String, syn::File)]) -> HashSet<String> {
parsed
.iter()
.filter(|(_, _, file)| super::cfg_test::has_cfg_test(&file.attrs))
.map(|(path, _, _)| path.clone())
.collect()
}
fn integration_test_files(parsed: &[(String, String, syn::File)]) -> HashSet<String> {
parsed
.iter()
.map(|(path, _, _)| path.as_str())
.filter(|p| p.starts_with("tests/"))
.map(String::from)
.collect()
}
struct ChildPathResolver<'a> {
known_paths: HashSet<&'a str>,
}
impl<'a> ChildPathResolver<'a> {
fn from_parsed(parsed: &'a [(String, String, syn::File)]) -> Self {
Self {
known_paths: parsed.iter().map(|(p, _, _)| p.as_str()).collect(),
}
}
fn resolve(&self, parent_path: &str, mod_item: &syn::ItemMod) -> Option<String> {
if let Some(explicit) = path_attribute(&mod_item.attrs) {
return self.resolve_explicit_path(parent_path, &explicit);
}
self.resolve_by_convention(parent_path, &mod_item.ident.to_string())
}
fn resolve_explicit_path(&self, parent_path: &str, relative: &str) -> Option<String> {
let parent_dir = Path::new(parent_path)
.parent()
.unwrap_or(Path::new(""))
.to_path_buf();
let candidate = parent_dir
.join(relative)
.to_string_lossy()
.replace('\\', "/");
self.known_paths
.contains(candidate.as_str())
.then_some(candidate)
}
fn resolve_by_convention(&self, parent_path: &str, mod_name: &str) -> Option<String> {
let parent = Path::new(parent_path);
let child_dir = if parent
.file_stem()
.is_some_and(|s| s == "mod" || s == "lib" || s == "main")
{
parent.parent().unwrap_or(Path::new("")).to_path_buf()
} else {
parent.with_extension("")
};
let candidate_file = child_dir
.join(format!("{mod_name}.rs"))
.to_string_lossy()
.into_owned();
let candidate_dir = child_dir
.join(mod_name)
.join("mod.rs")
.to_string_lossy()
.into_owned();
if self.known_paths.contains(candidate_file.as_str()) {
Some(candidate_file)
} else if self.known_paths.contains(candidate_dir.as_str()) {
Some(candidate_dir)
} else {
None
}
}
}
fn path_attribute(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().find_map(|attr| {
if !attr.path().is_ident("path") {
return None;
}
match &attr.meta {
syn::Meta::NameValue(nv) => match &nv.value {
syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Str(s) => Some(s.value()),
_ => None,
},
_ => None,
},
_ => None,
}
})
}
fn direct_cfg_test_files(
parsed: &[(String, String, syn::File)],
resolver: &ChildPathResolver<'_>,
) -> HashSet<String> {
let is_ext_cfg_test =
|m: &syn::ItemMod| m.content.is_none() && super::cfg_test::has_cfg_test(&m.attrs);
parsed
.iter()
.flat_map(|(path, _, file)| {
file.items
.iter()
.filter_map(move |item| match item {
syn::Item::Mod(m) if is_ext_cfg_test(m) => Some((path.as_str(), m)),
_ => None,
})
.collect::<Vec<_>>()
})
.filter_map(|(parent, m)| resolver.resolve(parent, m))
.collect()
}
fn propagate_cfg_test_through_plain_mods(
parsed: &[(String, String, syn::File)],
resolver: &ChildPathResolver<'_>,
set: &mut HashSet<String>,
) {
let path_to_file: std::collections::HashMap<&str, &syn::File> =
parsed.iter().map(|(p, _, f)| (p.as_str(), f)).collect();
let is_any_ext_mod = |m: &syn::ItemMod| m.content.is_none();
loop {
let new_children: Vec<String> = set
.iter()
.filter_map(|parent_path| {
path_to_file
.get(parent_path.as_str())
.map(|f| (parent_path, *f))
})
.flat_map(|(parent_path, file)| {
file.items
.iter()
.filter_map(|item| match item {
syn::Item::Mod(m) if is_any_ext_mod(m) => resolver.resolve(parent_path, m),
_ => None,
})
.collect::<Vec<_>>()
})
.filter(|child| !set.contains(child))
.collect();
if new_children.is_empty() {
break;
}
set.extend(new_children);
}
}