use super::bindings::{canonicalise_type_segments_in_scope, CanonScope};
use super::calls::{collect_canonical_calls, FnContext};
use super::signature_params::extract_signature_params;
use super::type_infer::{build_workspace_type_index, WorkspaceIndexInputs, WorkspaceTypeIndex};
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, has_test_attr};
use crate::adapters::shared::use_tree::gather_alias_map_scoped;
use crate::adapters::shared::use_tree::ScopedAliasMap;
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.mod_stack,
&info.fn_name,
)
}
fn canonical_fn_name(
file: &str,
self_type: Option<&[String]>,
mod_stack: &[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(mod_stack.iter().cloned());
segs.extend(impl_segs.iter().cloned());
}
None => {
segs.push("crate".to_string());
segs.extend(file_to_module_segments(file));
segs.extend(mod_stack.iter().cloned());
}
}
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) use super::local_symbols::{
build_workspace_files_map, collect_local_symbols, collect_local_symbols_scoped, FileScope,
LocalSymbols,
};
pub(crate) fn resolve_impl_self_type(
self_ty: &syn::Type,
scope: &CanonScope<'_>,
) -> Option<Vec<String>> {
let raw = impl_self_ty_segments(self_ty)?;
Some(canonicalise_type_segments_in_scope(&raw, scope).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,
transparent_wrappers: &HashSet<String>,
) -> CallGraph {
let crate_root_modules = collect_crate_root_modules(files);
let local_symbols_per_file: HashMap<String, LocalSymbols> = files
.iter()
.filter(|(p, _)| !cfg_test_files.contains(*p))
.map(|(path, ast)| (path.to_string(), collect_local_symbols_scoped(ast)))
.collect();
let aliases_scoped_per_file: HashMap<String, ScopedAliasMap> = files
.iter()
.filter(|(p, _)| !cfg_test_files.contains(*p))
.map(|(path, ast)| (path.to_string(), gather_alias_map_scoped(ast)))
.collect();
let workspace_files = build_workspace_files_map(super::local_symbols::WorkspaceFilesInputs {
files,
cfg_test_files,
aliases_per_file,
aliases_scoped_per_file: &aliases_scoped_per_file,
local_symbols_per_file: &local_symbols_per_file,
crate_root_modules: &crate_root_modules,
});
let type_index = build_workspace_type_index(&WorkspaceIndexInputs {
files,
workspace_files: &workspace_files,
cfg_test_files,
transparent_wrappers,
});
let mut graph = CallGraph::new();
for (path, ast) in files {
let Some(file) = workspace_files.get(*path) else {
continue;
};
let mut collector = FileFnCollector {
file,
workspace_files: &workspace_files,
type_index: &type_index,
impl_type_stack: Vec::new(),
mod_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> {
file: &'a FileScope<'a>,
workspace_files: &'a HashMap<String, FileScope<'a>>,
type_index: &'a WorkspaceTypeIndex,
impl_type_stack: Vec<Option<Vec<String>>>,
mod_stack: 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.file.path,
self_type.as_deref(),
&self.mod_stack,
fn_name,
);
let ctx = FnContext {
file: self.file,
mod_stack: &self.mod_stack,
body,
signature_params: extract_signature_params(sig),
self_type,
workspace_index: Some(self.type_index),
workspace_files: Some(self.workspace_files),
};
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) || has_test_attr(&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,
&CanonScope {
file: self.file,
mod_stack: &self.mod_stack,
},
);
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) || has_test_attr(&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;
}
self.mod_stack.push(node.ident.to_string());
syn::visit::visit_item_mod(self, node);
self.mod_stack.pop();
}
}