use super::bindings::CanonScope;
use super::file_visibility::collect_file_root_visibility;
use super::local_symbols::{collect_local_symbols_scoped, FileScope, LocalSymbols};
use super::pub_fns_visibility::{collect_visible_type_canonicals_workspace, is_visible};
use super::signature_params::extract_signature_params;
use super::workspace_graph::{collect_crate_root_modules, resolve_impl_self_type};
use crate::adapters::analyzers::architecture::layer_rule::LayerDefinitions;
use crate::adapters::shared::cfg_test::{has_cfg_test, has_test_attr};
use crate::adapters::shared::use_tree::gather_alias_map_scoped;
use std::collections::{HashMap, HashSet};
use syn::visit::Visit;
pub(crate) struct PubFnInfo<'ast> {
pub file: String,
pub fn_name: String,
pub line: usize,
pub body: &'ast syn::Block,
pub signature_params: Vec<(String, &'ast syn::Type)>,
pub self_type: Option<Vec<String>>,
pub mod_stack: Vec<String>,
pub deprecated: bool,
}
pub(crate) fn collect_pub_fns_by_layer<'ast>(
files: &[(&'ast str, &'ast syn::File)],
aliases_per_file: &HashMap<String, HashMap<String, Vec<String>>>,
layers: &LayerDefinitions,
cfg_test_files: &HashSet<String>,
transparent_wrappers: &HashSet<String>,
) -> HashMap<String, Vec<PubFnInfo<'ast>>> {
let crate_root_modules = collect_crate_root_modules(files);
let file_root_visibility = collect_file_root_visibility(files);
let visible_canonicals = collect_visible_type_canonicals_workspace(
files,
cfg_test_files,
aliases_per_file,
&crate_root_modules,
transparent_wrappers,
);
let empty_aliases = HashMap::new();
let mut out: HashMap<String, Vec<PubFnInfo<'ast>>> = HashMap::new();
for (path, ast) in files {
if cfg_test_files.contains(*path) {
continue;
}
let Some(layer) = layers.layer_for_file(path) else {
continue;
};
let layer = layer.to_string();
let alias_map = aliases_per_file.get(*path).unwrap_or(&empty_aliases);
let LocalSymbols { flat, by_name } = collect_local_symbols_scoped(ast);
let aliases_per_scope = gather_alias_map_scoped(ast);
let file = FileScope {
path,
alias_map,
aliases_per_scope: &aliases_per_scope,
local_symbols: &flat,
local_decl_scopes: &by_name,
crate_root_modules: &crate_root_modules,
};
let file_visible = file_root_visibility.get(*path).copied().unwrap_or(true);
let mut collector = PubFnCollector {
file_path: path.to_string(),
file: &file,
found: Vec::new(),
visible_canonicals: &visible_canonicals,
impl_stack: Vec::new(),
mod_stack: Vec::new(),
enclosing_mod_visible: file_visible,
};
collector.visit_file(ast);
out.entry(layer).or_default().extend(collector.found);
}
out
}
struct PubFnCollector<'ast, 'vis> {
file_path: String,
file: &'vis FileScope<'vis>,
found: Vec<PubFnInfo<'ast>>,
visible_canonicals: &'vis HashSet<String>,
impl_stack: Vec<(Vec<String>, bool, bool)>,
mod_stack: Vec<String>,
enclosing_mod_visible: bool,
}
impl<'ast, 'vis> PubFnCollector<'ast, 'vis> {
fn current_self_type(&self) -> Option<Vec<String>> {
self.impl_stack.last().map(|(segs, _, _)| segs.clone())
}
fn current_impl_visible(&self) -> bool {
self.impl_stack.last().map(|(_, v, _)| *v).unwrap_or(false)
}
fn current_impl_is_visible_trait(&self) -> bool {
self.impl_stack.last().map(|(_, _, t)| *t).unwrap_or(false)
}
fn record_fn(
&mut self,
name: String,
line: usize,
body: &'ast syn::Block,
sig: &'ast syn::Signature,
attrs: &[syn::Attribute],
) {
self.found.push(PubFnInfo {
file: self.file_path.clone(),
fn_name: name,
line,
body,
signature_params: extract_signature_params(sig),
self_type: self.current_self_type(),
mod_stack: self.mod_stack.clone(),
deprecated: has_deprecated_attribute(attrs),
});
}
}
fn is_test_fn(attrs: &[syn::Attribute]) -> bool {
has_test_attr(attrs) || has_cfg_test(attrs)
}
fn has_deprecated_attribute(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|a| a.path().is_ident("deprecated"))
}
impl<'ast, 'vis> Visit<'ast> for PubFnCollector<'ast, 'vis> {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
if self.enclosing_mod_visible && is_visible(&node.vis) && !is_test_fn(&node.attrs) {
let line = syn::spanned::Spanned::span(&node.sig.ident).start().line;
let name = node.sig.ident.to_string();
self.record_fn(name, line, &node.block, &node.sig, &node.attrs);
}
syn::visit::visit_item_fn(self, node);
}
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
if has_cfg_test(&node.attrs) {
return;
}
let scope = CanonScope {
file: self.file,
mod_stack: &self.mod_stack,
};
let canonical_segs = resolve_impl_self_type(&node.self_ty, &scope).unwrap_or_default();
let visible = !canonical_segs.is_empty()
&& self.visible_canonicals.contains(&canonical_segs.join("::"));
let is_visible_trait_impl =
is_impl_for_visible_trait(node, &scope, self.visible_canonicals);
self.impl_stack
.push((canonical_segs, visible, is_visible_trait_impl));
syn::visit::visit_item_impl(self, node);
self.impl_stack.pop();
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
let method_visible = is_visible(&node.vis) || self.current_impl_is_visible_trait();
if self.current_impl_visible() && method_visible && !is_test_fn(&node.attrs) {
let line = syn::spanned::Spanned::span(&node.sig.ident).start().line;
let name = node.sig.ident.to_string();
self.record_fn(name, line, &node.block, &node.sig, &node.attrs);
}
syn::visit::visit_impl_item_fn(self, node);
}
fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
if has_cfg_test(&node.attrs) {
return;
}
let parent_visible = self.enclosing_mod_visible;
self.enclosing_mod_visible = parent_visible && is_visible(&node.vis);
self.mod_stack.push(node.ident.to_string());
syn::visit::visit_item_mod(self, node);
self.mod_stack.pop();
self.enclosing_mod_visible = parent_visible;
}
}
fn is_impl_for_visible_trait(
node: &syn::ItemImpl,
scope: &CanonScope<'_>,
visible_canonicals: &HashSet<String>,
) -> bool {
use crate::adapters::analyzers::architecture::call_parity_rule::bindings::canonicalise_type_segments_in_scope;
let Some((_, trait_path, _)) = node.trait_.as_ref() else {
return false;
};
let segs: Vec<String> = trait_path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
let Some(canonical) = canonicalise_type_segments_in_scope(&segs, scope) else {
return false;
};
visible_canonicals.contains(&canonical.join("::"))
}