use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use rayon::prelude::*;
use smallvec::SmallVec;
use tree_sitter::Node;
use crate::ast::types::ImportInfo;
use crate::callgraph::indexer::FunctionIndex;
use crate::callgraph::types::{CallEdge, CallGraph, FunctionRef};
use crate::error::{Result, BrrrError};
use crate::lang::LanguageRegistry;
type AttributeChain = SmallVec<[String; 4]>;
type MethodChain = SmallVec<[String; 8]>;
const MIN_FILES_FOR_PARALLEL: usize = 10;
const MAX_TREE_DEPTH: usize = 500;
fn resolve_relative_import(
import: &ImportInfo,
current_file: &Path,
project_root: &Path,
) -> Option<String> {
if import.level == 0 {
return Some(import.module.clone());
}
let rel_path = current_file.strip_prefix(project_root).ok()?;
let mut components: Vec<_> = rel_path
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
components.pop();
for _ in 0..(import.level - 1) {
if components.is_empty() {
return None;
}
components.pop();
}
if !import.module.is_empty() {
components.extend(import.module.split('.'));
}
if components.is_empty() {
None
} else {
Some(components.join("."))
}
}
#[derive(Debug, Clone)]
struct CallSite {
caller_name: Option<String>,
target: CallTarget,
line: usize,
}
#[derive(Debug, Clone)]
enum CallTarget {
Direct(String),
Method(String, String),
Qualified(AttributeChain),
Constructor(String),
ChainedCall(MethodChain),
}
#[derive(Debug, Default)]
struct FileContext {
file_path: String,
language: String,
defined_functions: HashSet<String>,
defined_classes: HashSet<String>,
module_imports: HashMap<String, String>,
from_imports: HashMap<String, (String, String)>,
star_imports: Vec<String>,
}
#[derive(Debug)]
struct FileCallInfo {
file_path: String,
call_sites: Vec<CallSite>,
context: FileContext,
}
fn format_qualified_name(parts: &[String], language: &str) -> String {
if parts.is_empty() {
return String::new();
}
match language {
"rust" | "c" | "cpp" => parts.join("::"),
"typescript" | "javascript" => {
if parts.len() >= 2 {
format!("{}/{}", parts[0], parts[1..].join("."))
} else {
parts[0].clone()
}
}
_ => parts.join("."),
}
}
fn format_module_qualified_name(module: &str, name: &str, language: &str) -> String {
match language {
"rust" | "c" | "cpp" => format!("{}::{}", module, name),
"typescript" | "javascript" => format!("{}/{}", module, name),
_ => format!("{}.{}", module, name),
}
}
fn format_method_qualified_name(module: &str, obj: &str, method: &str, language: &str) -> String {
match language {
"rust" | "c" | "cpp" => format!("{}::{}::{}", module, obj, method),
"typescript" | "javascript" => format!("{}/{}.{}", module, obj, method),
_ => format!("{}.{}.{}", module, obj, method),
}
}
pub fn resolve_calls(
files: &[PathBuf],
index: &FunctionIndex,
project_root: &Path,
) -> Result<CallGraph> {
let registry = LanguageRegistry::global();
let file_infos: Vec<FileCallInfo> = if files.len() >= MIN_FILES_FOR_PARALLEL {
files
.par_iter()
.filter_map(|path| extract_file_calls(path, registry, project_root).ok().flatten())
.collect()
} else {
files
.iter()
.filter_map(|path| extract_file_calls(path, registry, project_root).ok().flatten())
.collect()
};
let edges: Vec<CallEdge> = file_infos
.par_iter()
.flat_map(|info| resolve_file_calls(info, index))
.collect();
let mut graph = CallGraph::from_edges(edges);
graph.build_indexes();
Ok(graph)
}
fn extract_file_calls(
path: &PathBuf,
registry: &'static LanguageRegistry,
project_root: &Path,
) -> Result<Option<FileCallInfo>> {
let lang = match registry.detect_language(path) {
Some(l) => l,
None => return Ok(None),
};
let source = fs::read(path).map_err(|e| BrrrError::io_with_path(e, path))?;
let mut parser = lang.parser_for_path(path)?;
let tree = parser
.parse(&source, None)
.ok_or_else(|| BrrrError::Parse {
file: path.display().to_string(),
message: "Failed to parse file".to_string(),
})?;
let language_name = lang.name().to_string();
let mut context = FileContext {
file_path: path.display().to_string(),
language: language_name.clone(),
..Default::default()
};
let imports = lang.extract_imports(&tree, &source);
build_import_map(&imports, &mut context, path, project_root);
let call_sites = match language_name.as_str() {
"python" => extract_python_calls(&tree, &source, &mut context),
"typescript" => extract_typescript_calls(&tree, &source, &mut context),
"go" => extract_go_calls(&tree, &source, &mut context),
"rust" => extract_rust_calls(&tree, &source, &mut context),
"java" => extract_java_calls(&tree, &source, &mut context),
"c" | "cpp" => extract_c_calls(&tree, &source, &mut context),
_ => Vec::new(),
};
Ok(Some(FileCallInfo {
file_path: path.display().to_string(),
call_sites,
context,
}))
}
fn build_import_map(
imports: &[ImportInfo],
context: &mut FileContext,
current_file: &Path,
project_root: &Path,
) {
for import in imports {
let resolved_module = if import.level > 0 {
resolve_relative_import(import, current_file, project_root)
.unwrap_or_else(|| import.module.clone())
} else {
import.module.clone()
};
if import.is_from {
if import.names.iter().any(|n| n == "*") {
if !resolved_module.is_empty() {
context.star_imports.push(resolved_module.clone());
}
continue;
}
for name in &import.names {
context
.from_imports
.insert(name.clone(), (resolved_module.clone(), name.clone()));
if let Some(alias) = import.aliases.get(name) {
context
.from_imports
.insert(alias.clone(), (resolved_module.clone(), name.clone()));
}
}
} else {
let alias = import.aliases.values().next().cloned().unwrap_or_else(|| {
resolved_module
.split(|c| c == '.' || c == '/')
.last()
.unwrap_or(&resolved_module)
.to_string()
});
context.module_imports.insert(alias, resolved_module);
}
}
}
fn extract_python_calls(
tree: &tree_sitter::Tree,
source: &[u8],
context: &mut FileContext,
) -> Vec<CallSite> {
let mut calls = Vec::new();
let mut current_function: Option<String> = None;
collect_python_definitions(tree.root_node(), source, context, 0);
collect_python_calls(
tree.root_node(),
source,
context,
&mut current_function,
&mut calls,
0,
);
calls
}
fn collect_python_definitions(node: Node, source: &[u8], context: &mut FileContext, depth: usize) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_definition" => {
if let Some(name) = get_child_text(node, "identifier", source) {
context.defined_functions.insert(name);
}
}
"class_definition" => {
if let Some(name) = get_child_text(node, "identifier", source) {
context.defined_classes.insert(name);
}
}
"decorated_definition" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "function_definition" || child.kind() == "class_definition" {
collect_python_definitions(child, source, context, depth + 1);
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_python_definitions(child, source, context, depth + 1);
}
}
fn collect_python_calls(
node: Node,
source: &[u8],
context: &FileContext,
current_function: &mut Option<String>,
calls: &mut Vec<CallSite>,
depth: usize,
) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_definition" => {
let name = get_child_text(node, "identifier", source);
let prev = current_function.clone();
*current_function = name;
if let Some(block) = child_by_kind(node, "block") {
collect_python_calls(block, source, context, current_function, calls, depth + 1);
}
*current_function = prev;
return;
}
"call" => {
if let Some(target) = extract_python_call_target(node, source, context) {
calls.push(CallSite {
caller_name: current_function.clone(),
target,
line: node.start_position().row + 1,
});
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_python_calls(child, source, context, current_function, calls, depth + 1);
}
}
fn extract_python_call_target(
node: Node,
source: &[u8],
context: &FileContext,
) -> Option<CallTarget> {
if is_python_chained_call(node) {
let chain = extract_python_chained_call(node, source);
if chain.len() > 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
let func_node =
child_by_kind(node, "identifier").or_else(|| child_by_kind(node, "attribute"))?;
match func_node.kind() {
"identifier" => {
let name = node_text(func_node, source).to_string();
if context.defined_classes.contains(&name) {
Some(CallTarget::Constructor(name))
} else {
Some(CallTarget::Direct(name))
}
}
"attribute" => {
let mut cursor = func_node.walk();
for child in func_node.children(&mut cursor) {
if child.kind() == "call" {
let chain = extract_python_chained_call(node, source);
if chain.len() >= 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
}
let parts = extract_attribute_chain(func_node, source);
match (parts.first(), parts.last(), parts.len()) {
(Some(obj), Some(method), len) if len >= 2 => {
if context.module_imports.contains_key(obj)
|| context.from_imports.contains_key(obj)
{
Some(CallTarget::Qualified(parts))
} else {
Some(CallTarget::Method(obj.clone(), method.clone()))
}
}
(Some(first), _, 1) => Some(CallTarget::Direct(first.clone())),
_ => None,
}
}
_ => None,
}
}
fn extract_typescript_calls(
tree: &tree_sitter::Tree,
source: &[u8],
context: &mut FileContext,
) -> Vec<CallSite> {
let mut calls = Vec::new();
let mut current_function: Option<String> = None;
collect_ts_definitions(tree.root_node(), source, context, 0);
collect_ts_calls(
tree.root_node(),
source,
context,
&mut current_function,
&mut calls,
0,
);
calls
}
fn collect_ts_definitions(node: Node, source: &[u8], context: &mut FileContext, depth: usize) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_declaration" | "method_definition" => {
if let Some(name) = get_child_text(node, "identifier", source)
.or_else(|| get_child_text(node, "property_identifier", source))
{
context.defined_functions.insert(name);
}
}
"class_declaration" => {
if let Some(name) = get_child_text(node, "type_identifier", source)
.or_else(|| get_child_text(node, "identifier", source))
{
context.defined_classes.insert(name);
}
}
"lexical_declaration" | "variable_declaration" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "variable_declarator" {
let name = get_child_text(child, "identifier", source);
let has_function = child_by_kind(child, "arrow_function").is_some()
|| child_by_kind(child, "function_expression").is_some();
if let (Some(n), true) = (name, has_function) {
context.defined_functions.insert(n);
}
}
}
}
"export_statement" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_ts_definitions(child, source, context, depth + 1);
}
return;
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_ts_definitions(child, source, context, depth + 1);
}
}
fn collect_ts_calls(
node: Node,
source: &[u8],
context: &FileContext,
current_function: &mut Option<String>,
calls: &mut Vec<CallSite>,
depth: usize,
) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_declaration" | "arrow_function" | "function_expression" => {
let name = get_child_text(node, "identifier", source);
let prev = current_function.clone();
*current_function = name.or_else(|| prev.clone());
let body = child_by_kind(node, "statement_block")
.or_else(|| child_by_kind(node, "expression_statement"));
if let Some(b) = body {
collect_ts_calls(b, source, context, current_function, calls, depth + 1);
}
*current_function = prev;
return;
}
"method_definition" => {
let name = get_child_text(node, "property_identifier", source);
let prev = current_function.clone();
*current_function = name;
if let Some(body) = child_by_kind(node, "statement_block") {
collect_ts_calls(body, source, context, current_function, calls, depth + 1);
}
*current_function = prev;
return;
}
"call_expression" => {
if let Some(target) = extract_ts_call_target(node, source, context) {
calls.push(CallSite {
caller_name: current_function.clone(),
target,
line: node.start_position().row + 1,
});
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_ts_calls(child, source, context, current_function, calls, depth + 1);
}
}
fn extract_ts_call_target(node: Node, source: &[u8], context: &FileContext) -> Option<CallTarget> {
if is_ts_chained_call(node) {
let chain = extract_ts_chained_call(node, source);
if chain.len() > 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
let name = node_text(child, source).to_string();
if context.defined_classes.contains(&name) {
return Some(CallTarget::Constructor(name));
}
return Some(CallTarget::Direct(name));
}
"member_expression" => {
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "call_expression" {
let chain = extract_ts_chained_call(node, source);
if chain.len() >= 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
}
let parts = extract_member_expression_chain(child, source);
if let (Some(obj), Some(method)) = (parts.first(), parts.last()) {
if parts.len() >= 2 {
if context.module_imports.contains_key(obj)
|| context.from_imports.contains_key(obj)
{
return Some(CallTarget::Qualified(parts));
}
return Some(CallTarget::Method(obj.clone(), method.clone()));
}
}
}
"new_expression" => {
if let Some(id) = child_by_kind(child, "identifier") {
return Some(CallTarget::Constructor(node_text(id, source).to_string()));
}
}
_ => {}
}
}
None
}
fn extract_go_calls(
tree: &tree_sitter::Tree,
source: &[u8],
context: &mut FileContext,
) -> Vec<CallSite> {
let mut calls = Vec::new();
let mut current_function: Option<String> = None;
collect_go_definitions(tree.root_node(), source, context, 0);
collect_go_calls(
tree.root_node(),
source,
context,
&mut current_function,
&mut calls,
0,
);
calls
}
fn collect_go_definitions(node: Node, source: &[u8], context: &mut FileContext, depth: usize) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_declaration" => {
if let Some(name) = get_child_text(node, "identifier", source) {
context.defined_functions.insert(name);
}
}
"method_declaration" => {
if let Some(name) = get_child_text(node, "field_identifier", source) {
context.defined_functions.insert(name);
}
}
"type_declaration" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "type_spec" {
if let Some(name) = get_child_text(child, "type_identifier", source) {
context.defined_classes.insert(name);
}
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_go_definitions(child, source, context, depth + 1);
}
}
fn collect_go_calls(
node: Node,
source: &[u8],
context: &FileContext,
current_function: &mut Option<String>,
calls: &mut Vec<CallSite>,
depth: usize,
) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_declaration" | "method_declaration" => {
let name = get_child_text(node, "identifier", source)
.or_else(|| get_child_text(node, "field_identifier", source));
let prev = current_function.clone();
*current_function = name;
if let Some(body) = child_by_kind(node, "block") {
collect_go_calls(body, source, context, current_function, calls, depth + 1);
}
*current_function = prev;
return;
}
"call_expression" => {
if let Some(target) = extract_go_call_target(node, source, context) {
calls.push(CallSite {
caller_name: current_function.clone(),
target,
line: node.start_position().row + 1,
});
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_go_calls(child, source, context, current_function, calls, depth + 1);
}
}
fn extract_go_call_target(node: Node, source: &[u8], context: &FileContext) -> Option<CallTarget> {
if is_go_chained_call(node) {
let chain = extract_go_chained_call(node, source);
if chain.len() > 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
return Some(CallTarget::Direct(node_text(child, source).to_string()));
}
"selector_expression" => {
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "call_expression" {
let chain = extract_go_chained_call(node, source);
if chain.len() >= 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
}
let mut parts: AttributeChain = SmallVec::new();
extract_go_selector_parts(child, source, &mut parts);
if let (Some(obj), Some(method)) = (parts.first(), parts.last()) {
if parts.len() >= 2 {
if context.module_imports.contains_key(obj) {
return Some(CallTarget::Qualified(parts));
}
return Some(CallTarget::Method(obj.clone(), method.clone()));
}
}
}
"type_identifier" => {
return Some(CallTarget::Constructor(
node_text(child, source).to_string(),
));
}
_ => {}
}
}
None
}
fn extract_go_selector_parts(node: Node, source: &[u8], parts: &mut AttributeChain) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
parts.push(node_text(child, source).to_string());
}
"field_identifier" => {
parts.push(node_text(child, source).to_string());
}
"selector_expression" => {
extract_go_selector_parts(child, source, parts);
}
_ => {}
}
}
}
fn extract_rust_calls(
tree: &tree_sitter::Tree,
source: &[u8],
context: &mut FileContext,
) -> Vec<CallSite> {
let mut calls = Vec::new();
let mut current_function: Option<String> = None;
collect_rust_definitions(tree.root_node(), source, context, 0);
collect_rust_calls(
tree.root_node(),
source,
context,
&mut current_function,
&mut calls,
0,
);
calls
}
fn collect_rust_definitions(node: Node, source: &[u8], context: &mut FileContext, depth: usize) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_item" => {
if let Some(name) = get_child_text(node, "identifier", source) {
context.defined_functions.insert(name);
}
}
"struct_item" | "enum_item" => {
if let Some(name) = get_child_text(node, "type_identifier", source)
.or_else(|| get_child_text(node, "identifier", source))
{
context.defined_classes.insert(name);
}
}
"impl_item" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "declaration_list" {
collect_rust_definitions(child, source, context, depth + 1);
}
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_rust_definitions(child, source, context, depth + 1);
}
}
fn collect_rust_calls(
node: Node,
source: &[u8],
context: &FileContext,
current_function: &mut Option<String>,
calls: &mut Vec<CallSite>,
depth: usize,
) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_item" => {
let name = get_child_text(node, "identifier", source);
let prev = current_function.clone();
*current_function = name;
if let Some(body) = child_by_kind(node, "block") {
collect_rust_calls(body, source, context, current_function, calls, depth + 1);
}
*current_function = prev;
return;
}
"call_expression" => {
if let Some(target) = extract_rust_call_target(node, source, context) {
calls.push(CallSite {
caller_name: current_function.clone(),
target,
line: node.start_position().row + 1,
});
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_rust_calls(child, source, context, current_function, calls, depth + 1);
}
}
fn extract_rust_call_target(
node: Node,
source: &[u8],
context: &FileContext,
) -> Option<CallTarget> {
if is_rust_chained_call(node) {
let chain = extract_rust_chained_call(node, source);
if chain.len() > 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
let name = node_text(child, source).to_string();
if context.defined_classes.contains(&name) {
return Some(CallTarget::Constructor(name));
}
return Some(CallTarget::Direct(name));
}
"scoped_identifier" => {
let parts = extract_rust_path_parts(child, source);
if !parts.is_empty() {
return Some(CallTarget::Qualified(parts));
}
}
"field_expression" => {
if let Some(method) = get_child_text(child, "field_identifier", source) {
if let Some(obj) = child_by_kind(child, "identifier") {
return Some(CallTarget::Method(
node_text(obj, source).to_string(),
method,
));
}
let chain = extract_rust_chained_call(node, source);
if chain.len() >= 2 {
return Some(CallTarget::ChainedCall(chain));
}
return Some(CallTarget::Direct(method));
}
}
_ => {}
}
}
None
}
fn extract_rust_path_parts(node: Node, source: &[u8]) -> AttributeChain {
let mut parts = SmallVec::new();
extract_rust_path_parts_recursive(node, source, &mut parts);
parts
}
fn extract_rust_path_parts_recursive(node: Node, source: &[u8], parts: &mut AttributeChain) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" | "type_identifier" => {
parts.push(node_text(child, source).to_string());
}
"scoped_identifier" => {
extract_rust_path_parts_recursive(child, source, parts);
}
_ => {}
}
}
}
fn extract_java_calls(
tree: &tree_sitter::Tree,
source: &[u8],
context: &mut FileContext,
) -> Vec<CallSite> {
let mut calls = Vec::new();
let mut current_function: Option<String> = None;
collect_java_definitions(tree.root_node(), source, context, 0);
collect_java_calls(
tree.root_node(),
source,
context,
&mut current_function,
&mut calls,
0,
);
calls
}
fn collect_java_definitions(node: Node, source: &[u8], context: &mut FileContext, depth: usize) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"method_declaration" | "constructor_declaration" => {
if let Some(name) = get_child_text(node, "identifier", source) {
context.defined_functions.insert(name);
}
}
"class_declaration" | "interface_declaration" => {
if let Some(name) = get_child_text(node, "identifier", source) {
context.defined_classes.insert(name);
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_java_definitions(child, source, context, depth + 1);
}
}
fn collect_java_calls(
node: Node,
source: &[u8],
context: &FileContext,
current_function: &mut Option<String>,
calls: &mut Vec<CallSite>,
depth: usize,
) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"method_declaration" | "constructor_declaration" => {
let name = get_child_text(node, "identifier", source);
let prev = current_function.clone();
*current_function = name;
if let Some(body) = child_by_kind(node, "block") {
collect_java_calls(body, source, context, current_function, calls, depth + 1);
}
*current_function = prev;
return;
}
"method_invocation" => {
if let Some(target) = extract_java_call_target(node, source, context) {
calls.push(CallSite {
caller_name: current_function.clone(),
target,
line: node.start_position().row + 1,
});
}
}
"object_creation_expression" => {
if let Some(type_node) = child_by_kind(node, "type_identifier") {
calls.push(CallSite {
caller_name: current_function.clone(),
target: CallTarget::Constructor(node_text(type_node, source).to_string()),
line: node.start_position().row + 1,
});
}
}
"lambda_expression" => {
}
"method_reference" => {
if let Some(target) = extract_java_method_reference(node, source) {
calls.push(CallSite {
caller_name: current_function.clone(),
target,
line: node.start_position().row + 1,
});
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_java_calls(child, source, context, current_function, calls, depth + 1);
}
}
fn extract_java_method_reference(node: Node, source: &[u8]) -> Option<CallTarget> {
let mut type_or_expr: Option<String> = None;
let mut method_name: Option<String> = None;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" | "type_identifier" => {
if method_name.is_none() {
if type_or_expr.is_none() {
type_or_expr = Some(node_text(child, source).to_string());
} else {
method_name = Some(node_text(child, source).to_string());
}
}
}
"this" | "super" => {
type_or_expr = Some(node_text(child, source).to_string());
}
"field_access" => {
type_or_expr = Some(node_text(child, source).to_string());
}
"::" => {
}
_ => {}
}
}
let mut found_double_colon = false;
let mut cursor2 = node.walk();
for child in node.children(&mut cursor2) {
if child.kind() == "::" {
found_double_colon = true;
} else if found_double_colon && child.kind() == "identifier" {
method_name = Some(node_text(child, source).to_string());
break;
}
}
if let Some(method) = method_name {
if let Some(obj) = type_or_expr {
if obj == "this" || obj == "super" {
return Some(CallTarget::Direct(method));
}
return Some(CallTarget::Method(obj, method));
}
return Some(CallTarget::Direct(method));
}
None
}
fn extract_java_call_target(
node: Node,
source: &[u8],
_context: &FileContext,
) -> Option<CallTarget> {
if is_java_chained_call(node) {
let chain = extract_java_chained_call(node, source);
if chain.len() > 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
let method_name = get_child_text(node, "identifier", source)?;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" if node_text(child, source) != method_name => {
let obj = node_text(child, source).to_string();
return Some(CallTarget::Method(obj, method_name));
}
"field_access" => {
let parts = extract_java_field_access_parts(child, source);
if !parts.is_empty() {
let mut full_parts = parts;
full_parts.push(method_name.clone());
return Some(CallTarget::Qualified(full_parts));
}
}
"method_invocation" => {
let chain = extract_java_chained_call(node, source);
if chain.len() >= 2 {
return Some(CallTarget::ChainedCall(chain));
}
return Some(CallTarget::Direct(method_name));
}
_ => {}
}
}
Some(CallTarget::Direct(method_name))
}
fn extract_java_field_access_parts(node: Node, source: &[u8]) -> AttributeChain {
let mut parts = SmallVec::new();
extract_java_field_access_parts_recursive(node, source, &mut parts);
parts
}
fn extract_java_field_access_parts_recursive(node: Node, source: &[u8], parts: &mut AttributeChain) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
parts.push(node_text(child, source).to_string());
}
"field_access" => {
extract_java_field_access_parts_recursive(child, source, parts);
}
_ => {}
}
}
}
fn extract_c_calls(
tree: &tree_sitter::Tree,
source: &[u8],
context: &mut FileContext,
) -> Vec<CallSite> {
let mut calls = Vec::new();
let mut current_function: Option<String> = None;
collect_c_definitions(tree.root_node(), source, context, 0);
collect_c_calls(
tree.root_node(),
source,
context,
&mut current_function,
&mut calls,
0,
);
calls
}
fn collect_c_definitions(node: Node, source: &[u8], context: &mut FileContext, depth: usize) {
if depth > MAX_TREE_DEPTH {
return;
}
if node.kind() == "function_definition" {
if let Some(decl) = child_by_kind(node, "function_declarator") {
if let Some(name) = get_child_text(decl, "identifier", source) {
context.defined_functions.insert(name);
}
}
if let Some(ptr_decl) = child_by_kind(node, "pointer_declarator") {
if let Some(func_decl) = child_by_kind(ptr_decl, "function_declarator") {
if let Some(name) = get_child_text(func_decl, "identifier", source) {
context.defined_functions.insert(name);
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_c_definitions(child, source, context, depth + 1);
}
}
fn collect_c_calls(
node: Node,
source: &[u8],
context: &FileContext,
current_function: &mut Option<String>,
calls: &mut Vec<CallSite>,
depth: usize,
) {
if depth > MAX_TREE_DEPTH {
return;
}
match node.kind() {
"function_definition" => {
let name = child_by_kind(node, "function_declarator")
.and_then(|d| get_child_text(d, "identifier", source));
let prev = current_function.clone();
*current_function = name;
if let Some(body) = child_by_kind(node, "compound_statement") {
collect_c_calls(body, source, context, current_function, calls, depth + 1);
}
*current_function = prev;
return;
}
"call_expression" => {
if let Some(target) = extract_c_call_target(node, source) {
calls.push(CallSite {
caller_name: current_function.clone(),
target,
line: node.start_position().row + 1,
});
}
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_c_calls(child, source, context, current_function, calls, depth + 1);
}
}
fn extract_c_call_target(node: Node, source: &[u8]) -> Option<CallTarget> {
if is_c_chained_call(node) {
let chain = extract_c_chained_call(node, source);
if chain.len() > 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
return Some(CallTarget::Direct(node_text(child, source).to_string()));
}
"qualified_identifier" => {
let full_name = node_text(child, source);
let parts: AttributeChain = full_name.split("::").map(|s| s.to_string()).collect();
if parts.len() == 1 {
return Some(CallTarget::Direct(parts[0].clone()));
}
return Some(CallTarget::Qualified(parts));
}
"template_function" => {
if let Some(name_node) = child_by_kind(child, "identifier") {
return Some(CallTarget::Direct(node_text(name_node, source).to_string()));
}
if let Some(qid) = child_by_kind(child, "qualified_identifier") {
let full_name = node_text(qid, source);
let parts: AttributeChain = full_name.split("::").map(|s| s.to_string()).collect();
if parts.len() == 1 {
return Some(CallTarget::Direct(parts[0].clone()));
}
return Some(CallTarget::Qualified(parts));
}
}
"field_expression" => {
if let Some(field) = get_child_text(child, "field_identifier", source) {
if let Some(obj) = child_by_kind(child, "identifier") {
return Some(CallTarget::Method(
node_text(obj, source).to_string(),
field,
));
}
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "call_expression" {
let chain = extract_c_chained_call(node, source);
if chain.len() >= 2 {
return Some(CallTarget::ChainedCall(chain));
}
}
}
}
}
_ => {}
}
}
None
}
fn score_candidate(candidate: &FunctionRef, context: &FileContext) -> i32 {
let mut score = 0;
for (_, (module, original_name)) in &context.from_imports {
if candidate.name == *original_name {
let module_parts: Vec<&str> = module.split('.').collect();
let file_contains_module = module_parts
.iter()
.any(|part| candidate.file.contains(part));
if file_contains_module {
score += 100;
break;
}
}
}
let candidate_dir = std::path::Path::new(&candidate.file).parent();
let current_dir = std::path::Path::new(&context.file_path).parent();
if let (Some(cand_dir), Some(curr_dir)) = (candidate_dir, current_dir) {
if cand_dir == curr_dir {
score += 50;
}
}
let candidate_root = candidate
.file
.split(|c| c == '/' || c == '\\')
.find(|s| !s.is_empty());
let current_root = context
.file_path
.split(|c| c == '/' || c == '\\')
.find(|s| !s.is_empty());
if let (Some(cand_root), Some(curr_root)) = (candidate_root, current_root) {
if cand_root == curr_root {
score += 25;
}
}
if let Some(ref qname) = candidate.qualified_name {
for module in context.module_imports.values() {
if qname.starts_with(module) {
score += 10;
break;
}
}
}
score
}
fn select_best_candidate(candidates: &[&FunctionRef], context: &FileContext) -> Option<FunctionRef> {
if candidates.is_empty() {
return None;
}
if candidates.len() == 1 {
return Some(candidates[0].clone());
}
let mut scored: Vec<(i32, &FunctionRef)> = candidates
.iter()
.map(|c| (score_candidate(c, context), *c))
.collect();
scored.sort_by(|a, b| {
match b.0.cmp(&a.0) {
std::cmp::Ordering::Equal => {
match a.1.file.cmp(&b.1.file) {
std::cmp::Ordering::Equal => {
a.1.name.cmp(&b.1.name)
}
other => other,
}
}
other => other,
}
});
Some(scored[0].1.clone())
}
fn resolve_file_calls(info: &FileCallInfo, index: &FunctionIndex) -> Vec<CallEdge> {
let mut edges = Vec::new();
for call_site in &info.call_sites {
let caller = FunctionRef {
file: info.file_path.clone(),
name: call_site
.caller_name
.clone()
.unwrap_or_else(|| "<module>".to_string()),
qualified_name: None,
};
if let Some(callee) = resolve_call_target(&call_site.target, &info.context, index) {
edges.push(CallEdge {
caller,
callee,
call_line: call_site.line,
});
}
}
edges
}
fn resolve_with_star_imports(
name: &str,
context: &FileContext,
index: &FunctionIndex,
) -> Option<FunctionRef> {
for module in &context.star_imports {
let qname = format_module_qualified_name(module, name, &context.language);
if let Some(func) = index.lookup_qualified(&qname) {
return Some(func.clone());
}
let candidates = index.lookup(name);
for candidate in &candidates {
let module_parts: Vec<&str> = module.split('.').collect();
let file_matches_module = module_parts.iter().any(|part| {
candidate.file.contains(part)
|| candidate
.qualified_name
.as_ref()
.is_some_and(|q| q.contains(part))
});
if file_matches_module {
return Some((*candidate).clone());
}
}
}
None
}
fn resolve_call_target(
target: &CallTarget,
context: &FileContext,
index: &FunctionIndex,
) -> Option<FunctionRef> {
match target {
CallTarget::Direct(name) => {
if context.defined_functions.contains(name) {
return Some(FunctionRef {
file: context.file_path.clone(),
name: name.clone(),
qualified_name: None,
});
}
if let Some((module, original)) = context.from_imports.get(name) {
let qname = format_module_qualified_name(module, original, &context.language);
if let Some(func) = index.lookup_qualified(&qname) {
return Some(func.clone());
}
let candidates = index.lookup(original);
if candidates.len() == 1 {
return Some(candidates[0].clone());
}
}
let candidates = index.lookup(name);
if candidates.len() == 1 {
return Some(candidates[0].clone());
}
if !context.star_imports.is_empty() {
if let Some(func) = resolve_with_star_imports(name, context, index) {
return Some(func);
}
}
select_best_candidate(&candidates, context)
}
CallTarget::Method(obj, method) => {
let candidates = index.lookup(method);
if candidates.len() == 1 {
return Some(candidates[0].clone());
}
if let Some(module) = context.module_imports.get(obj) {
let qname = format_module_qualified_name(module, method, &context.language);
if let Some(func) = index.lookup_qualified(&qname) {
return Some(func.clone());
}
}
if let Some((module, original)) = context.from_imports.get(obj) {
let qname = format_method_qualified_name(module, original, method, &context.language);
if let Some(func) = index.lookup_qualified(&qname) {
return Some(func.clone());
}
}
select_best_candidate(&candidates, context)
}
CallTarget::Qualified(parts) => {
let resolved_parts = if !parts.is_empty() {
if let Some(module) = context.module_imports.get(&parts[0]) {
let mut resolved: Vec<String> =
module.split('.').map(|s| s.to_string()).collect();
resolved.extend(parts[1..].iter().cloned());
resolved
} else if let Some((module, original)) = context.from_imports.get(&parts[0]) {
let mut resolved: Vec<String> =
module.split('.').map(|s| s.to_string()).collect();
resolved.push(original.clone());
resolved.extend(parts[1..].iter().cloned());
resolved
} else {
parts.to_vec()
}
} else {
parts.to_vec()
};
let full_name = format_qualified_name(&resolved_parts, &context.language);
if let Some(func) = index.lookup_qualified(&full_name) {
return Some(func.clone());
}
if resolved_parts[..] != parts[..] {
let original_name = format_qualified_name(parts, &context.language);
if let Some(func) = index.lookup_qualified(&original_name) {
return Some(func.clone());
}
}
if let Some(name) = resolved_parts.last() {
let candidates = index.lookup(name);
if candidates.len() == 1 {
return Some(candidates[0].clone());
}
let prefix_parts: Vec<String> = resolved_parts[..resolved_parts.len() - 1].to_vec();
let module_prefix = format_qualified_name(&prefix_parts, &context.language);
for candidate in &candidates {
if candidate
.qualified_name
.as_ref()
.is_some_and(|q| q.starts_with(&module_prefix))
{
return Some((*candidate).clone());
}
}
if resolved_parts[..] != parts[..] && parts.len() > 1 {
let orig_prefix_parts: Vec<String> = parts[..parts.len() - 1].to_vec();
let original_prefix = format_qualified_name(&orig_prefix_parts, &context.language);
for candidate in &candidates {
if candidate
.qualified_name
.as_ref()
.is_some_and(|q| q.starts_with(&original_prefix))
{
return Some((*candidate).clone());
}
}
}
select_best_candidate(&candidates, context)
} else {
None
}
}
CallTarget::Constructor(name) => {
let candidates = index.lookup(name);
if candidates.len() == 1 {
return Some(candidates[0].clone());
}
if let Some((module, original)) = context.from_imports.get(name) {
for candidate in &candidates {
if candidate.file.contains(module) && candidate.name == *original {
return Some((*candidate).clone());
}
}
let module_parts: Vec<&str> = module.split('.').collect();
for candidate in &candidates {
let file_matches_module = module_parts
.iter()
.any(|part| candidate.file.contains(part));
if file_matches_module && candidate.name == *original {
return Some((*candidate).clone());
}
}
}
for (alias, module) in &context.module_imports {
if alias == name || module.ends_with(name) {
for candidate in &candidates {
if candidate.file.contains(module) {
return Some((*candidate).clone());
}
}
let module_parts: Vec<&str> = module.split('.').collect();
for candidate in &candidates {
let file_matches = module_parts
.iter()
.any(|part| candidate.file.contains(part));
if file_matches {
return Some((*candidate).clone());
}
}
}
}
select_best_candidate(&candidates, context)
}
CallTarget::ChainedCall(chain) => {
if chain.is_empty() {
return None;
}
let last_method = chain.last()?;
let candidates = index.lookup(last_method);
if candidates.len() == 1 {
return Some(candidates[0].clone());
}
let first = &chain[0];
if let Some(module) = context.module_imports.get(first) {
let mut qparts: Vec<String> = module.split('.').map(|s| s.to_string()).collect();
qparts.extend(chain[1..].iter().cloned());
let qname = format_qualified_name(&qparts, &context.language);
if let Some(func) = index.lookup_qualified(&qname) {
return Some(func.clone());
}
}
if let Some((module, original)) = context.from_imports.get(first) {
let mut qparts: Vec<String> = module.split('.').map(|s| s.to_string()).collect();
qparts.push(original.clone());
qparts.extend(chain[1..].iter().cloned());
let qname = format_qualified_name(&qparts, &context.language);
if let Some(func) = index.lookup_qualified(&qname) {
return Some(func.clone());
}
}
for method in chain.iter().rev() {
let method_candidates = index.lookup(method);
if method_candidates.len() == 1 {
return Some(method_candidates[0].clone());
}
}
select_best_candidate(&candidates, context)
}
}
}
fn node_text<'a>(node: Node<'a>, source: &'a [u8]) -> &'a str {
std::str::from_utf8(&source[node.start_byte()..node.end_byte()]).unwrap_or("")
}
fn child_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == kind {
return Some(child);
}
}
None
}
fn get_child_text(node: Node, kind: &str, source: &[u8]) -> Option<String> {
child_by_kind(node, kind).map(|n| node_text(n, source).to_string())
}
fn extract_attribute_chain(node: Node, source: &[u8]) -> AttributeChain {
let mut parts = SmallVec::new();
extract_attribute_chain_recursive(node, source, &mut parts);
parts
}
fn extract_attribute_chain_recursive(node: Node, source: &[u8], parts: &mut AttributeChain) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
parts.push(node_text(child, source).to_string());
}
"attribute" => {
extract_attribute_chain_recursive(child, source, parts);
}
_ => {}
}
}
}
fn extract_member_expression_chain(node: Node, source: &[u8]) -> AttributeChain {
let mut parts = SmallVec::new();
extract_member_expression_chain_recursive(node, source, &mut parts);
parts
}
fn extract_member_expression_chain_recursive(node: Node, source: &[u8], parts: &mut AttributeChain) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" | "property_identifier" => {
parts.push(node_text(child, source).to_string());
}
"member_expression" => {
extract_member_expression_chain_recursive(child, source, parts);
}
"this" => {
parts.push("this".to_string());
}
_ => {}
}
}
}
fn extract_rust_chained_call(node: Node, source: &[u8]) -> MethodChain {
let mut chain = SmallVec::new();
extract_rust_chain_recursive(node, source, &mut chain);
chain
}
fn extract_rust_chain_recursive(node: Node, source: &[u8], chain: &mut MethodChain) {
match node.kind() {
"call_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"field_expression" => {
extract_rust_chain_recursive(child, source, chain);
}
"identifier" => {
chain.push(node_text(child, source).to_string());
}
"scoped_identifier" => {
let parts = extract_rust_path_parts(child, source);
chain.extend(parts);
}
_ => {}
}
}
}
"field_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
chain.push(node_text(child, source).to_string());
}
"field_identifier" => {
chain.push(node_text(child, source).to_string());
}
"call_expression" | "field_expression" => {
extract_rust_chain_recursive(child, source, chain);
}
_ => {}
}
}
}
"identifier" => {
chain.push(node_text(node, source).to_string());
}
_ => {}
}
}
fn is_rust_chained_call(node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "field_expression" {
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "call_expression" {
return true;
}
}
}
}
false
}
fn extract_python_chained_call(node: Node, source: &[u8]) -> MethodChain {
let mut chain = SmallVec::new();
extract_python_chain_recursive(node, source, &mut chain);
chain
}
fn extract_python_chain_recursive(node: Node, source: &[u8], chain: &mut MethodChain) {
match node.kind() {
"call" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"attribute" => {
extract_python_chain_recursive(child, source, chain);
}
"identifier" => {
chain.push(node_text(child, source).to_string());
}
_ => {}
}
}
}
"attribute" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
chain.push(node_text(child, source).to_string());
}
"call" | "attribute" => {
extract_python_chain_recursive(child, source, chain);
}
_ => {}
}
}
}
"identifier" => {
chain.push(node_text(node, source).to_string());
}
_ => {}
}
}
fn is_python_chained_call(node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "attribute" {
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "call" {
return true;
}
}
}
}
false
}
fn extract_ts_chained_call(node: Node, source: &[u8]) -> MethodChain {
let mut chain = SmallVec::new();
extract_ts_chain_recursive(node, source, &mut chain);
chain
}
fn extract_ts_chain_recursive(node: Node, source: &[u8], chain: &mut MethodChain) {
match node.kind() {
"call_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"member_expression" => {
extract_ts_chain_recursive(child, source, chain);
}
"identifier" => {
chain.push(node_text(child, source).to_string());
}
_ => {}
}
}
}
"member_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" | "property_identifier" => {
chain.push(node_text(child, source).to_string());
}
"call_expression" | "member_expression" => {
extract_ts_chain_recursive(child, source, chain);
}
"this" => {
chain.push("this".to_string());
}
_ => {}
}
}
}
"identifier" => {
chain.push(node_text(node, source).to_string());
}
_ => {}
}
}
fn is_ts_chained_call(node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "member_expression" {
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "call_expression" {
return true;
}
}
}
}
false
}
fn extract_java_chained_call(node: Node, source: &[u8]) -> MethodChain {
let mut chain = SmallVec::new();
extract_java_chain_recursive(node, source, &mut chain);
chain
}
fn extract_java_chain_recursive(node: Node, source: &[u8], chain: &mut MethodChain) {
match node.kind() {
"method_invocation" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" => {
chain.push(node_text(child, source).to_string());
}
"method_invocation" => {
extract_java_chain_recursive(child, source, chain);
}
"field_access" => {
let parts = extract_java_field_access_parts(child, source);
chain.extend(parts);
}
_ => {}
}
}
}
"identifier" => {
chain.push(node_text(node, source).to_string());
}
_ => {}
}
}
fn is_java_chained_call(node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "method_invocation" {
return true;
}
}
false
}
fn extract_go_chained_call(node: Node, source: &[u8]) -> MethodChain {
let mut chain = SmallVec::new();
extract_go_chain_recursive(node, source, &mut chain);
chain
}
fn extract_go_chain_recursive(node: Node, source: &[u8], chain: &mut MethodChain) {
match node.kind() {
"call_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"selector_expression" => {
extract_go_chain_recursive(child, source, chain);
}
"identifier" => {
chain.push(node_text(child, source).to_string());
}
_ => {}
}
}
}
"selector_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" | "field_identifier" => {
chain.push(node_text(child, source).to_string());
}
"call_expression" | "selector_expression" => {
extract_go_chain_recursive(child, source, chain);
}
_ => {}
}
}
}
"identifier" => {
chain.push(node_text(node, source).to_string());
}
_ => {}
}
}
fn is_go_chained_call(node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "selector_expression" {
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "call_expression" {
return true;
}
}
}
}
false
}
fn extract_c_chained_call(node: Node, source: &[u8]) -> MethodChain {
let mut chain = SmallVec::new();
extract_c_chain_recursive(node, source, &mut chain);
chain
}
fn extract_c_chain_recursive(node: Node, source: &[u8], chain: &mut MethodChain) {
match node.kind() {
"call_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"field_expression" => {
extract_c_chain_recursive(child, source, chain);
}
"identifier" => {
chain.push(node_text(child, source).to_string());
}
_ => {}
}
}
}
"field_expression" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
match child.kind() {
"identifier" | "field_identifier" => {
chain.push(node_text(child, source).to_string());
}
"call_expression" | "field_expression" => {
extract_c_chain_recursive(child, source, chain);
}
_ => {}
}
}
}
"identifier" => {
chain.push(node_text(node, source).to_string());
}
_ => {}
}
}
fn is_c_chained_call(node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "field_expression" {
let mut inner_cursor = child.walk();
for inner_child in child.children(&mut inner_cursor) {
if inner_child.kind() == "call_expression" {
return true;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use smallvec::smallvec;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
fn setup_test_project() -> TempDir {
let dir = TempDir::new().unwrap();
let root = dir.path();
let py_content = r#"
from utils import helper
import os
def foo():
bar()
helper()
os.path.join("a", "b")
def bar():
pass
"#;
let mut f = File::create(root.join("main.py")).unwrap();
f.write_all(py_content.as_bytes()).unwrap();
let utils_content = r#"
def helper():
pass
"#;
let mut f = File::create(root.join("utils.py")).unwrap();
f.write_all(utils_content.as_bytes()).unwrap();
dir
}
#[test]
fn test_resolve_calls_basic() {
let dir = setup_test_project();
let root = dir.path();
let files = vec![root.join("main.py"), root.join("utils.py")];
let index = FunctionIndex::build(&files).unwrap();
let graph = resolve_calls(&files, &index, root).unwrap();
assert!(!graph.edges.is_empty());
let has_foo_bar = graph
.edges
.iter()
.any(|e| e.caller.name == "foo" && e.callee.name == "bar");
assert!(has_foo_bar, "Should find foo -> bar edge");
}
#[test]
fn test_call_target_variants() {
let target = CallTarget::Direct("test".to_string());
assert!(matches!(target, CallTarget::Direct(_)));
let target = CallTarget::Method("obj".to_string(), "method".to_string());
assert!(matches!(target, CallTarget::Method(_, _)));
let target = CallTarget::Qualified(smallvec!["a".to_string(), "b".to_string()]);
assert!(matches!(target, CallTarget::Qualified(_)));
let target = CallTarget::Constructor("MyClass".to_string());
assert!(matches!(target, CallTarget::Constructor(_)));
let target = CallTarget::ChainedCall(smallvec![
"data".to_string(),
"transform".to_string(),
"filter".to_string(),
"save".to_string(),
]);
assert!(matches!(target, CallTarget::ChainedCall(_)));
if let CallTarget::ChainedCall(chain) = target {
assert_eq!(chain.len(), 4);
assert_eq!(chain[0], "data");
assert_eq!(chain[3], "save");
}
}
#[test]
fn test_java_lambda_calls() {
let dir = TempDir::new().unwrap();
let root = dir.path();
let java_content = r#"
import java.util.List;
public class Service {
public void processItems(List<String> items) {
// Lambda expression - should track process() call
items.forEach(x -> process(x));
// this::method reference - should track handleItem
items.forEach(this::handleItem);
// Static method reference within same file
items.stream().map(Service::transform);
}
private void process(String item) {
// process implementation
}
private void handleItem(String item) {
// handle implementation
}
public static String transform(String s) {
return s.toUpperCase();
}
}
"#;
let mut f = File::create(root.join("Service.java")).unwrap();
f.write_all(java_content.as_bytes()).unwrap();
let files = vec![root.join("Service.java")];
let index = FunctionIndex::build(&files).unwrap();
let graph = resolve_calls(&files, &index, root).unwrap();
for edge in &graph.edges {
eprintln!(
"Edge: {} -> {} (line {})",
edge.caller.name, edge.callee.name, edge.call_line
);
}
let has_lambda_call = graph
.edges
.iter()
.any(|e| e.caller.name == "processItems" && e.callee.name == "process");
assert!(
has_lambda_call,
"Should find processItems -> process call from lambda"
);
let has_this_ref = graph
.edges
.iter()
.any(|e| e.caller.name == "processItems" && e.callee.name == "handleItem");
assert!(
has_this_ref,
"Should find processItems -> handleItem from this::method reference"
);
let has_static_ref = graph
.edges
.iter()
.any(|e| e.caller.name == "processItems" && e.callee.name == "transform");
assert!(
has_static_ref,
"Should find processItems -> transform from Service::transform method reference"
);
}
#[test]
fn test_aliased_from_import_resolution() {
let dir = TempDir::new().unwrap();
let root = dir.path();
let main_content = r#"
from utils import helper as h
def caller():
h() # Should resolve to utils.helper
"#;
let mut f = File::create(root.join("main.py")).unwrap();
f.write_all(main_content.as_bytes()).unwrap();
let utils_content = r#"
def helper():
pass
"#;
let mut f = File::create(root.join("utils.py")).unwrap();
f.write_all(utils_content.as_bytes()).unwrap();
let files = vec![root.join("main.py"), root.join("utils.py")];
let index = FunctionIndex::build(&files).unwrap();
let graph = resolve_calls(&files, &index, root).unwrap();
for edge in &graph.edges {
eprintln!(
"Edge: {} -> {} (line {})",
edge.caller.name, edge.callee.name, edge.call_line
);
}
let has_aliased_call = graph
.edges
.iter()
.any(|e| e.caller.name == "caller" && e.callee.name == "helper");
assert!(
has_aliased_call,
"Aliased import h() should resolve to utils.helper"
);
}
#[test]
fn test_build_import_map_alias_mapping() {
let mut context = FileContext::default();
let dummy_file = Path::new("/project/src/main.py");
let dummy_root = Path::new("/project");
let import = ImportInfo {
module: "utils".to_string(),
names: vec!["helper".to_string()],
aliases: [("helper".to_string(), "h".to_string())]
.into_iter()
.collect(),
is_from: true,
level: 0,
line_number: 1,
visibility: None,
};
build_import_map(&[import], &mut context, dummy_file, dummy_root);
assert!(
context.from_imports.contains_key("helper"),
"Original name 'helper' should be a key"
);
assert!(
context.from_imports.contains_key("h"),
"Alias 'h' should be a key"
);
let (mod1, orig1) = context.from_imports.get("helper").unwrap();
assert_eq!(mod1, "utils");
assert_eq!(orig1, "helper");
let (mod2, orig2) = context.from_imports.get("h").unwrap();
assert_eq!(mod2, "utils");
assert_eq!(orig2, "helper");
}
#[test]
fn test_ambiguous_resolution_deterministic() {
let dir = TempDir::new().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("pkg_a")).unwrap();
std::fs::create_dir_all(root.join("pkg_b")).unwrap();
let pkg_a_content = r#"
def process():
pass
"#;
let mut f = File::create(root.join("pkg_a").join("utils.py")).unwrap();
f.write_all(pkg_a_content.as_bytes()).unwrap();
let pkg_b_content = r#"
def process():
pass
"#;
let mut f = File::create(root.join("pkg_b").join("utils.py")).unwrap();
f.write_all(pkg_b_content.as_bytes()).unwrap();
let main_content = r#"
from pkg_a.utils import process
def caller():
process()
"#;
let mut f = File::create(root.join("pkg_a").join("main.py")).unwrap();
f.write_all(main_content.as_bytes()).unwrap();
let files = vec![
root.join("pkg_a").join("utils.py"),
root.join("pkg_b").join("utils.py"),
root.join("pkg_a").join("main.py"),
];
let mut resolved_files: Vec<String> = Vec::new();
for _ in 0..5 {
let index = FunctionIndex::build(&files).unwrap();
let graph = resolve_calls(&files, &index, root).unwrap();
let edge = graph
.edges
.iter()
.find(|e| e.caller.name == "caller" && e.callee.name == "process");
if let Some(e) = edge {
resolved_files.push(e.callee.file.clone());
}
}
assert!(
!resolved_files.is_empty(),
"Should resolve at least one call"
);
let first = &resolved_files[0];
for f in &resolved_files {
assert_eq!(
f, first,
"Resolution should be deterministic across multiple runs"
);
}
assert!(
first.contains("pkg_a"),
"Should prefer explicitly imported function from pkg_a, got: {}",
first
);
}
#[test]
fn test_score_candidate_same_directory() {
let context = FileContext {
file_path: "/project/src/api/routes.py".to_string(),
language: "python".to_string(),
from_imports: HashMap::new(),
module_imports: HashMap::new(),
defined_functions: HashSet::new(),
defined_classes: HashSet::new(),
..Default::default()
};
let same_dir_candidate = FunctionRef {
file: "/project/src/api/helpers.py".to_string(),
name: "process".to_string(),
qualified_name: None,
};
let different_dir_candidate = FunctionRef {
file: "/project/src/utils/helpers.py".to_string(),
name: "process".to_string(),
qualified_name: None,
};
let same_dir_score = score_candidate(&same_dir_candidate, &context);
let diff_dir_score = score_candidate(&different_dir_candidate, &context);
assert!(
same_dir_score > diff_dir_score,
"Same directory candidate should score higher: {} > {}",
same_dir_score,
diff_dir_score
);
}
#[test]
fn test_score_candidate_explicit_import() {
let mut from_imports = HashMap::new();
from_imports.insert(
"helper".to_string(),
("mymodule".to_string(), "helper".to_string()),
);
let context = FileContext {
file_path: "/project/main.py".to_string(),
language: "python".to_string(),
from_imports,
module_imports: HashMap::new(),
defined_functions: HashSet::new(),
defined_classes: HashSet::new(),
..Default::default()
};
let imported_candidate = FunctionRef {
file: "/project/mymodule/utils.py".to_string(),
name: "helper".to_string(),
qualified_name: None,
};
let other_candidate = FunctionRef {
file: "/project/other/utils.py".to_string(),
name: "helper".to_string(),
qualified_name: None,
};
let imported_score = score_candidate(&imported_candidate, &context);
let other_score = score_candidate(&other_candidate, &context);
assert!(
imported_score > other_score,
"Explicitly imported candidate should score higher: {} > {}",
imported_score,
other_score
);
}
#[test]
fn test_format_qualified_name_language_specific() {
let parts = vec!["module".to_string(), "Class".to_string(), "method".to_string()];
assert_eq!(format_qualified_name(&parts, "python"), "module.Class.method");
assert_eq!(format_qualified_name(&parts, "java"), "module.Class.method");
assert_eq!(format_qualified_name(&parts, "go"), "module.Class.method");
assert_eq!(format_qualified_name(&parts, "typescript"), "module/Class.method");
assert_eq!(format_qualified_name(&parts, "javascript"), "module/Class.method");
assert_eq!(format_qualified_name(&parts, "rust"), "module::Class::method");
assert_eq!(format_qualified_name(&parts, "c"), "module::Class::method");
assert_eq!(format_qualified_name(&parts, "cpp"), "module::Class::method");
let single = vec!["func".to_string()];
assert_eq!(format_qualified_name(&single, "python"), "func");
assert_eq!(format_qualified_name(&single, "rust"), "func");
assert_eq!(format_qualified_name(&single, "typescript"), "func");
let empty: Vec<String> = vec![];
assert_eq!(format_qualified_name(&empty, "python"), "");
}
#[test]
fn test_format_module_qualified_name() {
assert_eq!(
format_module_qualified_name("utils", "helper", "python"),
"utils.helper"
);
assert_eq!(
format_module_qualified_name("utils", "helper", "rust"),
"utils::helper"
);
assert_eq!(
format_module_qualified_name("utils", "helper", "typescript"),
"utils/helper"
);
}
#[test]
fn test_format_method_qualified_name() {
assert_eq!(
format_method_qualified_name("mod", "Class", "method", "python"),
"mod.Class.method"
);
assert_eq!(
format_method_qualified_name("mod", "Struct", "method", "rust"),
"mod::Struct::method"
);
assert_eq!(
format_method_qualified_name("mod", "Class", "method", "typescript"),
"mod/Class.method"
);
}
#[test]
fn test_constructor_resolution_with_import_context() {
let dir = TempDir::new().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("pkg_a")).unwrap();
std::fs::create_dir_all(root.join("pkg_b")).unwrap();
let pkg_a_content = r#"
class Model:
def __init__(self):
pass
"#;
let mut f = File::create(root.join("pkg_a").join("models.py")).unwrap();
f.write_all(pkg_a_content.as_bytes()).unwrap();
let pkg_b_content = r#"
class Model:
def __init__(self):
pass
"#;
let mut f = File::create(root.join("pkg_b").join("models.py")).unwrap();
f.write_all(pkg_b_content.as_bytes()).unwrap();
let main_content = r#"
from pkg_a.models import Model
def create_model():
obj = Model() # Should resolve to pkg_a.models.Model, not pkg_b
return obj
"#;
let mut f = File::create(root.join("main.py")).unwrap();
f.write_all(main_content.as_bytes()).unwrap();
let files = vec![
root.join("pkg_a").join("models.py"),
root.join("pkg_b").join("models.py"),
root.join("main.py"),
];
for iteration in 0..3 {
let index = FunctionIndex::build(&files).unwrap();
let graph = resolve_calls(&files, &index, root).unwrap();
for edge in &graph.edges {
eprintln!(
"[iter {}] Edge: {} ({}) -> {} ({})",
iteration, edge.caller.name, edge.caller.file, edge.callee.name, edge.callee.file
);
}
let constructor_edge = graph
.edges
.iter()
.find(|e| e.caller.name == "create_model" && e.callee.name == "Model");
assert!(
constructor_edge.is_some(),
"Should find constructor call from create_model to Model"
);
let edge = constructor_edge.unwrap();
assert!(
edge.callee.file.contains("pkg_a"),
"Constructor should resolve to explicitly imported pkg_a.models.Model, \
not arbitrary choice. Got: {}",
edge.callee.file
);
}
}
#[test]
fn test_constructor_resolution_module_import() {
let dir = TempDir::new().unwrap();
let root = dir.path();
let models_content = r#"
class User:
def __init__(self, name):
self.name = name
"#;
let mut f = File::create(root.join("models.py")).unwrap();
f.write_all(models_content.as_bytes()).unwrap();
let main_content = r#"
import models
def create_user():
user = models.User("test")
return user
"#;
let mut f = File::create(root.join("main.py")).unwrap();
f.write_all(main_content.as_bytes()).unwrap();
let files = vec![root.join("models.py"), root.join("main.py")];
let index = FunctionIndex::build(&files).unwrap();
let graph = resolve_calls(&files, &index, root).unwrap();
for edge in &graph.edges {
eprintln!(
"Edge: {} -> {} (line {})",
edge.caller.name, edge.callee.name, edge.call_line
);
}
let has_user_call = graph.edges.iter().any(|e| {
e.caller.name == "create_user"
&& (e.callee.name == "User" || e.callee.name.contains("User"))
});
assert!(
has_user_call,
"Should resolve models.User() constructor call"
);
}
#[test]
fn test_python_chained_method_calls() {
let dir = TempDir::new().unwrap();
let root = dir.path();
let py_content = r#"
class DataFrame:
def transform(self):
return self
def filter(self):
return self
def save(self):
pass
def process_data():
data = DataFrame()
# Chained call - should detect all methods
data.transform().filter().save()
"#;
let mut f = File::create(root.join("data_pipeline.py")).unwrap();
f.write_all(py_content.as_bytes()).unwrap();
let files = vec![root.join("data_pipeline.py")];
let index = FunctionIndex::build(&files).unwrap();
let graph = resolve_calls(&files, &index, root).unwrap();
for edge in &graph.edges {
eprintln!(
"Chained call edge: {} -> {} (line {})",
edge.caller.name, edge.callee.name, edge.call_line
);
}
let has_chained_call = graph.edges.iter().any(|e| {
e.caller.name == "process_data"
&& (e.callee.name == "save"
|| e.callee.name == "filter"
|| e.callee.name == "transform")
});
eprintln!(
"Found {} edges from process_data",
graph
.edges
.iter()
.filter(|e| e.caller.name == "process_data")
.count()
);
let has_any_call = graph
.edges
.iter()
.any(|e| e.caller.name == "process_data");
assert!(
has_any_call,
"Should detect at least one call from process_data"
);
}
#[test]
fn test_chained_call_target_extraction() {
let chain: MethodChain = smallvec![
"data".to_string(),
"transform".to_string(),
"filter".to_string(),
"save".to_string(),
];
let target = CallTarget::ChainedCall(chain.clone());
if let CallTarget::ChainedCall(extracted) = target {
assert_eq!(extracted.len(), 4);
assert_eq!(extracted[0], "data"); assert_eq!(extracted[1], "transform"); assert_eq!(extracted[2], "filter"); assert_eq!(extracted[3], "save"); } else {
panic!("Expected ChainedCall variant");
}
}
#[test]
fn test_chained_call_resolution_uses_last_method() {
let dir = TempDir::new().unwrap();
let root = dir.path();
let py_content = r#"
def save_data(data):
"""Final save method that the chain should resolve to"""
pass
def process():
# This simulates a chained call where only save_data is defined locally
# In real code, other_lib would be an external library
pass
"#;
let mut f = File::create(root.join("processor.py")).unwrap();
f.write_all(py_content.as_bytes()).unwrap();
let files = vec![root.join("processor.py")];
let index = FunctionIndex::build(&files).unwrap();
let context = FileContext {
file_path: "processor.py".to_string(),
language: "python".to_string(),
..Default::default()
};
let chain: MethodChain = smallvec![
"data".to_string(),
"transform".to_string(),
"filter".to_string(),
"save_data".to_string(), ];
let target = CallTarget::ChainedCall(chain);
let resolved = resolve_call_target(&target, &context, &index);
eprintln!("Resolved: {:?}", resolved);
}
#[test]
fn test_star_import_resolution() {
let dir = TempDir::new().unwrap();
let root = dir.path();
let config_content = r#"
def get_config():
return {}
def validate_config(cfg):
pass
"#;
let mut f = File::create(root.join("config.py")).unwrap();
f.write_all(config_content.as_bytes()).unwrap();
let main_content = r#"
from config import *
def main():
cfg = get_config()
validate_config(cfg)
"#;
let mut f = File::create(root.join("main.py")).unwrap();
f.write_all(main_content.as_bytes()).unwrap();
let files = vec![root.join("config.py"), root.join("main.py")];
let index = FunctionIndex::build(&files).unwrap();
let graph = resolve_calls(&files, &index, root).unwrap();
for edge in &graph.edges {
eprintln!(
"Edge: {} -> {} (line {})",
edge.caller.name, edge.callee.name, edge.call_line
);
}
let has_get_config = graph
.edges
.iter()
.any(|e| e.caller.name == "main" && e.callee.name == "get_config");
let has_validate_config = graph
.edges
.iter()
.any(|e| e.caller.name == "main" && e.callee.name == "validate_config");
assert!(
has_get_config || has_validate_config,
"Star import should allow function resolution. Edges: {:?}",
graph.edges.iter().map(|e| format!("{} -> {}", e.caller.name, e.callee.name)).collect::<Vec<_>>()
);
}
#[test]
fn test_build_import_map_star_import() {
let mut context = FileContext::default();
let dummy_file = Path::new("/project/main.py");
let dummy_root = Path::new("/project");
let import = ImportInfo {
module: "config".to_string(),
names: vec!["*".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 0,
line_number: 1,
visibility: None,
};
build_import_map(&[import], &mut context, dummy_file, dummy_root);
assert!(
context.star_imports.contains(&"config".to_string()),
"Module 'config' should be in star_imports"
);
assert!(
!context.from_imports.contains_key("*"),
"Star '*' should NOT be a key in from_imports"
);
assert!(
context.from_imports.is_empty(),
"from_imports should be empty for star imports"
);
}
#[test]
fn test_resolve_relative_import_same_package() {
let import = ImportInfo {
module: "".to_string(),
names: vec!["sibling".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 1,
line_number: 1,
visibility: None,
};
let current_file = Path::new("/project/pkg/main.py");
let project_root = Path::new("/project");
let resolved = resolve_relative_import(&import, current_file, project_root);
assert_eq!(resolved, Some("pkg".to_string()));
}
#[test]
fn test_resolve_relative_import_submodule() {
let import = ImportInfo {
module: "utils".to_string(),
names: vec!["helper".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 1,
line_number: 1,
visibility: None,
};
let current_file = Path::new("/project/pkg/main.py");
let project_root = Path::new("/project");
let resolved = resolve_relative_import(&import, current_file, project_root);
assert_eq!(resolved, Some("pkg.utils".to_string()));
}
#[test]
fn test_resolve_relative_import_parent_package() {
let import = ImportInfo {
module: "".to_string(),
names: vec!["utils".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 2,
line_number: 1,
visibility: None,
};
let current_file = Path::new("/project/pkg/subpkg/main.py");
let project_root = Path::new("/project");
let resolved = resolve_relative_import(&import, current_file, project_root);
assert_eq!(resolved, Some("pkg".to_string()));
}
#[test]
fn test_resolve_relative_import_sibling_package() {
let import = ImportInfo {
module: "sibling".to_string(),
names: vec!["func".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 2,
line_number: 1,
visibility: None,
};
let current_file = Path::new("/project/pkg/subpkg/main.py");
let project_root = Path::new("/project");
let resolved = resolve_relative_import(&import, current_file, project_root);
assert_eq!(resolved, Some("pkg.sibling".to_string()));
}
#[test]
fn test_resolve_relative_import_grandparent() {
let import = ImportInfo {
module: "other.module".to_string(),
names: vec!["func".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 3,
line_number: 1,
visibility: None,
};
let current_file = Path::new("/project/pkg/sub1/sub2/main.py");
let project_root = Path::new("/project");
let resolved = resolve_relative_import(&import, current_file, project_root);
assert_eq!(resolved, Some("pkg.other.module".to_string()));
}
#[test]
fn test_resolve_relative_import_beyond_root() {
let import = ImportInfo {
module: "".to_string(),
names: vec!["x".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 4,
line_number: 1,
visibility: None,
};
let current_file = Path::new("/project/pkg/main.py");
let project_root = Path::new("/project");
let resolved = resolve_relative_import(&import, current_file, project_root);
assert_eq!(resolved, None);
}
#[test]
fn test_resolve_absolute_import_passthrough() {
let import = ImportInfo {
module: "os.path".to_string(),
names: vec!["join".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 0,
line_number: 1,
visibility: None,
};
let current_file = Path::new("/project/pkg/main.py");
let project_root = Path::new("/project");
let resolved = resolve_relative_import(&import, current_file, project_root);
assert_eq!(resolved, Some("os.path".to_string()));
}
#[test]
fn test_build_import_map_relative_import_resolution() {
let mut context = FileContext::default();
let current_file = Path::new("/project/mypackage/submodule/handlers.py");
let project_root = Path::new("/project");
let import = ImportInfo {
module: "utils".to_string(),
names: vec!["helper".to_string()],
aliases: HashMap::new(),
is_from: true,
level: 2,
line_number: 1,
visibility: None,
};
build_import_map(&[import], &mut context, current_file, project_root);
assert!(context.from_imports.contains_key("helper"));
let (module, name) = context.from_imports.get("helper").unwrap();
assert_eq!(module, "mypackage.utils");
assert_eq!(name, "helper");
}
}