use super::bindings::canonicalise_type_segments;
use super::calls::{collect_canonical_calls, FnContext};
use crate::adapters::analyzers::architecture::forbidden_rule::file_to_module_segments;
use crate::adapters::analyzers::architecture::layer_rule::LayerDefinitions;
use crate::adapters::shared::cfg_test::has_cfg_test;
use std::collections::{HashMap, HashSet, VecDeque};
use syn::visit::Visit;
pub(crate) struct CallGraph {
pub forward: HashMap<String, HashSet<String>>,
pub reverse: HashMap<String, HashSet<String>>,
pub layer_of: HashMap<String, Option<String>>,
}
impl CallGraph {
fn new() -> Self {
Self {
forward: HashMap::new(),
reverse: HashMap::new(),
layer_of: HashMap::new(),
}
}
pub fn layer_of(&self, canonical: &str) -> Option<&str> {
self.layer_of.get(canonical).and_then(Option::as_deref)
}
fn add_edge(&mut self, caller: &str, callee: &str) {
self.forward
.entry(caller.to_string())
.or_default()
.insert(callee.to_string());
self.reverse
.entry(callee.to_string())
.or_default()
.insert(caller.to_string());
}
fn add_node(&mut self, canonical: &str) {
self.forward.entry(canonical.to_string()).or_default();
}
}
pub(crate) fn canonical_name_for_pub_fn(info: &super::pub_fns::PubFnInfo<'_>) -> String {
canonical_fn_name(&info.file, info.self_type.as_deref(), &info.fn_name)
}
fn canonical_fn_name(file: &str, self_type: Option<&[String]>, fn_name: &str) -> String {
let mut segs: Vec<String> = Vec::new();
match self_type {
Some(impl_segs) if is_crate_rooted(impl_segs) => {
segs.extend(impl_segs.iter().cloned());
}
Some(impl_segs) => {
segs.push("crate".to_string());
segs.extend(file_to_module_segments(file));
segs.extend(impl_segs.iter().cloned());
}
None => {
segs.push("crate".to_string());
segs.extend(file_to_module_segments(file));
}
}
segs.push(fn_name.to_string());
segs.join("::")
}
fn is_crate_rooted(segments: &[String]) -> bool {
segments.first().map(|s| s.as_str()) == Some("crate")
}
pub(crate) fn collect_crate_root_modules(files: &[(&str, &syn::File)]) -> HashSet<String> {
files
.iter()
.filter_map(|(path, _)| crate_root_module_of(path))
.collect()
}
fn crate_root_module_of(path: &str) -> Option<String> {
let rest = path.strip_prefix("src/")?;
let first = rest.split('/').next()?;
let name = first.strip_suffix(".rs").unwrap_or(first);
if matches!(name, "lib" | "main") {
return None;
}
Some(name.to_string())
}
pub(crate) fn collect_local_symbols(ast: &syn::File) -> HashSet<String> {
ast.items
.iter()
.filter_map(|item| 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,
})
.collect()
}
pub(crate) fn extract_signature_params(sig: &syn::Signature) -> Vec<(String, &syn::Type)> {
sig.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Typed(pt) => match pt.pat.as_ref() {
syn::Pat::Ident(pi) => Some((pi.ident.to_string(), pt.ty.as_ref())),
_ => None,
},
_ => None,
})
.collect()
}
pub(crate) fn resolve_impl_self_type(
self_ty: &syn::Type,
alias_map: &HashMap<String, Vec<String>>,
local_symbols: &HashSet<String>,
crate_root_modules: &HashSet<String>,
importing_file: &str,
) -> Option<Vec<String>> {
let raw = impl_self_ty_segments(self_ty)?;
Some(
canonicalise_type_segments(
&raw,
alias_map,
local_symbols,
crate_root_modules,
importing_file,
)
.unwrap_or(raw),
)
}
pub(crate) fn impl_self_ty_segments(self_ty: &syn::Type) -> Option<Vec<String>> {
match self_ty {
syn::Type::Path(p) => Some(
p.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect(),
),
_ => None,
}
}
pub(crate) struct WalkState {
pub queue: VecDeque<(String, usize)>,
pub visited: HashSet<String>,
}
impl WalkState {
pub fn seeded(start: &str, direct: &HashSet<String>) -> Self {
let mut visited = HashSet::new();
visited.insert(start.to_string());
let mut queue = VecDeque::new();
for c in direct {
if visited.insert(c.clone()) {
queue.push_back((c.clone(), 1));
}
}
Self { queue, visited }
}
pub fn enqueue_unvisited(&mut self, nodes: &HashSet<String>, depth: usize) {
for c in nodes {
if self.visited.insert(c.clone()) {
self.queue.push_back((c.clone(), depth));
}
}
}
}
pub(crate) fn build_call_graph<'ast>(
files: &[(&'ast str, &'ast syn::File)],
aliases_per_file: &HashMap<String, HashMap<String, Vec<String>>>,
cfg_test_files: &HashSet<String>,
layers: &LayerDefinitions,
) -> CallGraph {
let crate_root_modules = collect_crate_root_modules(files);
let mut graph = CallGraph::new();
for (path, ast) in files {
if cfg_test_files.contains(*path) {
continue;
}
let Some(alias_map) = aliases_per_file.get(*path) else {
continue;
};
let local_symbols = collect_local_symbols(ast);
let mut collector = FileFnCollector {
path,
alias_map,
local_symbols: &local_symbols,
crate_root_modules: &crate_root_modules,
impl_type_stack: Vec::new(),
graph: &mut graph,
};
collector.visit_file(ast);
}
populate_layer_cache(&mut graph, layers);
graph
}
fn populate_layer_cache(graph: &mut CallGraph, layers: &LayerDefinitions) {
let mut canonicals: HashSet<String> = graph.forward.keys().cloned().collect();
for callees in graph.forward.values() {
canonicals.extend(callees.iter().cloned());
}
for canonical in canonicals {
let layer = layers.layer_of_crate_path(&canonical).map(String::from);
graph.layer_of.insert(canonical, layer);
}
}
struct FileFnCollector<'a> {
path: &'a str,
alias_map: &'a HashMap<String, Vec<String>>,
local_symbols: &'a HashSet<String>,
crate_root_modules: &'a HashSet<String>,
impl_type_stack: Vec<Option<Vec<String>>>,
graph: &'a mut CallGraph,
}
impl<'a> FileFnCollector<'a> {
fn record_fn<'ast>(
&mut self,
fn_name: &str,
sig: &'ast syn::Signature,
body: &'ast syn::Block,
) {
let self_type = match self.impl_type_stack.last() {
None => None,
Some(Some(segs)) => Some(segs.clone()),
Some(None) => return,
};
let canonical = canonical_fn_name(self.path, self_type.as_deref(), fn_name);
let ctx = FnContext {
body,
signature_params: extract_signature_params(sig),
self_type,
alias_map: self.alias_map,
local_symbols: self.local_symbols,
crate_root_modules: self.crate_root_modules,
importing_file: self.path,
};
let calls = collect_canonical_calls(&ctx);
self.graph.add_node(&canonical);
for callee in calls {
self.graph.add_edge(&canonical, &callee);
}
}
}
impl<'a, 'ast> Visit<'ast> for FileFnCollector<'a> {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
if has_cfg_test(&node.attrs) {
return;
}
let name = node.sig.ident.to_string();
self.record_fn(&name, &node.sig, &node.block);
syn::visit::visit_item_fn(self, node);
}
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
let resolved = resolve_impl_self_type(
&node.self_ty,
self.alias_map,
self.local_symbols,
self.crate_root_modules,
self.path,
);
self.impl_type_stack.push(resolved);
syn::visit::visit_item_impl(self, node);
self.impl_type_stack.pop();
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
if has_cfg_test(&node.attrs) {
return;
}
let name = node.sig.ident.to_string();
self.record_fn(&name, &node.sig, &node.block);
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;
}
syn::visit::visit_item_mod(self, node);
}
}