use std::collections::HashMap;
use crate::lang::call_target::parse_call_target;
use crate::types::{KgEdgeKind, KgGraph, KgNode};
pub fn resolve_calls(graph: KgGraph) -> KgGraph {
let KgGraph { nodes, mut edges } = graph;
let mut name_index: HashMap<(&str, &str), Vec<&KgNode>> = HashMap::new();
for node in &nodes {
name_index
.entry((node.language.as_str(), node.name.as_str()))
.or_default()
.push(node);
}
for edge in edges.iter_mut() {
if !matches!(edge.kind, KgEdgeKind::Calls) {
continue;
}
let Some(unresolved) = parse_call_target(&edge.to) else {
continue;
};
let caller_file: Option<&str> = {
let parts: Vec<&str> = edge.from.splitn(4, ':').collect();
if parts.len() >= 3 {
Some(parts[2])
} else {
None
}
};
let candidates = name_index
.get(&(unresolved.lang, unresolved.callee))
.map(|v| v.as_slice())
.unwrap_or(&[]);
let resolved = match candidates {
[] => None,
[single] => Some(single.id.as_str()),
many => {
caller_file.and_then(|file| {
many.iter().find(|n| n.file == file).map(|n| n.id.as_str())
})
}
};
if let Some(id) = resolved {
edge.to = id.to_string();
}
}
edges.retain(|e| e.from != e.to);
KgGraph { nodes, edges }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::call_target::build_call_target;
use crate::types::{KgEdge, KgEdgeKind, KgGraph, KgNode, KgNodeKind};
fn mk(id: &str, name: &str, kind: KgNodeKind, lang: &str, file: &str) -> KgNode {
KgNode {
id: id.into(),
kind,
name: name.into(),
qualified_name: name.into(),
language: lang.into(),
file: file.into(),
start_line: 1,
end_line: 10,
doc_comment: None,
is_public: true,
extra: serde_json::Value::Null,
}
}
fn cedge(from: &str, to: &str) -> KgEdge {
KgEdge {
from: from.into(),
to: to.into(),
kind: KgEdgeKind::Calls,
weight: 1.0,
}
}
#[test]
fn node_id_and_target_share_name_component() {
let node_id = "rust:Function:src/foo.rs:helper";
let target = build_call_target("rust", "Function", "helper");
assert_eq!(
node_id.splitn(4, ':').nth(3),
target.splitn(4, ':').nth(3),
"name component must match between node id and call target"
);
}
#[test]
fn resolve_calls_intra_file() {
let caller_id = "rust:Function:foo.rs:caller";
let helper_id = "rust:Function:foo.rs:helper";
let mut g = KgGraph::default();
g.nodes.push(mk(
caller_id,
"caller",
KgNodeKind::Function,
"rust",
"foo.rs",
));
g.nodes.push(mk(
helper_id,
"helper",
KgNodeKind::Function,
"rust",
"foo.rs",
));
g.edges.push(cedge(
caller_id,
&build_call_target("rust", "Function", "helper"),
));
let out = resolve_calls(g);
let call = out
.edges
.iter()
.find(|e| matches!(e.kind, KgEdgeKind::Calls))
.unwrap();
assert_eq!(call.to, helper_id, "intra-file call must resolve");
}
#[test]
fn resolve_calls_cross_file() {
let caller_id = "rust:Function:src/a.rs:do_work";
let callee_id = "rust:Function:src/b.rs:utility";
let mut g = KgGraph::default();
g.nodes.push(mk(
caller_id,
"do_work",
KgNodeKind::Function,
"rust",
"src/a.rs",
));
g.nodes.push(mk(
callee_id,
"utility",
KgNodeKind::Function,
"rust",
"src/b.rs",
));
g.edges.push(cedge(
caller_id,
&build_call_target("rust", "Function", "utility"),
));
let out = resolve_calls(g);
let call = out
.edges
.iter()
.find(|e| matches!(e.kind, KgEdgeKind::Calls))
.unwrap();
assert_eq!(call.to, callee_id, "cross-file unique callee must resolve");
}
#[test]
fn resolve_calls_class_method_qualified() {
let caller_id = "csharp:Method:OrderService.cs:OrderService:Process";
let callee_id = "csharp:Method:Repository.cs:Repository:Save";
let mut g = KgGraph::default();
g.nodes.push(mk(
caller_id,
"Process",
KgNodeKind::Method,
"csharp",
"OrderService.cs",
));
g.nodes.push(mk(
callee_id,
"Save",
KgNodeKind::Method,
"csharp",
"Repository.cs",
));
g.edges.push(cedge(
caller_id,
&build_call_target("csharp", "Method", "Save"),
));
let out = resolve_calls(g);
let call = out
.edges
.iter()
.find(|e| matches!(e.kind, KgEdgeKind::Calls))
.unwrap();
assert_eq!(
call.to, callee_id,
"class-method call must resolve to qualified node id"
);
}
#[test]
fn resolve_calls_external_stays_unresolved() {
let caller_id = "rust:Function:src/main.rs:main";
let ext = build_call_target("rust", "Function", "println");
let mut g = KgGraph::default();
g.nodes.push(mk(
caller_id,
"main",
KgNodeKind::Function,
"rust",
"src/main.rs",
));
g.edges.push(cedge(caller_id, &ext));
let out = resolve_calls(g);
let call = out
.edges
.iter()
.find(|e| matches!(e.kind, KgEdgeKind::Calls))
.unwrap();
assert_eq!(call.to, ext, "external call must keep sentinel target");
}
#[test]
fn resolve_calls_high_resolution_rate() {
let alpha_id = "rust:Function:a.rs:alpha";
let beta_id = "rust:Function:a.rs:beta";
let gamma_id = "rust:Function:b.rs:gamma";
let mut g = KgGraph::default();
g.nodes
.push(mk(alpha_id, "alpha", KgNodeKind::Function, "rust", "a.rs"));
g.nodes
.push(mk(beta_id, "beta", KgNodeKind::Function, "rust", "a.rs"));
g.nodes
.push(mk(gamma_id, "gamma", KgNodeKind::Function, "rust", "b.rs"));
for (from, to) in [
(alpha_id, "beta"),
(alpha_id, "gamma"),
(alpha_id, "delta"),
(beta_id, "gamma"),
] {
g.edges
.push(cedge(from, &build_call_target("rust", "Function", to)));
}
let out = resolve_calls(g);
let calls: Vec<_> = out
.edges
.iter()
.filter(|e| matches!(e.kind, KgEdgeKind::Calls))
.collect();
let ids: std::collections::HashSet<_> = out.nodes.iter().map(|n| n.id.as_str()).collect();
let resolved = calls.iter().filter(|e| ids.contains(e.to.as_str())).count();
assert_eq!(calls.len(), 4, "should have 4 call edges");
assert_eq!(resolved, 3, "3/4 calls must resolve (delta is external)");
}
}