use std::collections::HashMap;
use petgraph::visit::{EdgeRef, IntoEdgeReferences};
use crate::model::{EdgeKind, KnowledgeGraph, NodeId};
pub struct CallGraph<'a> {
graph: &'a KnowledgeGraph,
}
pub type CallIndex = HashMap<String, (Vec<String>, Vec<String>)>;
impl<'a> CallGraph<'a> {
pub fn new(graph: &'a KnowledgeGraph) -> Self {
Self { graph }
}
pub fn callee_of(&self, name: &str) -> Vec<NodeId> {
let mut callees = Vec::new();
for n in self.graph.graph.node_indices() {
if let Some(w) = self.graph.graph.node_weight(n) && w.name == name {
for e in self.graph.graph.edges(n) {
if e.weight().kind == EdgeKind::Calls {
callees.push(e.target());
}
}
}
}
callees
}
pub fn caller_of(&self, name: &str) -> Vec<NodeId> {
let mut callers = Vec::new();
for n in self.graph.graph.node_indices() {
if let Some(w) = self.graph.graph.node_weight(n) && w.name == name {
for e in self.graph.graph.edges_directed(n, petgraph::Direction::Incoming) {
if e.weight().kind == EdgeKind::Calls {
callers.push(e.source());
}
}
}
}
callers
}
pub fn all_call_edges(&self) -> Vec<(NodeId, NodeId)> {
let mut edges = Vec::new();
for e in self.graph.graph.edge_references() {
if e.weight().kind == EdgeKind::Calls {
edges.push((e.source(), e.target()));
}
}
edges
}
pub fn build_call_index(&self) -> CallIndex {
let mut index: HashMap<String, (Vec<String>, Vec<String>)> = HashMap::new();
for (src, dst) in self.all_call_edges() {
if let (Some(s), Some(d)) = (self.graph.graph.node_weight(src), self.graph.graph.node_weight(dst)) {
index.entry(d.name.clone()).or_default().0.push(s.name.clone());
index.entry(s.name.clone()).or_default().1.push(d.name.clone());
}
}
index
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{CodeNode, CodeEdge, NodeKind};
use petgraph::stable_graph::StableDiGraph;
fn make_test_graph() -> KnowledgeGraph {
let mut g = StableDiGraph::<CodeNode, CodeEdge>::new();
let caller = g.add_node(CodeNode {
id: NodeId::new(0), kind: NodeKind::Function,
name: "caller".into(), file_path: None,
line_range: None, doc_comment: None, signature: None, visibility: None,
module_path: vec!["test".into()],
});
let callee = g.add_node(CodeNode {
id: NodeId::new(1), kind: NodeKind::Function,
name: "callee".into(), file_path: None,
line_range: None, doc_comment: None, signature: None, visibility: None,
module_path: vec!["test".into()],
});
g.add_edge(caller, callee, CodeEdge {
id: petgraph::stable_graph::EdgeIndex::new(0),
kind: EdgeKind::Calls, source: caller, target: callee,
weight: 1.0, location: None,
});
KnowledgeGraph { graph: g, modules: vec![], features: Vec::new() }
}
#[test]
fn test_callee_of() {
let kg = make_test_graph();
let cg = CallGraph::new(&kg);
let callees = cg.callee_of("caller");
assert_eq!(callees.len(), 1);
}
#[test]
fn test_caller_of() {
let kg = make_test_graph();
let cg = CallGraph::new(&kg);
let callers = cg.caller_of("callee");
assert_eq!(callers.len(), 1);
}
#[test]
fn test_call_index_serde_round_trip() {
let kg = make_test_graph();
let cg = CallGraph::new(&kg);
let index = cg.build_call_index();
let json = serde_json::to_string(&index).unwrap();
let back: CallIndex = serde_json::from_str(&json).unwrap();
assert_eq!(back, index);
assert_eq!(back.get("callee").unwrap().0, vec!["caller".to_string()]);
assert_eq!(back.get("caller").unwrap().1, vec!["callee".to_string()]);
}
}