1use petgraph::visit::EdgeRef;
16use petgraph::Graph;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20use srcgraph_core::{ClassNode, EdgeKind};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct NamespaceInstability {
25 pub namespace: String,
26 pub ca: u32,
28 pub ce: u32,
30 pub instability: Option<f64>,
32}
33
34pub fn instability(ca: u32, ce: u32) -> Option<f64> {
36 let total = ca + ce;
37 if total == 0 {
38 None
39 } else {
40 Some(ce as f64 / total as f64)
41 }
42}
43
44pub fn compute_instability<N, E>(graph: &Graph<N, E>) -> Vec<NamespaceInstability>
50where
51 N: ClassNode,
52 E: EdgeKind,
53{
54 let mut ca: HashMap<String, u32> = HashMap::new();
55 let mut ce: HashMap<String, u32> = HashMap::new();
56
57 for nx in graph.node_indices() {
59 let ns = graph[nx].namespace().to_owned();
60 ca.entry(ns.clone()).or_insert(0);
61 ce.entry(ns).or_insert(0);
62 }
63
64 for edge in graph.edge_references() {
65 let src_ns = graph[edge.source()].namespace();
66 let tgt_ns = graph[edge.target()].namespace();
67 if src_ns == tgt_ns {
68 continue;
69 }
70 *ce.entry(src_ns.to_owned()).or_insert(0) += 1;
71 *ca.entry(tgt_ns.to_owned()).or_insert(0) += 1;
72 }
73
74 let mut out: Vec<NamespaceInstability> = ca
75 .keys()
76 .map(|ns| {
77 let ca_v = ca[ns];
78 let ce_v = ce[ns];
79 NamespaceInstability {
80 namespace: ns.clone(),
81 ca: ca_v,
82 ce: ce_v,
83 instability: instability(ca_v, ce_v),
84 }
85 })
86 .collect();
87 out.sort_by(|a, b| a.namespace.cmp(&b.namespace));
88 out
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SdpViolation {
94 pub source_namespace: String,
95 pub target_namespace: String,
96 pub i_source: f64,
97 pub i_target: f64,
98 pub gap: f64,
100}
101
102pub fn find_sdp_violations<N, E>(graph: &Graph<N, E>) -> Vec<SdpViolation>
107where
108 N: ClassNode,
109 E: EdgeKind,
110{
111 let by_ns: HashMap<String, Option<f64>> = compute_instability(graph)
112 .into_iter()
113 .map(|r| (r.namespace, r.instability))
114 .collect();
115
116 let mut violations: Vec<SdpViolation> = Vec::new();
117 for edge in graph.edge_references() {
118 let src_ns = graph[edge.source()].namespace();
119 let tgt_ns = graph[edge.target()].namespace();
120 if src_ns == tgt_ns {
121 continue;
122 }
123 let (Some(i_src), Some(i_tgt)) = (
124 by_ns.get(src_ns).copied().flatten(),
125 by_ns.get(tgt_ns).copied().flatten(),
126 ) else {
127 continue;
128 };
129 if i_src < i_tgt {
130 violations.push(SdpViolation {
131 source_namespace: src_ns.to_owned(),
132 target_namespace: tgt_ns.to_owned(),
133 i_source: i_src,
134 i_target: i_tgt,
135 gap: i_tgt - i_src,
136 });
137 }
138 }
139 violations.sort_by(|a, b| b.gap.partial_cmp(&a.gap).unwrap_or(std::cmp::Ordering::Equal));
140 violations
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use srcgraph_core::{EdgeType, OwnedClassNode, OwnedGraph};
147 use petgraph::Graph;
148
149 fn class(id: &str, namespace: &str) -> OwnedClassNode {
150 OwnedClassNode {
151 id: id.to_owned(),
152 name: id.rsplit('.').next().unwrap_or(id).to_owned(),
153 namespace: namespace.to_owned(),
154 line_count: 10,
155 method_count: 1,
156 halstead_eta1: 0,
157 halstead_eta2: 0,
158 halstead_n1: 0,
159 halstead_n2: 0,
160 method_connectivity: None,
161 method_fingerprints: None,
162 method_tokens: None,
163 call_sequences: None,
164 cyclomatic_complexity: None,
165 path_conditions: None,
166 invariants: None,
167 error_messages: None,
168 magic_numbers: None,
169 dead_code: None,
170 tenant_branches: None,
171 state_transitions: None,
172 }
173 }
174
175 fn by_ns(rows: Vec<NamespaceInstability>) -> HashMap<String, NamespaceInstability> {
176 rows.into_iter().map(|r| (r.namespace.clone(), r)).collect()
177 }
178
179 #[test]
180 fn instability_formula_edges() {
181 assert_eq!(instability(5, 0), Some(0.0));
183 assert_eq!(instability(0, 5), Some(1.0));
185 assert_eq!(instability(2, 2), Some(0.5));
187 assert_eq!(instability(0, 0), None);
189 }
190
191 #[test]
192 fn intra_namespace_edges_do_not_count() {
193 let mut g: OwnedGraph = Graph::new();
196 let a = g.add_node(class("p.A", "p"));
197 let b = g.add_node(class("p.B", "p"));
198 g.add_edge(a, b, EdgeType::MethodCall);
199
200 let rows = compute_instability(&g);
201 let m = by_ns(rows);
202 assert_eq!(m["p"].ca, 0);
203 assert_eq!(m["p"].ce, 0);
204 assert!(m["p"].instability.is_none());
205 }
206
207 #[test]
208 fn cross_namespace_edge_assigns_ca_and_ce() {
209 let mut g: OwnedGraph = Graph::new();
211 let a = g.add_node(class("p.A", "p"));
212 let b = g.add_node(class("q.B", "q"));
213 g.add_edge(a, b, EdgeType::MethodCall);
214
215 let m = by_ns(compute_instability(&g));
216 assert_eq!(m["p"].ce, 1);
217 assert_eq!(m["p"].ca, 0);
218 assert_eq!(m["p"].instability, Some(1.0));
219
220 assert_eq!(m["q"].ca, 1);
221 assert_eq!(m["q"].ce, 0);
222 assert_eq!(m["q"].instability, Some(0.0));
223 }
224
225 #[test]
226 fn isolated_namespace_yields_none() {
227 let mut g: OwnedGraph = Graph::new();
228 g.add_node(class("p.A", "p"));
229 let rows = compute_instability(&g);
230 let m = by_ns(rows);
231 assert_eq!(m["p"].ca, 0);
232 assert_eq!(m["p"].ce, 0);
233 assert!(m["p"].instability.is_none());
234 }
235
236 #[test]
237 fn parallel_edges_each_count() {
238 let mut g: OwnedGraph = Graph::new();
240 let a = g.add_node(class("p.A", "p"));
241 let b = g.add_node(class("q.B", "q"));
242 g.add_edge(a, b, EdgeType::MethodCall);
243 g.add_edge(a, b, EdgeType::FieldRef);
244
245 let m = by_ns(compute_instability(&g));
246 assert_eq!(m["p"].ce, 2);
247 assert_eq!(m["q"].ca, 2);
248 }
249
250 #[test]
251 fn sdp_violation_detected_and_intra_namespace_skipped() {
252 let mut g: OwnedGraph = Graph::new();
258 let s = g.add_node(class("s.A", "s"));
259 let s2 = g.add_node(class("s.A2", "s"));
260 let u = g.add_node(class("u.A", "u"));
261 let z = g.add_node(class("z.A", "z"));
262 let x = g.add_node(class("x.A", "x"));
263 let y = g.add_node(class("y.A", "y"));
264 g.add_edge(x, s, EdgeType::MethodCall);
265 g.add_edge(y, s, EdgeType::MethodCall);
266 g.add_edge(s, u, EdgeType::MethodCall);
267 g.add_edge(u, z, EdgeType::MethodCall);
268 g.add_edge(s, s2, EdgeType::MethodCall); let m = by_ns(compute_instability(&g));
276 assert!((m["s"].instability.unwrap() - 1.0 / 3.0).abs() < 1e-12);
277 assert_eq!(m["u"].instability, Some(0.5));
278 assert_eq!(m["z"].instability, Some(0.0));
279 assert_eq!(m["x"].instability, Some(1.0));
280 assert_eq!(m["y"].instability, Some(1.0));
281
282 let violations = find_sdp_violations(&g);
283 assert_eq!(violations.len(), 1);
286 assert_eq!(violations[0].source_namespace, "s");
287 assert_eq!(violations[0].target_namespace, "u");
288 assert!((violations[0].gap - (0.5 - 1.0 / 3.0)).abs() < 1e-12);
289 }
290
291 #[test]
292 fn sdp_skips_edges_touching_isolated_namespace() {
293 let mut g: OwnedGraph = Graph::new();
299 let a = g.add_node(class("a.A", "a"));
300 let b = g.add_node(class("b.B", "b"));
301 let c = g.add_node(class("c.C", "c"));
302 g.add_edge(a, b, EdgeType::MethodCall);
303 g.add_edge(b, c, EdgeType::MethodCall);
304 g.add_edge(c, a, EdgeType::MethodCall);
305 assert!(find_sdp_violations(&g).is_empty());
307 }
308
309 #[test]
310 fn violations_sorted_by_gap_descending() {
311 let mut g: OwnedGraph = Graph::new();
318 let s = g.add_node(class("s.A", "s")); let m = g.add_node(class("m.A", "m")); let u = g.add_node(class("u.A", "u")); let v = g.add_node(class("v.A", "v")); let s_in1 = g.add_node(class("x.A", "x"));
327 let s_in2 = g.add_node(class("y.A", "y"));
328 g.add_edge(s_in1, s, EdgeType::MethodCall); g.add_edge(s_in2, s, EdgeType::MethodCall); g.add_edge(s, v, EdgeType::MethodCall); g.add_edge(m, u, EdgeType::MethodCall); g.add_edge(v, m, EdgeType::MethodCall); let by = by_ns(compute_instability(&g));
338 let i_s = by["s"].instability.unwrap();
339 let i_v = by["v"].instability.unwrap();
340 let i_m = by["m"].instability.unwrap();
341 let i_u = by["u"].instability.unwrap();
342
343 let violations = find_sdp_violations(&g);
344 let sv = violations.iter().find(|x| x.source_namespace == "s").unwrap();
349 assert!((sv.i_source - i_s).abs() < 1e-12);
350 assert!((sv.i_target - i_v).abs() < 1e-12);
351
352 assert!((i_m - 0.5).abs() < 1e-12);
354 assert_eq!(i_u, 0.0);
355
356 for w in violations.windows(2) {
358 assert!(w[0].gap >= w[1].gap);
359 }
360 }
361}