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.files);
10}
11
12fn is_external(node: &crate::graph::Node) -> bool {
14 node.kind == NodeKind::External || node.external.unwrap_or(false)
15}
16
17fn annotate_graph_hk(graph: &mut Graph) {
18 let external_ids: HashSet<&str> = graph
22 .nodes
23 .iter()
24 .filter(|n| is_external(n))
25 .map(|n| n.id.as_str())
26 .collect();
27
28 let mut fan_in: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
29 let mut fan_out: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
30 let mut fan_out_ext: HashMap<NodeId, HashSet<NodeId>> = HashMap::new();
31
32 for edge in &graph.edges {
33 if edge.kind == EdgeKind::Contains {
36 continue;
37 }
38 let to_external = external_ids.contains(edge.to.as_str());
41 let from_external = external_ids.contains(edge.from.as_str());
42 if to_external {
43 fan_out_ext
44 .entry(edge.from.clone())
45 .or_default()
46 .insert(edge.to.clone());
47 continue;
48 }
49 if from_external {
50 continue; }
52 fan_out
53 .entry(edge.from.clone())
54 .or_default()
55 .insert(edge.to.clone());
56 fan_in
57 .entry(edge.to.clone())
58 .or_default()
59 .insert(edge.from.clone());
60 }
61
62 for node in &mut graph.nodes {
63 if is_external(node) {
64 continue; }
66 let fi = fan_in.get(&node.id).map(|s| s.len()).unwrap_or(0);
67 let fo = fan_out.get(&node.id).map(|s| s.len()).unwrap_or(0);
68 let foe = fan_out_ext.get(&node.id).map(|s| s.len()).unwrap_or(0);
69 let struct_loc = node.loc; let cx = node.complexity.get_or_insert_with(Complexity::default);
72 if cx.loc.is_none()
76 && let Some(n) = struct_loc
77 && n > 0
78 {
79 cx.loc = Some(Loc {
80 source: n as f64,
81 logical: 0.0,
82 comments: 0.0,
83 blank: 0.0,
84 });
85 }
86 let loc = cx.loc.as_ref().map(|l| l.source).unwrap_or(0.0);
89 let hk = loc * ((fi * fo) as f64).powi(2);
90 cx.coupling = Some(Coupling {
91 fan_in: fi as u32,
92 fan_out: fo as u32,
93 fan_out_external: foe as u32,
94 hk,
95 });
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::graph::{Edge, EdgeKind, Loc, Node};
103
104 fn module(id: &str, complexity_loc: Option<f64>, struct_loc: Option<u32>) -> Node {
105 Node {
106 id: id.into(),
107 kind: NodeKind::Module,
108 name: id.into(),
109 path: "p".into(),
110 parent: None,
111 external: None,
112 version: None,
113 visibility: None,
114 loc: struct_loc,
115 line: None,
116 item_count: None,
117 method_count: None,
118 complexity: complexity_loc.map(|s| Complexity {
119 loc: Some(Loc {
120 source: s,
121 logical: 0.0,
122 comments: 0.0,
123 blank: 0.0,
124 }),
125 ..Default::default()
126 }),
127 cycle_kind: None,
128 }
129 }
130
131 fn uses(from: &str, to: &str) -> Edge {
132 Edge {
133 from: from.into(),
134 to: to.into(),
135 kind: EdgeKind::Uses,
136 unresolved: None,
137 external: None,
138 visibility: None,
139 }
140 }
141
142 fn coupling<'a>(g: &'a Graph, id: &str) -> &'a Coupling {
143 g.nodes
144 .iter()
145 .find(|n| n.id == id)
146 .unwrap()
147 .complexity
148 .as_ref()
149 .unwrap()
150 .coupling
151 .as_ref()
152 .unwrap()
153 }
154
155 #[test]
156 fn hk_is_loc_times_fan_squared() {
157 let mut g = PluginGraphs::default();
159 g.files.nodes = vec![
160 module("A", Some(4.0), Some(4)),
161 module("B", Some(10.0), Some(10)),
162 module("C", Some(5.0), Some(5)),
163 ];
164 g.files.edges = vec![uses("A", "B"), uses("B", "C")];
165 annotate_graph_hk(&mut g.files);
166
167 let b = coupling(&g.files, "B");
168 assert_eq!((b.fan_in, b.fan_out), (1, 1));
169 assert_eq!(b.hk, 10.0, "hk = loc(10) · (fan_in·fan_out)²");
170 }
171
172 #[test]
173 fn hk_falls_back_to_structural_loc_for_crate_like_nodes() {
174 let mut g = PluginGraphs::default();
177 g.files.nodes = vec![
178 module("X", None, Some(10)),
179 module("Y", Some(5.0), Some(5)),
180 module("Z", Some(5.0), Some(5)),
181 ];
182 g.files.edges = vec![uses("Y", "X"), uses("X", "Z")];
183 annotate_graph_hk(&mut g.files);
184
185 let x = g.files.nodes.iter().find(|n| n.id == "X").unwrap();
186 let xc = x.complexity.as_ref().unwrap();
187 assert_eq!(
188 xc.loc.as_ref().unwrap().source,
189 10.0,
190 "structural loc mirrored into complexity.loc so it is displayed"
191 );
192 let cp = xc.coupling.as_ref().unwrap();
193 assert_eq!((cp.fan_in, cp.fan_out), (1, 1));
194 assert_eq!(
195 cp.hk, 10.0,
196 "crate-like node keeps hk from its structural loc"
197 );
198 }
199
200 #[test]
201 fn hk_is_zero_without_any_loc() {
202 let mut g = PluginGraphs::default();
204 g.files.nodes = vec![module("M", None, None), module("N", Some(3.0), Some(3))];
205 g.files.edges = vec![uses("M", "N")];
206 annotate_graph_hk(&mut g.files);
207 let m = coupling(&g.files, "M");
208 assert_eq!(m.fan_out, 1);
209 assert_eq!(m.hk, 0.0, "no loc anywhere → hk 0");
210 }
211}