1use petgraph::graph::{Graph, NodeIndex};
23use petgraph::visit::EdgeRef;
24use petgraph::Directed;
25use serde::{Deserialize, Serialize};
26use std::collections::VecDeque;
27
28use srcgraph_core::{ClassNode, EdgeKind};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct BetweennessResult {
35 pub scores: Vec<f64>,
36 pub normalized: bool,
38}
39
40impl BetweennessResult {
41 pub fn get(&self, n: NodeIndex) -> f64 {
42 self.scores[n.index()]
43 }
44}
45
46pub fn compute_betweenness<N, E>(
55 graph: &Graph<N, E, Directed>,
56 normalized: bool,
57) -> BetweennessResult
58where
59 N: ClassNode,
60 E: EdgeKind,
61{
62 let n = graph.node_count();
63 let mut cb = vec![0.0_f64; n];
64
65 if n == 0 {
66 return BetweennessResult { scores: cb, normalized: false };
67 }
68
69 let mut out: Vec<Vec<usize>> = vec![Vec::new(); n];
72 for e in graph.edge_references() {
73 out[e.source().index()].push(e.target().index());
74 }
75
76 let mut stack: Vec<usize> = Vec::with_capacity(n);
77 let mut preds: Vec<Vec<usize>> = vec![Vec::new(); n];
78 let mut sigma = vec![0.0_f64; n];
79 let mut dist = vec![-1_i64; n];
80 let mut delta = vec![0.0_f64; n];
81 let mut queue: VecDeque<usize> = VecDeque::with_capacity(n);
82
83 for s in 0..n {
84 stack.clear();
85 for p in preds.iter_mut() {
86 p.clear();
87 }
88 for x in sigma.iter_mut() {
89 *x = 0.0;
90 }
91 for d in dist.iter_mut() {
92 *d = -1;
93 }
94 for d in delta.iter_mut() {
95 *d = 0.0;
96 }
97 queue.clear();
98
99 sigma[s] = 1.0;
100 dist[s] = 0;
101 queue.push_back(s);
102
103 while let Some(v) = queue.pop_front() {
104 stack.push(v);
105 for &w in &out[v] {
106 if dist[w] < 0 {
107 dist[w] = dist[v] + 1;
108 queue.push_back(w);
109 }
110 if dist[w] == dist[v] + 1 {
111 sigma[w] += sigma[v];
112 preds[w].push(v);
113 }
114 }
115 }
116
117 while let Some(w) = stack.pop() {
118 let coeff = (1.0 + delta[w]) / sigma[w];
119 for &v in &preds[w] {
120 delta[v] += sigma[v] * coeff;
121 }
122 if w != s {
123 cb[w] += delta[w];
124 }
125 }
126 }
127
128 if normalized && n > 2 {
129 let scale = 1.0 / ((n - 1) as f64 * (n - 2) as f64);
130 for x in cb.iter_mut() {
131 *x *= scale;
132 }
133 return BetweennessResult { scores: cb, normalized: true };
134 }
135
136 BetweennessResult { scores: cb, normalized: false }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use srcgraph_core::{EdgeType, OwnedClassNode, OwnedGraph};
143 use petgraph::Graph;
144
145 fn node(id: &str) -> OwnedClassNode {
146 OwnedClassNode {
147 id: id.to_owned(),
148 name: id.to_owned(),
149 namespace: "test".to_owned(),
150 line_count: 1,
151 method_count: 1,
152 halstead_eta1: 0,
153 halstead_eta2: 0,
154 halstead_n1: 0,
155 halstead_n2: 0,
156 method_connectivity: None,
157 method_fingerprints: None,
158 method_tokens: None,
159 call_sequences: None,
160 cyclomatic_complexity: None,
161 path_conditions: None,
162 invariants: None,
163 error_messages: None,
164 magic_numbers: None,
165 dead_code: None,
166 tenant_branches: None,
167 state_transitions: None,
168 }
169 }
170
171 fn approx(a: f64, b: f64) -> bool {
172 (a - b).abs() < 1e-9
173 }
174
175 #[test]
176 fn betweenness_path_graph_unnormalized() {
177 let mut g: OwnedGraph = Graph::new();
181 let a = g.add_node(node("A"));
182 let b = g.add_node(node("B"));
183 let c = g.add_node(node("C"));
184 let d = g.add_node(node("D"));
185 g.add_edge(a, b, EdgeType::MethodCall);
186 g.add_edge(b, c, EdgeType::MethodCall);
187 g.add_edge(c, d, EdgeType::MethodCall);
188
189 let r = compute_betweenness(&g, false);
190 assert!(approx(r.get(a), 0.0));
191 assert!(approx(r.get(b), 2.0), "B={}", r.get(b));
192 assert!(approx(r.get(c), 2.0), "C={}", r.get(c));
193 assert!(approx(r.get(d), 0.0));
194 assert!(!r.normalized);
195 }
196
197 #[test]
198 fn betweenness_hub_normalized() {
199 let mut g: OwnedGraph = Graph::new();
202 let a = g.add_node(node("A"));
203 let leaves: Vec<_> = ["B", "C", "D"].iter().map(|l| g.add_node(node(l))).collect();
204 for &l in &leaves {
205 g.add_edge(a, l, EdgeType::MethodCall);
206 g.add_edge(l, a, EdgeType::MethodCall);
207 }
208
209 let r = compute_betweenness(&g, true);
210 assert!(r.normalized);
211 assert!(approx(r.get(a), 1.0), "hub={}", r.get(a));
212 for &l in &leaves {
213 assert!(approx(r.get(l), 0.0), "leaf={}", r.get(l));
214 }
215 }
216
217 #[test]
218 fn betweenness_parallel_paths_split_credit() {
219 let mut g: OwnedGraph = Graph::new();
222 let a = g.add_node(node("A"));
223 let b = g.add_node(node("B"));
224 let c = g.add_node(node("C"));
225 let d = g.add_node(node("D"));
226 g.add_edge(a, b, EdgeType::MethodCall);
227 g.add_edge(a, c, EdgeType::MethodCall);
228 g.add_edge(b, d, EdgeType::MethodCall);
229 g.add_edge(c, d, EdgeType::MethodCall);
230
231 let r = compute_betweenness(&g, false);
232 assert!(approx(r.get(a), 0.0));
233 assert!(approx(r.get(b), 0.5), "B={}", r.get(b));
234 assert!(approx(r.get(c), 0.5), "C={}", r.get(c));
235 assert!(approx(r.get(d), 0.0));
236 }
237
238 #[test]
239 fn betweenness_isolated_nodes_zero() {
240 let mut g: OwnedGraph = Graph::new();
241 let a = g.add_node(node("A"));
242 let b = g.add_node(node("B"));
243 let c = g.add_node(node("C"));
244 let r = compute_betweenness(&g, true);
245 for nx in [a, b, c] {
246 assert!(approx(r.get(nx), 0.0));
247 }
248 }
249
250 #[test]
251 fn betweenness_empty_graph() {
252 let g: OwnedGraph = Graph::new();
253 let r = compute_betweenness(&g, true);
254 assert!(r.scores.is_empty());
255 }
256
257 #[test]
258 fn betweenness_three_cycle_symmetric() {
259 let mut g: OwnedGraph = Graph::new();
262 let a = g.add_node(node("A"));
263 let b = g.add_node(node("B"));
264 let c = g.add_node(node("C"));
265 g.add_edge(a, b, EdgeType::MethodCall);
266 g.add_edge(b, c, EdgeType::MethodCall);
267 g.add_edge(c, a, EdgeType::MethodCall);
268
269 let r = compute_betweenness(&g, false);
270 assert!(approx(r.get(a), 1.0), "A={}", r.get(a));
271 assert!(approx(r.get(b), 1.0), "B={}", r.get(b));
272 assert!(approx(r.get(c), 1.0), "C={}", r.get(c));
273 }
274}