1use crate::graph::{Complexity, Coupling, EdgeKind, Graph, Loc, NodeId, NodeKind};
5use crate::snapshot::PluginGraphs;
6use std::collections::{HashMap, HashSet};
7
8pub fn annotate_hk(graphs: &mut PluginGraphs) {
9 annotate_graph_hk(&mut graphs.modules);
10 annotate_graph_hk(&mut graphs.files);
11 annotate_graph_hk(&mut graphs.functions);
12}
13
14fn annotate_graph_hk(graph: &mut Graph) {
15 let has_calls = graph.edges.iter().any(|e| e.kind == EdgeKind::Calls);
19
20 let mut fan_in: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
21 let mut fan_out: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
22
23 for edge in &graph.edges {
24 if edge.kind == EdgeKind::Contains {
25 continue;
26 }
27 fan_out
28 .entry(edge.from.clone())
29 .or_default()
30 .insert(edge.to.clone());
31 fan_in
32 .entry(edge.to.clone())
33 .or_default()
34 .insert(edge.from.clone());
35 }
36
37 for node in &mut graph.nodes {
38 if !has_calls && matches!(node.kind, NodeKind::Fn | NodeKind::Method) {
39 continue;
40 }
41 let fi = fan_in.get(&node.id).map(|s| s.len()).unwrap_or(0);
42 let fo = fan_out.get(&node.id).map(|s| s.len()).unwrap_or(0);
43 let struct_loc = node.loc; let cx = node.complexity.get_or_insert_with(Complexity::default);
46 if cx.loc.is_none()
50 && let Some(n) = struct_loc
51 && n > 0
52 {
53 cx.loc = Some(Loc {
54 source: n as f64,
55 logical: 0.0,
56 comments: 0.0,
57 blank: 0.0,
58 });
59 }
60 let loc = cx.loc.as_ref().map(|l| l.source).unwrap_or(0.0);
63 let hk = loc * ((fi * fo) as f64).powi(2);
64 cx.coupling = Some(Coupling {
65 fan_in: fi as u32,
66 fan_out: fo as u32,
67 hk,
68 });
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::graph::{Edge, Loc, Node};
76
77 fn module(id: &str, complexity_loc: Option<f64>, struct_loc: Option<u32>) -> Node {
78 Node {
79 id: id.into(),
80 kind: NodeKind::Module,
81 name: id.into(),
82 path: "p".into(),
83 parent: None,
84 external: None,
85 visibility: None,
86 loc: struct_loc,
87 line: None,
88 item_count: None,
89 method_count: None,
90 complexity: complexity_loc.map(|s| Complexity {
91 loc: Some(Loc {
92 source: s,
93 logical: 0.0,
94 comments: 0.0,
95 blank: 0.0,
96 }),
97 ..Default::default()
98 }),
99 cycle_kind: None,
100 }
101 }
102
103 fn uses(from: &str, to: &str) -> Edge {
104 Edge {
105 from: from.into(),
106 to: to.into(),
107 kind: EdgeKind::Uses,
108 unresolved: None,
109 external: None,
110 visibility: None,
111 }
112 }
113
114 fn coupling<'a>(g: &'a Graph, id: &str) -> &'a Coupling {
115 g.nodes
116 .iter()
117 .find(|n| n.id == id)
118 .unwrap()
119 .complexity
120 .as_ref()
121 .unwrap()
122 .coupling
123 .as_ref()
124 .unwrap()
125 }
126
127 #[test]
128 fn hk_is_loc_times_fan_squared() {
129 let mut g = PluginGraphs::default();
131 g.modules.nodes = vec![
132 module("A", Some(4.0), Some(4)),
133 module("B", Some(10.0), Some(10)),
134 module("C", Some(5.0), Some(5)),
135 ];
136 g.modules.edges = vec![uses("A", "B"), uses("B", "C")];
137 annotate_graph_hk(&mut g.modules);
138
139 let b = coupling(&g.modules, "B");
140 assert_eq!((b.fan_in, b.fan_out), (1, 1));
141 assert_eq!(b.hk, 10.0, "hk = loc(10) · (fan_in·fan_out)²");
142 }
143
144 #[test]
145 fn hk_falls_back_to_structural_loc_for_crate_like_nodes() {
146 let mut g = PluginGraphs::default();
149 g.modules.nodes = vec![
150 module("X", None, Some(10)),
151 module("Y", Some(5.0), Some(5)),
152 module("Z", Some(5.0), Some(5)),
153 ];
154 g.modules.edges = vec![uses("Y", "X"), uses("X", "Z")];
155 annotate_graph_hk(&mut g.modules);
156
157 let x = g.modules.nodes.iter().find(|n| n.id == "X").unwrap();
158 let xc = x.complexity.as_ref().unwrap();
159 assert_eq!(
160 xc.loc.as_ref().unwrap().source,
161 10.0,
162 "structural loc mirrored into complexity.loc so it is displayed"
163 );
164 let cp = xc.coupling.as_ref().unwrap();
165 assert_eq!((cp.fan_in, cp.fan_out), (1, 1));
166 assert_eq!(
167 cp.hk, 10.0,
168 "crate-like node keeps hk from its structural loc"
169 );
170 }
171
172 #[test]
173 fn hk_is_zero_without_any_loc() {
174 let mut g = PluginGraphs::default();
176 g.modules.nodes = vec![module("M", None, None), module("N", Some(3.0), Some(3))];
177 g.modules.edges = vec![uses("M", "N")];
178 annotate_graph_hk(&mut g.modules);
179 let m = coupling(&g.modules, "M");
180 assert_eq!(m.fan_out, 1);
181 assert_eq!(m.hk, 0.0, "no loc anywhere → hk 0");
182 }
183}