synwire_agent/call_graph/
graph.rs1use std::collections::{HashMap, HashSet};
4
5#[non_exhaustive]
7pub struct CallNode {
8 pub name: String,
10 pub file: String,
12 pub line: u32,
14}
15
16pub struct DynamicCallGraph {
18 edges: Vec<(String, String)>,
19}
20
21impl DynamicCallGraph {
22 pub const fn new() -> Self {
24 Self { edges: Vec::new() }
25 }
26
27 #[allow(clippy::similar_names)]
29 pub fn add_edge(&mut self, caller: &str, callee: &str) {
30 self.edges.push((caller.to_owned(), callee.to_owned()));
31 }
32
33 pub fn callees(&self, caller: &str) -> Vec<&str> {
35 self.edges
36 .iter()
37 .filter(|(c, _)| c == caller)
38 .map(|(_, callee)| callee.as_str())
39 .collect()
40 }
41
42 pub fn callers(&self, callee: &str) -> Vec<&str> {
44 self.edges
45 .iter()
46 .filter(|(_, t)| t == callee)
47 .map(|(caller, _)| caller.as_str())
48 .collect()
49 }
50
51 pub fn has_cycle(&self) -> bool {
55 fn dfs<'a>(
56 node: &'a str,
57 adj: &HashMap<&'a str, Vec<&'a str>>,
58 visited: &mut HashSet<&'a str>,
59 in_stack: &mut HashSet<&'a str>,
60 ) -> bool {
61 if in_stack.contains(node) {
62 return true;
63 }
64 if visited.contains(node) {
65 return false;
66 }
67 let _ = visited.insert(node);
68 let _ = in_stack.insert(node);
69 if let Some(neighbors) = adj.get(node) {
70 for &neighbor in neighbors {
71 if dfs(neighbor, adj, visited, in_stack) {
72 return true;
73 }
74 }
75 }
76 let _ = in_stack.remove(node);
77 false
78 }
79
80 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
81 for (from, to) in &self.edges {
82 adj.entry(from.as_str()).or_default().push(to.as_str());
83 }
84
85 let mut visited: HashSet<&str> = HashSet::new();
86 let mut in_stack: HashSet<&str> = HashSet::new();
87
88 let all_nodes: Vec<&str> = adj.keys().copied().collect();
89 for node in all_nodes {
90 if dfs(node, &adj, &mut visited, &mut in_stack) {
91 return true;
92 }
93 }
94 false
95 }
96}
97
98impl Default for DynamicCallGraph {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn cycle_detection_finds_cycle() {
110 let mut g = DynamicCallGraph::new();
111 g.add_edge("a", "b");
112 g.add_edge("b", "c");
113 g.add_edge("c", "a"); assert!(g.has_cycle());
115 }
116
117 #[test]
118 fn cycle_detection_no_false_positive() {
119 let mut g = DynamicCallGraph::new();
120 g.add_edge("a", "b");
121 g.add_edge("b", "c");
122 assert!(!g.has_cycle());
123 }
124
125 #[test]
126 fn callees_and_callers() {
127 let mut g = DynamicCallGraph::new();
128 g.add_edge("main", "parse");
129 g.add_edge("main", "execute");
130 assert_eq!(g.callees("main").len(), 2);
131 assert_eq!(g.callers("parse"), vec!["main"]);
132 }
133}