use syn::visit::Visit;
use crate::adapters::shared::cfg_test::has_test_attr;
pub(crate) struct TestFnReferences {
pub name: String,
pub line: usize,
pub call_targets: Vec<String>,
pub type_qualified_calls: Vec<String>,
}
pub(crate) fn collect_test_references(file: &syn::File) -> Vec<TestFnReferences> {
let mut collector = TestReferenceCollector::default();
collector.visit_file(file);
collector.test_fns
}
#[derive(Default)]
struct TestReferenceCollector {
test_fns: Vec<TestFnReferences>,
in_test_fn: bool,
current_calls: Vec<String>,
current_type_calls: Vec<String>,
}
impl<'ast> Visit<'ast> for TestReferenceCollector {
fn visit_macro(&mut self, node: &'ast syn::Macro) {
if self.in_test_fn {
crate::adapters::shared::macro_tokens::recover_exprs(&node.tokens)
.iter()
.for_each(|expr| syn::visit::visit_expr(self, expr));
self.current_calls.extend(
crate::adapters::shared::macro_tokens::idents_in_call_position(&node.tokens),
);
}
syn::visit::visit_macro(self, node);
}
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
if has_test_attr(&node.attrs) {
self.in_test_fn = true;
self.current_calls.clear();
self.current_type_calls.clear();
syn::visit::visit_item_fn(self, node);
self.in_test_fn = false;
let line = node.sig.ident.span().start().line;
self.test_fns.push(TestFnReferences {
name: node.sig.ident.to_string(),
line,
call_targets: std::mem::take(&mut self.current_calls),
type_qualified_calls: std::mem::take(&mut self.current_type_calls),
});
} else {
syn::visit::visit_item_fn(self, node);
}
}
fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
if self.in_test_fn {
if let syn::Expr::Path(ref p) = *node.func {
let name = path_to_name(&p.path);
self.current_calls.push(name);
if let Some(type_name) = path_type_prefix(&p.path) {
self.current_type_calls.push(type_name);
}
}
}
syn::visit::visit_expr_call(self, node);
}
fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
if self.in_test_fn {
self.current_calls.push(node.method.to_string());
}
syn::visit::visit_expr_method_call(self, node);
}
fn visit_expr_struct(&mut self, node: &'ast syn::ExprStruct) {
if self.in_test_fn {
if let Some(last) = node.path.segments.last() {
self.current_type_calls.push(last.ident.to_string());
}
}
syn::visit::visit_expr_struct(self, node);
}
fn visit_expr_path(&mut self, node: &'ast syn::ExprPath) {
if self.in_test_fn && node.path.segments.len() >= 2 {
let type_seg = &node.path.segments[node.path.segments.len() - 2];
self.current_type_calls.push(type_seg.ident.to_string());
}
syn::visit::visit_expr_path(self, node);
}
}
fn path_to_name(path: &syn::Path) -> String {
path.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default()
}
fn path_type_prefix(path: &syn::Path) -> Option<String> {
let len = path.segments.len();
if len >= 2 {
path.segments
.iter()
.nth(len - 2)
.map(|s| s.ident.to_string())
} else {
None
}
}