use crate::adapters::shared::cfg_test::has_cfg_test;
use crate::adapters::shared::use_tree::{AliasMap, 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 AliasMap,
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 workspace_module_paths: Option<&'a HashSet<Vec<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, AliasMap>,
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 workspace_module_paths: Option<&'a HashSet<Vec<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,
workspace_module_paths: inputs.workspace_module_paths,
},
);
}
out
}
pub(crate) fn collect_workspace_module_paths(files: &[(&str, &syn::File)]) -> HashSet<Vec<String>> {
let segs_to_path =
crate::adapters::analyzers::architecture::forbidden_rule::build_module_segs_to_path_map(
files,
);
let path_to_ast: HashMap<&str, &syn::File> = files.iter().map(|(p, ast)| (*p, *ast)).collect();
let ctx = ModuleWalkCtx {
segs_to_path: &segs_to_path,
path_to_ast: &path_to_ast,
};
let crate_root_paths: Vec<&str> = files
.iter()
.filter(|(p, _)| matches!(*p, "src/lib.rs" | "src/main.rs"))
.map(|(p, _)| *p)
.collect();
let mut out = HashSet::new();
if crate_root_paths.is_empty() {
walk_fallback_roots(files, &segs_to_path, &ctx, &mut out);
} else {
for root_path in &crate_root_paths {
let ast = path_to_ast[root_path];
out.insert(Vec::new());
walk_declared_module_paths(&ast.items, &[], &ctx, &mut out);
}
}
out
}
struct ModuleWalkCtx<'a> {
segs_to_path: &'a HashMap<Vec<String>, &'a str>,
path_to_ast: &'a HashMap<&'a str, &'a syn::File>,
}
fn walk_declared_module_paths(
items: &[syn::Item],
stack: &[String],
ctx: &ModuleWalkCtx<'_>,
out: &mut HashSet<Vec<String>>,
) {
let recurse = |inner: &[syn::Item], next: &[String], out: &mut HashSet<Vec<String>>| {
walk_declared_module_paths(inner, next, ctx, out);
};
for item in items {
let syn::Item::Mod(m) = item else {
continue;
};
if has_cfg_test(&m.attrs) {
continue;
}
let mut next = stack.to_vec();
next.push(m.ident.to_string());
out.insert(next.clone());
if let Some((_, inner)) = m.content.as_ref() {
recurse(inner, &next, out);
} else if let Some(child_ast) = file_backed_child_ast(&next, ctx) {
recurse(child_ast, &next, out);
}
}
}
fn file_backed_child_ast<'a>(next: &[String], ctx: &ModuleWalkCtx<'a>) -> Option<&'a [syn::Item]> {
let child_path = ctx.segs_to_path.get(next)?;
ctx.path_to_ast
.get(child_path)
.map(|ast| ast.items.as_slice())
}
fn walk_fallback_roots(
files: &[(&str, &syn::File)],
segs_to_path: &HashMap<Vec<String>, &str>,
ctx: &ModuleWalkCtx<'_>,
out: &mut HashSet<Vec<String>>,
) {
for (path, ast) in files {
let base =
crate::adapters::analyzers::architecture::forbidden_rule::file_to_module_segments(path);
if has_workspace_ancestor(&base, segs_to_path) {
continue;
}
if !crate::adapters::analyzers::architecture::forbidden_rule::is_tie_break_winner(
path,
&base,
segs_to_path,
) {
continue;
}
out.insert(base.clone());
walk_declared_module_paths(&ast.items, &base, ctx, out);
}
}
fn has_workspace_ancestor(base: &[String], segs_to_path: &HashMap<Vec<String>, &str>) -> bool {
(1..base.len()).any(|len| segs_to_path.contains_key(&base[..len] as &[String]))
}
pub(crate) struct WorkspaceLookup<'a> {
pub cfg_test_files: &'a HashSet<String>,
pub crate_root_modules: &'a HashSet<String>,
pub workspace_module_paths: &'a HashSet<Vec<String>>,
}
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,
}
}