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(integration_test_files(parsed));
propagate_cfg_test_through_plain_mods(parsed, &resolver, &mut set);
set
}
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_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 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(|item| match item {
syn::Item::Mod(m) if is_ext_cfg_test(m) => {
Some((path.as_str(), m.ident.to_string()))
}
_ => None,
})
.collect::<Vec<_>>()
})
.filter_map(|(parent, name)| resolver.resolve(parent, &name))
.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.ident.to_string())
}
_ => None,
})
.collect::<Vec<_>>()
})
.filter(|child| !set.contains(child))
.collect();
if new_children.is_empty() {
break;
}
set.extend(new_children);
}
}