use std::collections::HashMap;
use syn::visit::Visit;
const DRIVE_FORWARDERS: &[(&str, usize)] = &[("visit_all_files", 1)];
pub(crate) fn visitor_dispatch_edges(
parsed: &[(String, String, syn::File)],
) -> Vec<(String, Vec<String>)> {
let helpers = collect_visitor_helpers(parsed);
let mut drives = DriveCollector {
helpers: &helpers,
edges: Vec::new(),
current_fn: None,
bindings: HashMap::new(),
};
for (_, _, file) in parsed {
drives.visit_file(file);
}
drives.edges
}
fn last_segment(path: &syn::Path) -> Option<String> {
path.segments.last().map(|s| s.ident.to_string())
}
fn self_type_name(ty: &syn::Type) -> Option<String> {
match ty {
syn::Type::Path(tp) => last_segment(&tp.path),
_ => None,
}
}
fn constructed_type(expr: &syn::Expr) -> Option<String> {
match expr {
syn::Expr::Struct(s) => last_segment(&s.path),
syn::Expr::Call(c) => match c.func.as_ref() {
syn::Expr::Path(p) if p.path.segments.len() >= 2 => {
let segs = &p.path.segments;
let last = segs.last()?.ident.to_string();
let is_ctor =
matches!(last.as_str(), "new" | "default" | "with_capacity" | "build");
is_ctor.then(|| segs[segs.len() - 2].ident.to_string())
}
_ => None,
},
_ => None,
}
}
#[derive(Default)]
struct HelperCollector {
map: HashMap<String, Vec<String>>,
type_stack: Vec<Option<String>>,
collecting: Option<String>,
}
impl HelperCollector {
fn record(&mut self, name: &str) {
if let Some(t) = self.collecting.clone() {
if !name.starts_with("visit_") {
self.map.entry(t).or_default().push(name.to_string());
}
}
}
}
impl<'ast> Visit<'ast> for HelperCollector {
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
let is_visit = node
.trait_
.as_ref()
.is_some_and(|(_, path, _)| last_segment(path).is_some_and(|s| s.starts_with("Visit")));
let ty = is_visit.then(|| self_type_name(&node.self_ty)).flatten();
self.type_stack.push(ty);
syn::visit::visit_item_impl(self, node);
self.type_stack.pop();
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
let enclosing = self.type_stack.last().cloned().flatten();
let is_visit_method = node.sig.ident.to_string().starts_with("visit_");
let prev = self.collecting.take();
if let Some(t) = enclosing {
if is_visit_method {
self.collecting = Some(t);
}
}
syn::visit::visit_impl_item_fn(self, node);
self.collecting = prev;
}
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
self.record(&node.method.to_string());
syn::visit::visit_expr_method_call(self, node);
}
fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
if let syn::Expr::Path(p) = node.func.as_ref() {
if let Some(last) = last_segment(&p.path) {
self.record(&last);
}
}
syn::visit::visit_expr_call(self, node);
}
}
fn collect_visitor_helpers(parsed: &[(String, String, syn::File)]) -> HashMap<String, Vec<String>> {
let mut collector = HelperCollector::default();
for (_, _, file) in parsed {
collector.visit_file(file);
}
collector.map
}
struct DriveCollector<'h> {
helpers: &'h HashMap<String, Vec<String>>,
edges: Vec<(String, Vec<String>)>,
current_fn: Option<String>,
bindings: HashMap<String, String>,
}
impl DriveCollector<'_> {
fn resolve_driven(&self, expr: &syn::Expr) -> Option<String> {
let inner = match expr {
syn::Expr::Reference(r) => r.expr.as_ref(),
other => other,
};
if let Some(t) = constructed_type(inner) {
return Some(t);
}
if let syn::Expr::Path(p) = inner {
if let Some(id) = p.path.get_ident() {
return self.bindings.get(&id.to_string()).cloned();
}
}
None
}
fn emit_drive(&mut self, driven_type: Option<String>) {
let (Some(t), Some(caller)) = (driven_type, self.current_fn.clone()) else {
return;
};
if let Some(hs) = self.helpers.get(&t) {
self.edges.push((caller, hs.clone()));
}
}
fn record_binding(&mut self, local: &syn::Local) {
let syn::Pat::Ident(pi) = &local.pat else {
return;
};
if let Some(init) = &local.init {
if let Some(t) = constructed_type(&init.expr) {
self.bindings.insert(pi.ident.to_string(), t);
}
}
}
fn handle_call_drive(&mut self, node: &syn::ExprCall) {
let syn::Expr::Path(p) = node.func.as_ref() else {
return;
};
let Some(name) = last_segment(&p.path) else {
return;
};
let is_syn_visit =
name.starts_with("visit_") && p.path.segments.iter().any(|s| s.ident == "visit");
if is_syn_visit {
let driven = node.args.first().and_then(|a| self.resolve_driven(a));
self.emit_drive(driven);
return;
}
if let Some((_, arg_idx)) = DRIVE_FORWARDERS.iter().find(|(n, _)| *n == name) {
let driven = node
.args
.iter()
.nth(*arg_idx)
.and_then(|a| self.resolve_driven(a));
self.emit_drive(driven);
}
}
fn enter_fn(&mut self, name: String, walk: impl FnOnce(&mut Self)) {
let prev_fn = self.current_fn.take();
let prev_bindings = std::mem::take(&mut self.bindings);
self.current_fn = Some(name);
walk(self);
self.current_fn = prev_fn;
self.bindings = prev_bindings;
}
}
impl<'ast> Visit<'ast> for DriveCollector<'_> {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
let name = node.sig.ident.to_string();
self.enter_fn(name, |s| syn::visit::visit_item_fn(s, node));
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
let name = node.sig.ident.to_string();
self.enter_fn(name, |s| syn::visit::visit_impl_item_fn(s, node));
}
fn visit_local(&mut self, node: &'ast syn::Local) {
self.record_binding(node);
syn::visit::visit_local(self, node);
}
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
if node.method.to_string().starts_with("visit_") {
let driven = self.resolve_driven(&node.receiver);
self.emit_drive(driven);
}
syn::visit::visit_expr_method_call(self, node);
}
fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
self.handle_call_drive(node);
syn::visit::visit_expr_call(self, node);
}
}