use crate::adapters::shared::cfg_test::has_cfg_test;
use crate::adapters::shared::use_tree::ScopedAliasMap;
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
#[derive(Debug, Default, Clone)]
pub(crate) struct LocalSymbols {
pub flat: HashSet<String>,
pub by_name: HashMap<String, Vec<Vec<String>>>,
}
pub(crate) struct FileScope<'a> {
pub path: &'a str,
pub alias_map: &'a HashMap<String, Vec<String>>,
pub aliases_per_scope: &'a ScopedAliasMap,
pub local_symbols: &'a HashSet<String>,
pub local_decl_scopes: &'a HashMap<String, Vec<Vec<String>>>,
pub crate_root_modules: &'a HashSet<String>,
}
pub(crate) struct WorkspaceFilesInputs<'a> {
pub files: &'a [(&'a str, &'a syn::File)],
pub cfg_test_files: &'a HashSet<String>,
pub aliases_per_file: &'a HashMap<String, HashMap<String, Vec<String>>>,
pub aliases_scoped_per_file: &'a HashMap<String, ScopedAliasMap>,
pub local_symbols_per_file: &'a HashMap<String, LocalSymbols>,
pub crate_root_modules: &'a HashSet<String>,
}
pub(crate) fn build_workspace_files_map<'a>(
inputs: WorkspaceFilesInputs<'a>,
) -> HashMap<String, FileScope<'a>> {
static EMPTY_SCOPED: OnceLock<ScopedAliasMap> = OnceLock::new();
let empty_scoped: &'static ScopedAliasMap = EMPTY_SCOPED.get_or_init(ScopedAliasMap::new);
let mut out = HashMap::new();
for (path, _) in inputs.files {
if inputs.cfg_test_files.contains(*path) {
continue;
}
let Some(alias_map) = inputs.aliases_per_file.get(*path) else {
continue;
};
let Some(local) = inputs.local_symbols_per_file.get(*path) else {
continue;
};
let aliases_per_scope = inputs
.aliases_scoped_per_file
.get(*path)
.unwrap_or(empty_scoped);
out.insert(
path.to_string(),
FileScope {
path,
alias_map,
aliases_per_scope,
local_symbols: &local.flat,
local_decl_scopes: &local.by_name,
crate_root_modules: inputs.crate_root_modules,
},
);
}
out
}
pub(crate) fn collect_local_symbols(ast: &syn::File) -> HashSet<String> {
let scoped = collect_local_symbols_scoped(ast);
scoped
.by_name
.into_iter()
.filter_map(|(name, scopes)| scopes.iter().any(|p| p.is_empty()).then_some(name))
.collect()
}
pub(crate) fn collect_local_symbols_scoped(ast: &syn::File) -> LocalSymbols {
let mut symbols = LocalSymbols::default();
walk_local_symbols(&ast.items, &mut Vec::new(), &mut symbols);
symbols
}
fn walk_local_symbols(items: &[syn::Item], mod_stack: &mut Vec<String>, out: &mut LocalSymbols) {
let recurse = |inner: &[syn::Item], stack: &mut Vec<String>, out: &mut LocalSymbols| {
walk_local_symbols(inner, stack, out);
};
for item in items {
if let Some(name) = item_name(item) {
out.flat.insert(name.clone());
out.by_name.entry(name).or_default().push(mod_stack.clone());
}
if let syn::Item::Mod(m) = item {
if !has_cfg_test(&m.attrs) {
if let Some((_, inner)) = m.content.as_ref() {
mod_stack.push(m.ident.to_string());
recurse(inner, mod_stack, out);
mod_stack.pop();
}
}
}
}
}
pub(crate) fn scope_for_local<'a>(
decl_scopes: &'a HashMap<String, Vec<Vec<String>>>,
name: &str,
mod_stack: &[String],
) -> Option<&'a [String]> {
if decl_scopes.is_empty() {
return Some(&[]);
}
let candidates = decl_scopes.get(name)?;
candidates
.iter()
.find(|path| path.as_slice() == mod_stack)
.map(Vec::as_slice)
}
fn item_name(item: &syn::Item) -> Option<String> {
match item {
syn::Item::Fn(f) => Some(f.sig.ident.to_string()),
syn::Item::Mod(m) => Some(m.ident.to_string()),
syn::Item::Struct(s) => Some(s.ident.to_string()),
syn::Item::Enum(e) => Some(e.ident.to_string()),
syn::Item::Union(u) => Some(u.ident.to_string()),
syn::Item::Trait(t) => Some(t.ident.to_string()),
syn::Item::Type(t) => Some(t.ident.to_string()),
syn::Item::Const(c) => Some(c.ident.to_string()),
syn::Item::Static(s) => Some(s.ident.to_string()),
_ => None,
}
}