use std::collections::{HashMap, HashSet};
use crate::ir::{ExtractResult, ImportInfo};
use crate::model::{ConfidenceTier, Edge, EdgeType, Graph};
use crate::resolve::includes_graph::IncludesGraph;
use crate::resolve::ProjectSymbolTable;
const CONFIDENCE_EXACT: f32 = 0.95;
const CONFIDENCE_IMPORT: f32 = 0.90;
const CONFIDENCE_PROJECT: f32 = 0.80;
pub struct CallResolver<'a> {
symbol_table: &'a ProjectSymbolTable,
project: &'a str,
imports: HashMap<String, Vec<ImportInfo>>,
includes_graph: IncludesGraph,
}
impl<'a> CallResolver<'a> {
#[must_use]
pub fn new(symbol_table: &'a ProjectSymbolTable, project: &'a str) -> Self {
Self {
symbol_table,
project,
imports: HashMap::new(),
includes_graph: IncludesGraph::new(),
}
}
#[must_use]
pub fn with_imports(mut self, results: &[ExtractResult]) -> Self {
for result in results {
self.imports
.insert(result.file_path.clone(), result.imports.clone());
}
self
}
#[must_use]
pub fn with_includes_graph(mut self, graph: IncludesGraph) -> Self {
self.includes_graph = graph;
self
}
pub fn resolve_calls(&self, results: &[ExtractResult], graph: &mut Graph) -> Vec<Edge> {
let mut edges = Vec::new();
let mut seen_pairs: HashSet<(String, String)> = HashSet::new();
for result in results {
let caller_file = &result.file_path;
let imports = &result.imports;
for call in &result.calls {
let Some(caller_qn) = &call.caller_qn else {
continue;
};
let Some((callee_qn, confidence, tier)) =
self.resolve_call_internal(caller_file, &call.callee_name, imports)
else {
continue;
};
if is_builder_type_method(&callee_qn) {
continue;
}
let pair_key = (caller_qn.clone(), callee_qn.clone());
if !seen_pairs.insert(pair_key) {
continue;
}
let edge =
Edge::builder(caller_qn.clone(), callee_qn, EdgeType::Calls, self.project)
.confidence(confidence)
.confidence_tier(tier)
.start_line(call.line)
.build();
graph.add_edge(edge.clone());
edges.push(edge);
}
}
edges
}
#[must_use]
pub fn resolve_call(
&self,
caller_file: &str,
callee_name: &str,
) -> Option<(String, f32, ConfidenceTier)> {
let imports = self
.imports
.get(caller_file)
.map(Vec::as_slice)
.unwrap_or(&[]);
self.resolve_call_internal(caller_file, callee_name, imports)
}
fn resolve_call_internal(
&self,
caller_file: &str,
callee_name: &str,
imports: &[ImportInfo],
) -> Option<(String, f32, ConfidenceTier)> {
if let Some(entry) = self
.symbol_table
.lookup_in_file(caller_file, callee_name)
.first()
{
return Some((entry.qn.clone(), CONFIDENCE_EXACT, ConfidenceTier::SameFile));
}
let is_imported = imports
.iter()
.any(|imp| imp.imported_names.iter().any(|n| n == callee_name));
if is_imported {
if let Some(entry) = self.symbol_table.lookup(callee_name).first() {
return Some((
entry.qn.clone(),
CONFIDENCE_IMPORT,
ConfidenceTier::ImportScoped,
));
}
}
let exported = if self.includes_graph.has_outgoing_edges(caller_file) {
self.symbol_table.lookup_exported_in_scope(
callee_name,
caller_file,
&self.includes_graph,
)
} else {
self.symbol_table.lookup_exported(callee_name)
};
if let Some(entry) = exported.first() {
return Some((entry.qn.clone(), CONFIDENCE_PROJECT, ConfidenceTier::Global));
}
None
}
}
fn is_builder_type_method(callee_qn: &str) -> bool {
callee_qn
.rfind('#')
.map(|idx| {
let disambiguator = &callee_qn[idx + 1..];
if disambiguator.ends_with("Builder") {
return true;
}
let method_name = callee_qn[..idx].rsplit('.').next().unwrap_or("");
method_name == "builder"
})
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{AssignInfo, CallInfo};
use crate::model::{Language, Node, NodeLabel};
use crate::resolve::{build_symbol_table, FqnGenerator};
fn fqn(project: &str, file: &str, name: &str, language: Language) -> String {
FqnGenerator::generate(project, file, name, language, None)
}
fn make_node(name: &str, file: &str, project: &str, label: NodeLabel) -> Node {
let qn = fqn(project, file, name, Language::Rust);
Node::builder(label, name, qn)
.file_path(file)
.project(project)
.language(Language::Rust)
.build()
}
fn make_exported_node(name: &str, file: &str, project: &str, label: NodeLabel) -> Node {
let qn = fqn(project, file, name, Language::Rust);
Node::builder(label, name, qn)
.file_path(file)
.project(project)
.language(Language::Rust)
.is_exported(true)
.build()
}
fn make_result(file: &str, nodes: Vec<Node>) -> ExtractResult {
let mut result = ExtractResult::new(file, Language::Rust);
result.nodes = nodes;
result
}
fn add_nodes_to_graph(graph: &mut Graph, results: &[ExtractResult], project: &str) {
for result in results {
for node in &result.nodes {
let qn = fqn(project, &result.file_path, &node.name, Language::Rust);
let mut graph_node = node.clone();
graph_node.id = qn.clone();
graph_node.qualified_name = qn;
graph.add_node(graph_node);
}
}
}
#[test]
fn resolve_call_finds_function_in_same_file() {
let node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let result = make_result("a.rs", vec![node]);
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("a.rs", "foo");
assert!(resolved.is_some());
let (qn, confidence, tier) = resolved.unwrap();
assert_eq!(qn, "proj.a.rs.foo");
assert!((confidence - 0.95).abs() < 1e-6);
assert_eq!(tier, ConfidenceTier::SameFile);
}
#[test]
fn resolve_call_file_level_returns_correct_qn_for_nested_path() {
let node = make_node("bar", "src/deep/file.rs", "proj", NodeLabel::Function);
let result = make_result("src/deep/file.rs", vec![node]);
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("src/deep/file.rs", "bar");
assert!(resolved.is_some());
let (qn, _, tier) = resolved.unwrap();
assert_eq!(qn, "proj.src.deep.file.rs.bar");
assert_eq!(tier, ConfidenceTier::SameFile);
}
#[test]
fn resolve_call_finds_function_via_import() {
let bar_node = make_exported_node("bar", "b.rs", "proj", NodeLabel::Function);
let bar_result = make_result("b.rs", vec![bar_node]);
let mut a_result = make_result("a.rs", vec![]);
a_result.imports.push(ImportInfo {
source_file: "b.rs".to_string(),
imported_names: vec!["bar".to_string()],
line: 1,
});
let results = vec![bar_result, a_result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj").with_imports(&results);
let resolved = resolver.resolve_call("a.rs", "bar");
assert!(resolved.is_some());
let (qn, confidence, tier) = resolved.unwrap();
assert_eq!(qn, "proj.b.rs.bar");
assert!((confidence - 0.90).abs() < 1e-6);
assert_eq!(tier, ConfidenceTier::ImportScoped);
}
#[test]
fn resolve_call_import_takes_precedence_over_project_export() {
let bar_node = make_exported_node("bar", "b.rs", "proj", NodeLabel::Function);
let bar_result = make_result("b.rs", vec![bar_node]);
let mut a_result = make_result("a.rs", vec![]);
a_result.imports.push(ImportInfo {
source_file: "b.rs".to_string(),
imported_names: vec!["bar".to_string()],
line: 1,
});
let results = vec![bar_result, a_result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj").with_imports(&results);
let resolved = resolver.resolve_call("a.rs", "bar").unwrap();
assert!((resolved.1 - 0.90).abs() < 1e-6);
assert_eq!(resolved.2, ConfidenceTier::ImportScoped);
}
#[test]
fn resolve_call_without_imports_registered_uses_project_lookup() {
let bar_node = make_exported_node("bar", "b.rs", "proj", NodeLabel::Function);
let bar_result = make_result("b.rs", vec![bar_node]);
let a_result = make_result("a.rs", vec![]);
let results = vec![bar_result, a_result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("a.rs", "bar");
assert!(resolved.is_some());
let (qn, confidence, tier) = resolved.unwrap();
assert_eq!(qn, "proj.b.rs.bar");
assert!((confidence - 0.80).abs() < 1e-6);
assert_eq!(tier, ConfidenceTier::Global);
}
#[test]
fn resolve_call_finds_exported_function_in_project() {
let bar_node = make_exported_node("bar", "b.rs", "proj", NodeLabel::Function);
let bar_result = make_result("b.rs", vec![bar_node]);
let a_result = make_result("a.rs", vec![]);
let results = vec![bar_result, a_result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("a.rs", "bar");
assert!(resolved.is_some());
let (qn, confidence, tier) = resolved.unwrap();
assert_eq!(qn, "proj.b.rs.bar");
assert!((confidence - 0.80).abs() < 1e-6);
assert_eq!(tier, ConfidenceTier::Global);
}
#[test]
fn resolve_call_project_level_skips_non_exported() {
let bar_node = make_node("bar", "b.rs", "proj", NodeLabel::Function);
let bar_result = make_result("b.rs", vec![bar_node]);
let a_result = make_result("a.rs", vec![]);
let results = vec![bar_result, a_result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("a.rs", "bar");
assert!(resolved.is_none());
}
#[test]
fn resolve_call_cpp_scoped() {
use crate::resolve::symbol_table::SymbolEntry;
let mut table = ProjectSymbolTable::new();
table.add_symbol(
SymbolEntry::new(
"foo",
"proj.foo_h.foo",
NodeLabel::Function,
"/abs/foo.h",
"proj",
)
.with_exported(true),
);
table.add_symbol(
SymbolEntry::new(
"foo",
"proj.bar_cpp.foo",
NodeLabel::Function,
"/abs/bar.cpp",
"proj",
)
.with_exported(true),
);
let mut graph = IncludesGraph::new();
graph.add_include("/abs/main.cpp", "/abs/foo.h");
let resolver = CallResolver::new(&table, "proj").with_includes_graph(graph);
let resolved = resolver.resolve_call("/abs/main.cpp", "foo");
assert!(resolved.is_some());
let (qn, confidence, tier) = resolved.unwrap();
assert_eq!(
qn, "proj.foo_h.foo",
"should resolve to foo.h's foo, not bar.cpp's"
);
assert!((confidence - 0.80).abs() < 1e-6);
assert_eq!(tier, ConfidenceTier::Global);
}
#[test]
fn resolve_call_non_cpp_uses_global_when_no_includes() {
use crate::resolve::symbol_table::SymbolEntry;
let mut table = ProjectSymbolTable::new();
table.add_symbol(
SymbolEntry::new(
"bar",
"proj.b_rs.bar",
NodeLabel::Function,
"/abs/b.rs",
"proj",
)
.with_exported(true),
);
let graph = IncludesGraph::new();
let resolver = CallResolver::new(&table, "proj").with_includes_graph(graph);
let resolved = resolver.resolve_call("/abs/a.rs", "bar");
assert!(resolved.is_some());
let (qn, _, _) = resolved.unwrap();
assert_eq!(
qn, "proj.b_rs.bar",
"non-C++ file should use global lookup (backward compat)"
);
}
#[test]
fn resolve_call_cpp_scoped_excludes_unreachable() {
use crate::resolve::symbol_table::SymbolEntry;
let mut table = ProjectSymbolTable::new();
table.add_symbol(
SymbolEntry::new(
"foo",
"proj.bar_cpp.foo",
NodeLabel::Function,
"/abs/bar.cpp",
"proj",
)
.with_exported(true),
);
let mut graph = IncludesGraph::new();
graph.add_include("/abs/main.cpp", "/abs/foo.h");
let resolver = CallResolver::new(&table, "proj").with_includes_graph(graph);
let resolved = resolver.resolve_call("/abs/main.cpp", "foo");
assert!(resolved.is_none(), "foo in bar.cpp should not be in scope");
}
#[test]
fn resolve_call_cpp_scoped_transitive_include() {
use crate::resolve::symbol_table::SymbolEntry;
let mut table = ProjectSymbolTable::new();
table.add_symbol(
SymbolEntry::new(
"foo",
"proj.b_h.foo",
NodeLabel::Function,
"/abs/b.h",
"proj",
)
.with_exported(true),
);
let mut graph = IncludesGraph::new();
graph.add_include("/abs/main.cpp", "/abs/a.h");
graph.add_include("/abs/a.h", "/abs/b.h");
let resolver = CallResolver::new(&table, "proj").with_includes_graph(graph);
let resolved = resolver.resolve_call("/abs/main.cpp", "foo");
assert!(resolved.is_some());
let (qn, _, _) = resolved.unwrap();
assert_eq!(qn, "proj.b_h.foo", "transitive include should reach b.h");
}
#[test]
fn resolve_call_returns_none_for_unknown_function() {
let a_result = make_result("a.rs", vec![]);
let results = vec![a_result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("a.rs", "nonexistent");
assert!(resolved.is_none());
}
#[test]
fn resolve_call_returns_none_for_empty_callee_name() {
let node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let result = make_result("a.rs", vec![node]);
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("a.rs", "");
assert!(resolved.is_none());
}
#[test]
fn resolve_call_returns_none_for_unknown_file() {
let node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let result = make_result("a.rs", vec![node]);
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("nonexistent.rs", "foo");
assert!(resolved.is_none());
}
#[test]
fn resolve_calls_creates_calls_edges_for_all_resolvable_calls() {
let foo_node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let bar_node = make_node("bar", "a.rs", "proj", NodeLabel::Function);
let mut result = make_result("a.rs", vec![foo_node, bar_node]);
let foo_qn = fqn("proj", "a.rs", "foo", Language::Rust);
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "bar".to_string(),
line: 5,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert_eq!(edges.len(), 1);
let edge = &edges[0];
assert_eq!(edge.source, foo_qn);
assert_eq!(edge.target, "proj.a.rs.bar");
assert_eq!(edge.edge_type, EdgeType::Calls);
assert!((edge.confidence - 0.95).abs() < 1e-6);
assert_eq!(edge.confidence_tier, ConfidenceTier::SameFile);
assert_eq!(edge.start_line, Some(5));
assert_eq!(graph.edge_count(), 1);
}
#[test]
fn resolve_calls_skips_calls_without_caller_qn() {
let foo_node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let bar_node = make_node("bar", "a.rs", "proj", NodeLabel::Function);
let mut result = make_result("a.rs", vec![foo_node, bar_node]);
result.calls.push(CallInfo {
caller_qn: None,
callee_name: "bar".to_string(),
line: 5,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert!(edges.is_empty());
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn resolve_calls_skips_unresolvable_callees() {
let foo_node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let foo_qn = fqn("proj", "a.rs", "foo", Language::Rust);
let mut result = make_result("a.rs", vec![foo_node]);
result.calls.push(CallInfo {
caller_qn: Some(foo_qn),
callee_name: "nonexistent".to_string(),
line: 5,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert!(edges.is_empty());
}
#[test]
fn resolve_calls_handles_multiple_results() {
let a_node = make_node("func_a", "a.rs", "proj", NodeLabel::Function);
let b_node = make_exported_node("func_b", "b.rs", "proj", NodeLabel::Function);
let a_qn = fqn("proj", "a.rs", "func_a", Language::Rust);
let mut a_result = make_result("a.rs", vec![a_node]);
a_result.calls.push(CallInfo {
caller_qn: Some(a_qn.clone()),
callee_name: "func_b".to_string(),
line: 3,
args: vec![],
});
let b_result = make_result("b.rs", vec![b_node]);
let results = vec![a_result, b_result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].source, a_qn);
assert_eq!(edges[0].target, "proj.b.rs.func_b");
assert!((edges[0].confidence - 0.80).abs() < 1e-6);
assert_eq!(edges[0].confidence_tier, ConfidenceTier::Global);
}
#[test]
fn resolve_calls_creates_multiple_edges_for_multiple_calls() {
let foo_node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let bar_node = make_node("bar", "a.rs", "proj", NodeLabel::Function);
let baz_node = make_node("baz", "a.rs", "proj", NodeLabel::Function);
let foo_qn = fqn("proj", "a.rs", "foo", Language::Rust);
let mut result = make_result("a.rs", vec![foo_node, bar_node, baz_node]);
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "bar".to_string(),
line: 3,
args: vec![],
});
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "baz".to_string(),
line: 4,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert_eq!(edges.len(), 2);
assert_eq!(graph.edge_count(), 2);
}
#[test]
fn resolve_calls_deduplicates_same_callee_pair() {
let foo_node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let bar_node = make_node("bar", "a.rs", "proj", NodeLabel::Function);
let foo_qn = fqn("proj", "a.rs", "foo", Language::Rust);
let mut result = make_result("a.rs", vec![foo_node, bar_node]);
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "bar".to_string(),
line: 3,
args: vec![],
});
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "bar".to_string(),
line: 7,
args: vec![],
});
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "bar".to_string(),
line: 12,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert_eq!(edges.len(), 1);
assert_eq!(graph.edge_count(), 1);
assert_eq!(edges[0].start_line, Some(3));
}
#[test]
fn resolve_calls_empty_results_returns_empty() {
let table = ProjectSymbolTable::new();
let mut graph = Graph::new();
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&[], &mut graph);
assert!(edges.is_empty());
}
#[test]
fn resolve_calls_adds_edges_to_graph() {
let foo_node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let bar_node = make_node("bar", "a.rs", "proj", NodeLabel::Function);
let foo_qn = fqn("proj", "a.rs", "foo", Language::Rust);
let bar_qn = fqn("proj", "a.rs", "bar", Language::Rust);
let mut result = make_result("a.rs", vec![foo_node, bar_node]);
result.calls.push(CallInfo {
caller_qn: Some(foo_qn.clone()),
callee_name: "bar".to_string(),
line: 5,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
resolver.resolve_calls(&results, &mut graph);
let neighbors = graph.neighbors(&foo_qn, Some(EdgeType::Calls));
assert_eq!(neighbors.len(), 1);
assert_eq!(neighbors[0].id, bar_qn);
}
#[test]
fn ac_trace_001_call_path_a_to_b() {
let a_node = make_node("A", "a.rs", "proj", NodeLabel::Function);
let a_qn = fqn("proj", "a.rs", "A", Language::Rust);
let b_qn = fqn("proj", "b.rs", "B", Language::Rust);
let mut a_result = make_result("a.rs", vec![a_node]);
a_result.calls.push(CallInfo {
caller_qn: Some(a_qn.clone()),
callee_name: "B".to_string(),
line: 5,
args: vec![],
});
let b_result = make_result(
"b.rs",
vec![make_exported_node("B", "b.rs", "proj", NodeLabel::Function)],
);
let results = vec![a_result, b_result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
resolver.resolve_calls(&results, &mut graph);
let neighbors = graph.neighbors(&a_qn, Some(EdgeType::Calls));
assert_eq!(
neighbors.len(),
1,
"A should have exactly one CALLS neighbor"
);
assert_eq!(neighbors[0].id, b_qn, "A's CALLS neighbor should be B");
assert_eq!(neighbors[0].name, "B");
}
#[test]
fn with_imports_is_chainable() {
let table = ProjectSymbolTable::new();
let result = ExtractResult::new("a.rs", Language::Rust);
let resolver = CallResolver::new(&table, "proj").with_imports(&[result]);
assert!(resolver.imports.contains_key("a.rs"));
}
#[test]
fn with_imports_empty_results_is_noop() {
let table = ProjectSymbolTable::new();
let resolver = CallResolver::new(&table, "proj").with_imports(&[]);
assert!(resolver.imports.is_empty());
}
#[test]
fn resolve_calls_ignores_assignments() {
let foo_node = make_node("foo", "a.rs", "proj", NodeLabel::Function);
let mut result = make_result("a.rs", vec![foo_node]);
result.assignments.push(AssignInfo {
target_name: "x".to_string(),
source_name: "foo".to_string(),
line: 5,
is_return_assign: true,
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert!(
edges.is_empty(),
"CallResolver should not process assignments"
);
}
#[test]
fn resolve_call_import_lookup_falls_through_when_symbol_not_in_table() {
let mut a_result = make_result("a.rs", vec![]);
a_result.imports.push(ImportInfo {
source_file: "b.rs".to_string(),
imported_names: vec!["bar".to_string()],
line: 1,
});
let results = vec![a_result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj").with_imports(&results);
let resolved = resolver.resolve_call("a.rs", "bar");
assert!(
resolved.is_none(),
"imported but non-existent symbol → None"
);
}
#[test]
fn resolve_call_with_unregistered_file_falls_through_to_project_lookup() {
let bar_node = make_exported_node("bar", "b.rs", "proj", NodeLabel::Function);
let bar_result = make_result("b.rs", vec![bar_node]);
let results = vec![bar_result];
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj");
let resolved = resolver.resolve_call("a.rs", "bar");
assert!(resolved.is_some(), "should find via project-level export");
let (qn, confidence, tier) = resolved.unwrap();
assert_eq!(qn, "proj.b.rs.bar");
assert!((confidence - 0.80).abs() < 1e-6);
assert_eq!(tier, ConfidenceTier::Global);
}
fn make_node_with_disambiguator(
name: &str,
file: &str,
project: &str,
label: NodeLabel,
disambiguator: Option<&str>,
) -> Node {
let qn = FqnGenerator::generate(project, file, name, Language::Rust, disambiguator);
Node::builder(label, name, qn)
.file_path(file)
.project(project)
.language(Language::Rust)
.build()
}
#[test]
fn c2_resolve_calls_filters_builder_type_methods() {
let build_method = make_node_with_disambiguator(
"build",
"a.rs",
"proj",
NodeLabel::Function,
Some("NodeBuilder"),
);
let language_method = make_node_with_disambiguator(
"language",
"a.rs",
"proj",
NodeLabel::Function,
Some("NodeBuilder"),
);
let caller_node = make_node("caller", "a.rs", "proj", NodeLabel::Function);
let caller_qn = fqn("proj", "a.rs", "caller", Language::Rust);
let mut result = make_result("a.rs", vec![build_method, language_method, caller_node]);
result.calls.push(CallInfo {
caller_qn: Some(caller_qn.clone()),
callee_name: "build".to_string(),
line: 5,
args: vec![],
});
result.calls.push(CallInfo {
caller_qn: Some(caller_qn.clone()),
callee_name: "language".to_string(),
line: 6,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert!(
edges.is_empty(),
"Builder type method calls should be filtered: {edges:?}"
);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn c2_resolve_calls_preserves_non_builder_type_methods() {
let save_method = make_node_with_disambiguator(
"save_nodes",
"a.rs",
"proj",
NodeLabel::Function,
Some("Repo"),
);
let caller_node = make_node("caller", "a.rs", "proj", NodeLabel::Function);
let caller_qn = fqn("proj", "a.rs", "caller", Language::Rust);
let save_qn = save_method.qualified_name.clone();
let mut result = make_result("a.rs", vec![save_method, caller_node]);
result.calls.push(CallInfo {
caller_qn: Some(caller_qn.clone()),
callee_name: "save_nodes".to_string(),
line: 5,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert_eq!(
edges.len(),
1,
"non-Builder method call should be preserved"
);
assert_eq!(edges[0].source, caller_qn);
assert_eq!(edges[0].target, save_qn);
}
#[test]
fn c2_resolve_calls_filters_builder_entry_method() {
let builder_method = make_node_with_disambiguator(
"builder",
"a.rs",
"proj",
NodeLabel::Function,
Some("Edge"),
);
let caller_node = make_node("caller", "a.rs", "proj", NodeLabel::Function);
let caller_qn = fqn("proj", "a.rs", "caller", Language::Rust);
let mut result = make_result("a.rs", vec![builder_method, caller_node]);
result.calls.push(CallInfo {
caller_qn: Some(caller_qn.clone()),
callee_name: "builder".to_string(),
line: 5,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert!(
edges.is_empty(),
"Builder entry method calls should be filtered: {edges:?}"
);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn is_builder_type_method_no_hash_returns_false() {
assert!(!is_builder_type_method("proj.a.rs.foo"));
assert!(!is_builder_type_method("simple_name"));
}
#[test]
fn is_builder_type_method_non_builder_disambiguator_returns_false() {
assert!(!is_builder_type_method("proj.a.rs.save_nodes#Repo"));
assert!(!is_builder_type_method("proj.a.rs.method#MyType"));
}
#[test]
fn with_includes_graph_is_chainable() {
let table = ProjectSymbolTable::new();
let graph = IncludesGraph::new();
let resolver = CallResolver::new(&table, "proj").with_includes_graph(graph);
assert!(resolver.includes_graph.is_empty());
}
#[test]
fn resolve_calls_batch_with_includes_graph_uses_scope_aware_lookup() {
use crate::resolve::symbol_table::SymbolEntry;
let mut table = ProjectSymbolTable::new();
table.add_symbol(
SymbolEntry::new(
"foo",
"proj.foo_h.foo",
NodeLabel::Function,
"/abs/foo.h",
"proj",
)
.with_exported(true),
);
table.add_symbol(
SymbolEntry::new(
"foo",
"proj.bar_cpp.foo",
NodeLabel::Function,
"/abs/bar.cpp",
"proj",
)
.with_exported(true),
);
let mut includes = IncludesGraph::new();
includes.add_include("/abs/main.cpp", "/abs/foo.h");
let mut result = ExtractResult::new("/abs/main.cpp", Language::Rust);
result.calls.push(CallInfo {
caller_qn: Some("proj.main_cpp.caller".to_string()),
callee_name: "foo".to_string(),
line: 5,
args: vec![],
});
let results = vec![result];
let mut graph = Graph::new();
graph.add_node(
Node::builder(NodeLabel::Function, "caller", "proj.main_cpp.caller")
.id("proj.main_cpp.caller")
.build(),
);
let resolver = CallResolver::new(&table, "proj").with_includes_graph(includes);
let edges = resolver.resolve_calls(&results, &mut graph);
assert_eq!(edges.len(), 1);
assert_eq!(
edges[0].target, "proj.foo_h.foo",
"batch resolution should use scope-aware lookup"
);
}
#[test]
fn resolve_calls_uses_import_lookup_in_batch_mode() {
let bar_node = make_exported_node("bar", "b.rs", "proj", NodeLabel::Function);
let bar_qn = fqn("proj", "b.rs", "bar", Language::Rust);
let a_node = make_node("caller", "a.rs", "proj", NodeLabel::Function);
let a_qn = fqn("proj", "a.rs", "caller", Language::Rust);
let mut a_result = make_result("a.rs", vec![a_node]);
a_result.imports.push(ImportInfo {
source_file: "b.rs".to_string(),
imported_names: vec!["bar".to_string()],
line: 1,
});
a_result.calls.push(CallInfo {
caller_qn: Some(a_qn.clone()),
callee_name: "bar".to_string(),
line: 5,
args: vec![],
});
let b_result = make_result("b.rs", vec![bar_node]);
let results = vec![a_result, b_result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].source, a_qn);
assert_eq!(edges[0].target, bar_qn);
assert!(
(edges[0].confidence - 0.90).abs() < 1e-6,
"import-based lookup should give 0.90"
);
assert_eq!(edges[0].confidence_tier, ConfidenceTier::ImportScoped);
}
#[test]
fn is_builder_type_method_builder_name_returns_true() {
assert!(is_builder_type_method("proj.a.rs.builder#Edge"));
assert!(is_builder_type_method("proj.a.rs.builder#Node"));
}
#[test]
fn is_builder_type_method_builder_disambiguator_returns_true() {
assert!(is_builder_type_method("proj.a.rs.build#NodeBuilder"));
assert!(is_builder_type_method("proj.a.rs.language#EdgeBuilder"));
}
#[test]
fn resolve_calls_mixed_builder_and_non_builder_produces_only_non_builder() {
let build_method = make_node_with_disambiguator(
"build",
"a.rs",
"proj",
NodeLabel::Function,
Some("NodeBuilder"),
);
let save_method = make_node_with_disambiguator(
"save_nodes",
"a.rs",
"proj",
NodeLabel::Function,
Some("Repo"),
);
let caller_node = make_node("caller", "a.rs", "proj", NodeLabel::Function);
let caller_qn = fqn("proj", "a.rs", "caller", Language::Rust);
let save_qn = save_method.qualified_name.clone();
let mut result = make_result("a.rs", vec![build_method, save_method, caller_node]);
result.calls.push(CallInfo {
caller_qn: Some(caller_qn.clone()),
callee_name: "build".to_string(),
line: 5,
args: vec![],
});
result.calls.push(CallInfo {
caller_qn: Some(caller_qn.clone()),
callee_name: "save_nodes".to_string(),
line: 6,
args: vec![],
});
let results = vec![result];
let table = build_symbol_table(&results, "proj");
let mut graph = Graph::new();
add_nodes_to_graph(&mut graph, &results, "proj");
let resolver = CallResolver::new(&table, "proj");
let edges = resolver.resolve_calls(&results, &mut graph);
assert_eq!(
edges.len(),
1,
"only non-Builder method call should produce edge"
);
assert_eq!(edges[0].source, caller_qn);
assert_eq!(edges[0].target, save_qn);
assert_eq!(edges[0].start_line, Some(6));
}
}