use super::bindings::{
canonical_from_type, extract_let_binding, normalize_alias_expansion, CanonScope,
};
use super::local_symbols::{scope_for_local, FileScope};
use super::type_infer::resolve::{resolve_type, ResolveContext};
use super::type_infer::self_subst::substitute_bare_self;
use super::type_infer::{
extract_bindings, extract_for_bindings, infer_type, BindingLookup, CanonicalType, InferContext,
WorkspaceTypeIndex,
};
use crate::adapters::analyzers::architecture::forbidden_rule::{
file_to_module_segments, resolve_to_crate_absolute_in,
};
use crate::adapters::shared::use_tree::AliasTarget;
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 file: &'a FileScope<'a>,
pub mod_stack: &'a [String],
pub body: &'a syn::Block,
pub signature_params: Vec<(String, &'a syn::Type)>,
pub generic_params: HashMap<String, super::signature_params::ParamInfo>,
pub self_type: Option<Vec<String>>,
pub workspace_index: Option<&'a WorkspaceTypeIndex>,
pub workspace_files: Option<&'a HashMap<String, FileScope<'a>>>,
}
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> {
file: &'a FileScope<'a>,
mod_stack: &'a [String],
self_type_canonical: Option<Vec<String>>,
signature_params: Vec<(String, &'a syn::Type)>,
generic_params: HashMap<String, super::signature_params::ParamInfo>,
bindings: Vec<HashMap<String, Vec<String>>>,
non_path_bindings: Vec<HashMap<String, CanonicalType>>,
calls: HashSet<String>,
workspace_index: Option<&'a WorkspaceTypeIndex>,
workspace_files: Option<&'a HashMap<String, FileScope<'a>>>,
}
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.file.path));
full.extend(ctx.mod_stack.iter().cloned());
full.extend_from_slice(segs);
full
});
Self {
file: ctx.file,
mod_stack: ctx.mod_stack,
self_type_canonical,
signature_params: ctx.signature_params.clone(),
generic_params: ctx.generic_params.clone(),
bindings: vec![HashMap::new()],
non_path_bindings: vec![HashMap::new()],
calls: HashSet::new(),
workspace_index: ctx.workspace_index,
workspace_files: ctx.workspace_files,
}
}
fn seed_signature_bindings(&mut self) {
if let Some(self_canonical) = self.self_type_canonical.clone() {
self.bindings[0].insert("self".to_string(), self_canonical);
}
let params = self.signature_params.clone();
for (name, ty) in ¶ms {
if self.workspace_index.is_some() {
self.seed_param_via_resolver(name, ty);
continue;
}
if let Some(canonical) = canonical_from_type(
ty,
self.file.alias_map,
self.file.local_symbols,
self.file.crate_root_modules,
self.file.path,
) {
self.bindings[0].insert(name.clone(), canonical);
}
}
}
fn seed_param_via_resolver(&mut self, name: &str, ty: &syn::Type) {
match self.resolve_param_type(ty) {
CanonicalType::Path(segs) => {
self.bindings[0].insert(name.to_string(), segs);
}
CanonicalType::Opaque => {}
other => {
self.non_path_bindings[0].insert(name.to_string(), other);
}
}
}
fn resolve_param_type(&self, ty: &syn::Type) -> CanonicalType {
let rctx = ResolveContext {
file: self.file,
mod_stack: self.mod_stack,
type_aliases: self.workspace_index.map(|w| &w.type_aliases),
transparent_wrappers: self.workspace_index.map(|w| &w.transparent_wrappers),
workspace_files: self.workspace_files,
alias_param_subs: None,
generic_params: Some(&self.generic_params),
};
match self.self_type_canonical.as_deref() {
Some(impl_segs) => resolve_type(&substitute_bare_self(ty, impl_segs), &rctx),
None => resolve_type(ty, &rctx),
}
}
fn enter_scope(&mut self) {
self.bindings.push(HashMap::new());
self.non_path_bindings.push(HashMap::new());
}
fn exit_scope(&mut self) {
self.bindings.pop();
self.non_path_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 current_non_path_scope_mut(&mut self) -> &mut HashMap<String, CanonicalType> {
if self.non_path_bindings.is_empty() {
self.non_path_bindings.push(HashMap::new());
}
let last = self.non_path_bindings.len() - 1;
&mut self.non_path_bindings[last]
}
fn install_path_binding(&mut self, name: String, segs: Vec<String>) {
self.current_non_path_scope_mut().remove(&name);
self.current_scope_mut().insert(name, segs);
}
fn install_non_path_binding(&mut self, name: String, ty: CanonicalType) {
self.current_scope_mut().remove(&name);
self.current_non_path_scope_mut().insert(name, ty);
}
fn install_closure_param(&mut self, pat: &syn::Pat) {
if let syn::Pat::Type(pt) = pat {
if let Some(name) = extract_pat_ident_name(pt.pat.as_ref()) {
match self.resolve_param_type(&pt.ty) {
CanonicalType::Path(segs) => self.install_path_binding(name, segs),
other => self.install_non_path_binding(name, other),
}
return;
}
}
let mut idents = Vec::new();
collect_pattern_idents(pat, &mut idents);
for name in idents {
self.install_non_path_binding(name, CanonicalType::Opaque);
}
}
fn canonicalise_generic_param_path(
&self,
segments: &[String],
leading_colon_set: bool,
) -> Option<Vec<String>> {
if segments.len() < 2 {
return None;
}
let info = super::signature_params::matched_generic_param(
segments,
leading_colon_set,
&self.generic_params,
)?;
let method_tail = &segments[1..];
let canonicals: Vec<String> = info
.bounds
.iter()
.map(|bound| {
let mut full = bound.clone();
full.extend_from_slice(method_tail);
full.join("::")
})
.collect();
Some(canonicals)
}
fn canonicalise_path(&self, segments: &[String], leading_colon_set: bool) -> String {
if segments.is_empty() {
return String::new();
}
if leading_colon_set {
return bare(&segments.join("::"));
}
if segments[0] == "Self" {
return self.canonicalise_self_path(segments);
}
if matches!(segments[0].as_str(), "crate" | "self" | "super") {
return self.canonicalise_keyword_path(segments);
}
if let Some(canonical) = self.canonicalise_alias_path(segments) {
return canonical;
}
if let Some(canonical) = self.canonicalise_local_symbol_path(segments) {
return canonical;
}
if self.file.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 canonicalise_self_path(&self, segments: &[String]) -> String {
if let Some(self_canonical) = &self.self_type_canonical {
let mut full = self_canonical.clone();
full.extend_from_slice(&segments[1..]);
return full.join("::");
}
bare(&segments.join("::"))
}
fn canonicalise_keyword_path(&self, segments: &[String]) -> String {
if let Some(resolved) =
resolve_to_crate_absolute_in(self.file.path, self.mod_stack, segments)
{
let mut full = vec!["crate".to_string()];
full.extend(resolved);
return full.join("::");
}
bare(&segments.join("::"))
}
fn canonicalise_alias_path(&self, segments: &[String]) -> Option<String> {
let alias = self.lookup_alias_at_scope(&segments[0])?;
let mut full = alias.segments.to_vec();
full.extend_from_slice(&segments[1..]);
let scope = CanonScope {
file: self.file,
mod_stack: self.mod_stack,
};
let normalized = normalize_alias_expansion(full, alias.absolute_root, &scope)?;
Some(normalized.join("::"))
}
fn lookup_alias_at_scope(&self, name: &str) -> Option<&AliasTarget> {
if let Some(map) = self.file.aliases_per_scope.get(self.mod_stack) {
return map.get(name);
}
self.file.alias_map.get(name)
}
fn canonicalise_local_symbol_path(&self, segments: &[String]) -> Option<String> {
if !self.file.local_symbols.contains(&segments[0]) {
return None;
}
let mod_path = scope_for_local(self.file.local_decl_scopes, &segments[0], self.mod_stack)?;
let mut full = vec!["crate".to_string()];
full.extend(file_to_module_segments(self.file.path));
full.extend(mod_path.iter().cloned());
full.extend_from_slice(segments);
Some(full.join("::"))
}
fn record_call(&mut self, target: String) {
self.calls.insert(target);
}
fn resolve_method_targets(&self, receiver: &syn::Expr, method_name: &str) -> Vec<String> {
if let Some(c) = self.try_fast_path_receiver(receiver, method_name) {
return vec![c];
}
self.try_inferred_targets(receiver, method_name)
}
fn try_fast_path_receiver(&self, receiver: &syn::Expr, method_name: &str) -> Option<String> {
let syn::Expr::Path(p) = receiver else {
return None;
};
if p.path.segments.len() != 1 {
return None;
}
let ident = p.path.segments[0].ident.to_string();
for (path_scope, non_path_scope) in self
.bindings
.iter()
.rev()
.zip(self.non_path_bindings.iter().rev())
{
if non_path_scope.contains_key(&ident) {
return None;
}
if let Some(binding) = path_scope.get(&ident) {
let mut full = binding.clone();
full.push(method_name.to_string());
return Some(full.join("::"));
}
}
None
}
fn try_inferred_targets(&self, receiver: &syn::Expr, method_name: &str) -> Vec<String> {
let Some(workspace) = self.workspace_index else {
return Vec::new();
};
let Some(inferred) = self.infer_receiver_type(receiver) else {
return Vec::new();
};
canonical_edges_for_method(&inferred, method_name, workspace)
}
fn infer_receiver_type(&self, expr: &syn::Expr) -> Option<CanonicalType> {
let adapter = CollectorBindings {
scope: &self.bindings,
non_path_scope: &self.non_path_bindings,
};
let ctx = InferContext {
file: self.file,
mod_stack: self.mod_stack,
workspace: self.workspace_index?,
bindings: &adapter,
self_type: self.self_type_canonical.clone(),
workspace_files: self.workspace_files,
generic_params: Some(&self.generic_params),
};
infer_type(expr, &ctx)
}
fn try_install_annotated_binding(&mut self, local: &syn::Local) -> bool {
let Some(wi) = self.workspace_index else {
return false;
};
let syn::Pat::Type(pt) = &local.pat else {
return false;
};
let syn::Pat::Ident(pi) = pt.pat.as_ref() else {
return false;
};
if matches!(pt.ty.as_ref(), syn::Type::Infer(_)) {
return false;
}
let rctx = ResolveContext {
file: self.file,
mod_stack: self.mod_stack,
type_aliases: Some(&wi.type_aliases),
transparent_wrappers: Some(&wi.transparent_wrappers),
workspace_files: self.workspace_files,
alias_param_subs: None,
generic_params: Some(&self.generic_params),
};
let name = pi.ident.to_string();
let resolved = match self.self_type_canonical.as_deref() {
Some(impl_segs) => {
resolve_type(&substitute_bare_self(pt.ty.as_ref(), impl_segs), &rctx)
}
None => resolve_type(pt.ty.as_ref(), &rctx),
};
match resolved {
CanonicalType::Path(segs) => self.install_path_binding(name, segs),
other => self.install_non_path_binding(name, other),
}
true
}
fn install_inferred_let_binding(&mut self, local: &syn::Local) {
let Some(name) = extract_pat_ident_name(&local.pat) else {
return;
};
let inferred = local
.init
.as_ref()
.and_then(|init| self.infer_receiver_type(&init.expr))
.unwrap_or(CanonicalType::Opaque);
match inferred {
CanonicalType::Path(segs) => self.install_path_binding(name, segs),
other => self.install_non_path_binding(name, other),
}
}
fn collect_macro_body(&mut self, mac: &syn::Macro) {
for expr in parse_macro_tokens(mac.tokens.clone()) {
self.visit_expr(&expr);
}
}
fn install_destructure_bindings(&mut self, pat: &syn::Pat, matched_expr: &syn::Expr) {
let matched = self
.infer_receiver_type(matched_expr)
.unwrap_or(CanonicalType::Opaque);
let pairs = self.extract_pattern_pairs(pat, &matched, PatKind::Value);
self.install_binding_pairs_with_tombstones(pat, pairs);
}
fn install_for_bindings(&mut self, pat: &syn::Pat, iter_expr: &syn::Expr) {
let iter_type = self
.infer_receiver_type(iter_expr)
.unwrap_or(CanonicalType::Opaque);
let pairs = self.extract_pattern_pairs(pat, &iter_type, PatKind::Iterator);
self.install_binding_pairs_with_tombstones(pat, pairs);
}
fn extract_pattern_pairs(
&self,
pat: &syn::Pat,
matched: &CanonicalType,
kind: PatKind,
) -> Vec<(String, CanonicalType)> {
let Some(workspace) = self.workspace_index else {
return Vec::new();
};
let adapter = CollectorBindings {
scope: &self.bindings,
non_path_scope: &self.non_path_bindings,
};
let ictx = InferContext {
file: self.file,
mod_stack: self.mod_stack,
workspace,
bindings: &adapter,
self_type: self.self_type_canonical.clone(),
workspace_files: self.workspace_files,
generic_params: Some(&self.generic_params),
};
match kind {
PatKind::Value => extract_bindings(pat, matched, &ictx),
PatKind::Iterator => extract_for_bindings(pat, matched, &ictx),
}
}
fn install_binding_pairs_with_tombstones(
&mut self,
pat: &syn::Pat,
pairs: Vec<(String, CanonicalType)>,
) {
let mut resolved: HashSet<String> = HashSet::new();
for (name, ty) in pairs {
resolved.insert(name.clone());
match ty {
CanonicalType::Path(segs) => self.install_path_binding(name, segs),
other => self.install_non_path_binding(name, other),
}
}
let mut idents = Vec::new();
collect_pattern_idents(pat, &mut idents);
for name in idents {
if !resolved.contains(&name) {
self.install_non_path_binding(name, CanonicalType::Opaque);
}
}
}
}
enum PatKind {
Value,
Iterator,
}
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 canonical_edges_for_method(
ty: &CanonicalType,
method: &str,
workspace: &WorkspaceTypeIndex,
) -> Vec<String> {
if let Some(bounds) = ty.as_trait_bounds() {
return bounds
.iter()
.flat_map(|trait_segs| trait_dispatch_edges(trait_segs, method, workspace))
.collect();
}
match ty {
CanonicalType::Path(segs) => {
let mut full = segs.clone();
full.push(method.to_string());
vec![full.join("::")]
}
_ => Vec::new(),
}
}
fn trait_dispatch_edges(
trait_segs: &[String],
method: &str,
workspace: &WorkspaceTypeIndex,
) -> Vec<String> {
let trait_canonical = trait_segs.join("::");
if !workspace.trait_has_method(&trait_canonical, method) {
return Vec::new();
}
vec![format!("{trait_canonical}::{method}")]
}
struct CollectorBindings<'a> {
scope: &'a [HashMap<String, Vec<String>>],
non_path_scope: &'a [HashMap<String, CanonicalType>],
}
impl BindingLookup for CollectorBindings<'_> {
fn lookup(&self, ident: &str) -> Option<CanonicalType> {
for (path_frame, non_path_frame) in self
.scope
.iter()
.rev()
.zip(self.non_path_scope.iter().rev())
{
if let Some(ty) = non_path_frame.get(ident) {
return Some(ty.clone());
}
if let Some(segs) = path_frame.get(ident) {
return Some(CanonicalType::Path(segs.clone()));
}
}
None
}
}
fn extract_pat_ident_name(pat: &syn::Pat) -> Option<String> {
match pat {
syn::Pat::Ident(pi) => Some(pi.ident.to_string()),
syn::Pat::Type(pt) => extract_pat_ident_name(&pt.pat),
_ => None,
}
}
fn collect_pattern_idents(pat: &syn::Pat, out: &mut Vec<String>) {
match pat {
syn::Pat::Ident(pi) => push_pat_ident(pi, out),
syn::Pat::Type(pt) => collect_pattern_idents(&pt.pat, out),
syn::Pat::Reference(r) => collect_pattern_idents(&r.pat, out),
syn::Pat::Paren(p) => collect_pattern_idents(&p.pat, out),
syn::Pat::Tuple(t) => walk_each(t.elems.iter(), out),
syn::Pat::TupleStruct(ts) => walk_each(ts.elems.iter(), out),
syn::Pat::Struct(s) => walk_each(s.fields.iter().map(|f| f.pat.as_ref()), out),
syn::Pat::Slice(s) => walk_each(s.elems.iter(), out),
syn::Pat::Or(o) => walk_each(o.cases.iter().take(1), out),
_ => {}
}
}
fn walk_each<'p, I: Iterator<Item = &'p syn::Pat>>(iter: I, out: &mut Vec<String>) {
for p in iter {
collect_pattern_idents(p, out);
}
}
fn push_pat_ident(pi: &syn::PatIdent, out: &mut Vec<String>) {
out.push(pi.ident.to_string());
if let Some((_, sub)) = &pi.subpat {
collect_pattern_idents(sub, out);
}
}
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 self.try_install_annotated_binding(local) {
return;
}
if self.workspace_index.is_none() {
if let Some((name, ty_canonical)) = extract_let_binding(
local,
self.file.alias_map,
self.file.local_symbols,
self.file.crate_root_modules,
self.file.path,
) {
self.install_path_binding(name, ty_canonical);
return;
}
}
if extract_pat_ident_name(&local.pat).is_some() {
self.install_inferred_let_binding(local);
return;
}
if let Some(init) = local.init.as_ref() {
self.install_destructure_bindings(&local.pat, &init.expr);
}
}
fn visit_expr_if(&mut self, expr_if: &'ast syn::ExprIf) {
self.enter_scope();
if let syn::Expr::Let(let_expr) = expr_if.cond.as_ref() {
self.visit_expr(&let_expr.expr);
self.install_destructure_bindings(&let_expr.pat, &let_expr.expr);
} else {
self.visit_expr(&expr_if.cond);
}
self.visit_block(&expr_if.then_branch);
self.exit_scope();
if let Some((_, else_branch)) = &expr_if.else_branch {
self.visit_expr(else_branch);
}
}
fn visit_expr_while(&mut self, expr_while: &'ast syn::ExprWhile) {
self.enter_scope();
if let syn::Expr::Let(let_expr) = expr_while.cond.as_ref() {
self.visit_expr(&let_expr.expr);
self.install_destructure_bindings(&let_expr.pat, &let_expr.expr);
} else {
self.visit_expr(&expr_while.cond);
}
self.visit_block(&expr_while.body);
self.exit_scope();
}
fn visit_expr_match(&mut self, expr_match: &'ast syn::ExprMatch) {
self.visit_expr(&expr_match.expr);
for arm in &expr_match.arms {
self.enter_scope();
self.install_destructure_bindings(&arm.pat, &expr_match.expr);
if let Some((_, guard)) = &arm.guard {
self.visit_expr(guard);
}
self.visit_expr(&arm.body);
self.exit_scope();
}
}
fn visit_expr_for_loop(&mut self, for_loop: &'ast syn::ExprForLoop) {
self.visit_expr(&for_loop.expr);
self.enter_scope();
self.install_for_bindings(&for_loop.pat, &for_loop.expr);
self.visit_block(&for_loop.body);
self.exit_scope();
}
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 leading_colon = p.path.leading_colon.is_some();
if let Some(targets) = self.canonicalise_generic_param_path(&segments, leading_colon) {
for t in targets {
self.record_call(t);
}
} else {
let canonical = self.canonicalise_path(&segments, leading_colon);
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 targets = self.resolve_method_targets(&call.receiver, &method_name);
if targets.is_empty() {
self.record_call(method_unknown(&method_name));
} else {
for t in targets {
self.record_call(t);
}
}
}
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 {
self.install_closure_param(input);
}
self.visit_expr(&c.body);
self.exit_scope();
}
}