use super::bindings::{canonical_from_type, extract_let_binding, normalize_alias_expansion};
use crate::adapters::analyzers::architecture::forbidden_rule::{
file_to_module_segments, resolve_to_crate_absolute,
};
use std::collections::{HashMap, HashSet};
use syn::visit::Visit;
const METHOD_UNKNOWN_PREFIX: &str = "<method>:";
const BARE_UNKNOWN_PREFIX: &str = "<bare>:";
pub struct FnContext<'a> {
pub body: &'a syn::Block,
pub signature_params: Vec<(String, &'a syn::Type)>,
pub self_type: Option<Vec<String>>,
pub alias_map: &'a HashMap<String, Vec<String>>,
pub local_symbols: &'a HashSet<String>,
pub crate_root_modules: &'a HashSet<String>,
pub importing_file: &'a str,
}
pub fn collect_canonical_calls(ctx: &FnContext<'_>) -> HashSet<String> {
let mut collector = CanonicalCallCollector::new(ctx);
collector.seed_signature_bindings();
collector.visit_block(ctx.body);
collector.calls
}
struct CanonicalCallCollector<'a> {
alias_map: &'a HashMap<String, Vec<String>>,
local_symbols: &'a HashSet<String>,
crate_root_modules: &'a HashSet<String>,
importing_file: &'a str,
self_type_canonical: Option<Vec<String>>,
signature_params: Vec<(String, &'a syn::Type)>,
bindings: Vec<HashMap<String, Vec<String>>>,
calls: HashSet<String>,
}
impl<'a> CanonicalCallCollector<'a> {
fn new(ctx: &'a FnContext<'a>) -> Self {
let self_type_canonical = ctx.self_type.as_ref().map(|segs| {
if segs.first().map(|s| s.as_str()) == Some("crate") {
return segs.clone();
}
let mut full = vec!["crate".to_string()];
full.extend(file_to_module_segments(ctx.importing_file));
full.extend_from_slice(segs);
full
});
Self {
alias_map: ctx.alias_map,
local_symbols: ctx.local_symbols,
crate_root_modules: ctx.crate_root_modules,
importing_file: ctx.importing_file,
self_type_canonical,
signature_params: ctx.signature_params.clone(),
bindings: vec![HashMap::new()],
calls: HashSet::new(),
}
}
fn seed_signature_bindings(&mut self) {
let params = self.signature_params.clone();
for (name, ty) in ¶ms {
if let Some(canonical) = canonical_from_type(
ty,
self.alias_map,
self.local_symbols,
self.crate_root_modules,
self.importing_file,
) {
self.bindings[0].insert(name.clone(), canonical);
}
}
}
fn enter_scope(&mut self) {
self.bindings.push(HashMap::new());
}
fn exit_scope(&mut self) {
self.bindings.pop();
}
fn current_scope_mut(&mut self) -> &mut HashMap<String, Vec<String>> {
if self.bindings.is_empty() {
self.bindings.push(HashMap::new());
}
let last = self.bindings.len() - 1;
&mut self.bindings[last]
}
fn resolve_binding(&self, ident: &str) -> Option<&Vec<String>> {
for scope in self.bindings.iter().rev() {
if let Some(v) = scope.get(ident) {
return Some(v);
}
}
None
}
fn canonicalise_path(&self, segments: &[String]) -> String {
if segments.is_empty() {
return String::new();
}
if segments[0] == "Self" {
if let Some(self_canonical) = &self.self_type_canonical {
let mut full = self_canonical.clone();
full.extend_from_slice(&segments[1..]);
return full.join("::");
}
return bare(&segments.join("::"));
}
if matches!(segments[0].as_str(), "crate" | "self" | "super") {
if let Some(resolved) = resolve_to_crate_absolute(self.importing_file, segments) {
let mut full = vec!["crate".to_string()];
full.extend(resolved);
return full.join("::");
}
return bare(&segments.join("::"));
}
if let Some(alias) = self.alias_map.get(&segments[0]) {
let mut full = alias.clone();
full.extend_from_slice(&segments[1..]);
if let Some(normalized) =
normalize_alias_expansion(full, self.importing_file, self.crate_root_modules)
{
return normalized.join("::");
}
}
if self.local_symbols.contains(&segments[0]) {
let mut full = vec!["crate".to_string()];
full.extend(file_to_module_segments(self.importing_file));
full.extend_from_slice(segments);
return full.join("::");
}
if self.crate_root_modules.contains(&segments[0]) {
let mut full = vec!["crate".to_string()];
full.extend_from_slice(segments);
return full.join("::");
}
bare(&segments.join("::"))
}
fn record_call(&mut self, target: String) {
self.calls.insert(target);
}
fn collect_macro_body(&mut self, mac: &syn::Macro) {
for expr in parse_macro_tokens(mac.tokens.clone()) {
self.visit_expr(&expr);
}
}
}
fn parse_macro_tokens(tokens: proc_macro2::TokenStream) -> Vec<syn::Expr> {
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::Token;
let parser = Punctuated::<syn::Expr, Token![,]>::parse_terminated;
if let Ok(exprs) = parser.parse2(tokens.clone()) {
return exprs.into_iter().collect();
}
let braced = quote::quote! { { #tokens } };
if let Ok(block) = syn::parse2::<syn::Block>(braced) {
return block
.stmts
.into_iter()
.filter_map(|stmt| match stmt {
syn::Stmt::Expr(e, _) => Some(e),
syn::Stmt::Local(l) => l.init.map(|init| *init.expr),
_ => None,
})
.collect();
}
if let Ok(expr) = syn::parse2::<syn::Expr>(tokens) {
return vec![expr];
}
Vec::new()
}
fn bare(path: &str) -> String {
format!("{BARE_UNKNOWN_PREFIX}{path}")
}
fn method_unknown(method: &str) -> String {
format!("{METHOD_UNKNOWN_PREFIX}{method}")
}
impl<'a, 'ast> Visit<'ast> for CanonicalCallCollector<'a> {
fn visit_block(&mut self, block: &'ast syn::Block) {
self.enter_scope();
syn::visit::visit_block(self, block);
self.exit_scope();
}
fn visit_local(&mut self, local: &'ast syn::Local) {
if let Some(init) = &local.init {
self.visit_expr(&init.expr);
if let Some((_, else_expr)) = &init.diverge {
self.visit_expr(else_expr);
}
}
if let Some((name, ty_canonical)) = extract_let_binding(
local,
self.alias_map,
self.local_symbols,
self.crate_root_modules,
self.importing_file,
) {
self.current_scope_mut().insert(name, ty_canonical);
}
}
fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) {
self.visit_expr(&call.func);
for arg in &call.args {
self.visit_expr(arg);
}
if let syn::Expr::Path(p) = call.func.as_ref() {
let segments: Vec<String> = p
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
let canonical = self.canonicalise_path(&segments);
self.record_call(canonical);
}
}
fn visit_expr_method_call(&mut self, call: &'ast syn::ExprMethodCall) {
self.visit_expr(&call.receiver);
for arg in &call.args {
self.visit_expr(arg);
}
let method_name = call.method.to_string();
let canonical = match call.receiver.as_ref() {
syn::Expr::Path(p) if p.path.segments.len() == 1 => {
let ident = p.path.segments[0].ident.to_string();
match self.resolve_binding(&ident) {
Some(binding) => {
let mut full = binding.clone();
full.push(method_name.clone());
full.join("::")
}
None => method_unknown(&method_name),
}
}
_ => method_unknown(&method_name),
};
self.record_call(canonical);
}
fn visit_macro(&mut self, mac: &'ast syn::Macro) {
self.collect_macro_body(mac);
}
fn visit_expr_closure(&mut self, c: &'ast syn::ExprClosure) {
self.enter_scope();
for input in &c.inputs {
if let syn::Pat::Type(pt) = input {
if let syn::Pat::Ident(pi) = pt.pat.as_ref() {
let name = pi.ident.to_string();
if let Some(canonical) = canonical_from_type(
&pt.ty,
self.alias_map,
self.local_symbols,
self.crate_root_modules,
self.importing_file,
) {
self.current_scope_mut().insert(name, canonical);
}
}
}
}
self.visit_expr(&c.body);
self.exit_scope();
}
}