Skip to main content

code_split_core/
hk.rs

1// Import from the defining modules (not the crate-root re-exports) so this module
2// depends "down" on `graph`/`snapshot` rather than "up" on the crate root — which
3// would close a `root → hk → root` cycle.
4use 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    // If the graph has no Calls edges (sema was skipped), fn/method nodes get
16    // no coupling annotation — showing 0 would be misleading vs. genuinely
17    // isolated nodes discovered when sema did run.
18    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; // structural LOC, present on aggregate (crate) nodes
44
45        let cx = node.complexity.get_or_insert_with(Complexity::default);
46        // When rust-code-analysis produced no LOC (e.g. synthetic crate nodes) but a
47        // structural line count exists, mirror it into `complexity.loc` so the displayed
48        // loc and hk always agree instead of one being blank.
49        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        // Henry-Kafura: hk = loc × (fan_in × fan_out)². Uses the same loc that is
61        // displayed; with no loc or no in/out coupling, hk is 0.
62        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        // A -> B -> C.  B has loc 10, fan_in 1, fan_out 1 → hk = 10·(1·1)² = 10.
130        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        // Y -> X -> Z.  X is crate-like: only structural node.loc, no complexity.loc.
147        // It must keep an hk (fan_in 1, fan_out 1) AND surface that loc in complexity.loc.
148        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        // M -> N : M has neither complexity.loc nor structural loc → hk 0 despite fan_out.
175        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}