Skip to main content

code_split_core/
stats.rs

1use crate::graph::{AvgCoupling, Graph, GraphStats, Halstead, Loc, Maintainability, Node};
2
3pub fn annotate_stats(graph: &mut Graph) {
4    fn avg<F>(nodes: &[Node], f: F) -> f64
5    where
6        F: Fn(&Node) -> Option<f64>,
7    {
8        let vals: Vec<f64> = nodes
9            .iter()
10            .filter_map(f)
11            .filter(|v| v.is_finite() && *v > 0.0)
12            .collect();
13        if vals.is_empty() {
14            return 0.0;
15        }
16        vals.iter().sum::<f64>() / vals.len() as f64
17    }
18
19    let nodes = &graph.nodes;
20
21    let cyclomatic = avg(nodes, |n| n.complexity.as_ref().map(|c| c.cyclomatic));
22    let cognitive = avg(nodes, |n| n.complexity.as_ref().map(|c| c.cognitive));
23
24    let fan_in = avg(nodes, |n| {
25        n.complexity
26            .as_ref()
27            .and_then(|c| c.coupling.as_ref())
28            .map(|c| c.fan_in as f64)
29    });
30    let fan_out = avg(nodes, |n| {
31        n.complexity
32            .as_ref()
33            .and_then(|c| c.coupling.as_ref())
34            .map(|c| c.fan_out as f64)
35    });
36    let hk = avg(nodes, |n| {
37        n.complexity
38            .as_ref()
39            .and_then(|c| c.coupling.as_ref())
40            .map(|c| c.hk)
41    });
42    let coupling = (fan_in > 0.0 || fan_out > 0.0 || hk > 0.0).then_some(AvgCoupling {
43        fan_in,
44        fan_out,
45        hk,
46    });
47
48    let mi = avg(nodes, |n| {
49        n.complexity
50            .as_ref()
51            .and_then(|c| c.maintainability.as_ref())
52            .map(|m| m.mi)
53    });
54    let mi_sei = avg(nodes, |n| {
55        n.complexity
56            .as_ref()
57            .and_then(|c| c.maintainability.as_ref())
58            .map(|m| m.mi_sei)
59    });
60    let maintainability = (mi > 0.0).then_some(Maintainability { mi, mi_sei });
61
62    let loc_source = avg(nodes, |n| {
63        n.complexity
64            .as_ref()
65            .and_then(|c| c.loc.as_ref())
66            .map(|l| l.source)
67    });
68    let loc_comments = avg(nodes, |n| {
69        n.complexity
70            .as_ref()
71            .and_then(|c| c.loc.as_ref())
72            .map(|l| l.comments)
73    });
74    let loc_blank = avg(nodes, |n| {
75        n.complexity
76            .as_ref()
77            .and_then(|c| c.loc.as_ref())
78            .map(|l| l.blank)
79    });
80    let loc = (loc_source > 0.0).then_some(Loc {
81        source: loc_source,
82        logical: 0.0,
83        comments: loc_comments,
84        blank: loc_blank,
85    });
86
87    let h_length = avg(nodes, |n| {
88        n.complexity
89            .as_ref()
90            .and_then(|c| c.halstead.as_ref())
91            .map(|h| h.length)
92    });
93    let h_vocabulary = avg(nodes, |n| {
94        n.complexity
95            .as_ref()
96            .and_then(|c| c.halstead.as_ref())
97            .map(|h| h.vocabulary)
98    });
99    let h_volume = avg(nodes, |n| {
100        n.complexity
101            .as_ref()
102            .and_then(|c| c.halstead.as_ref())
103            .map(|h| h.volume)
104    });
105    let h_effort = avg(nodes, |n| {
106        n.complexity
107            .as_ref()
108            .and_then(|c| c.halstead.as_ref())
109            .map(|h| h.effort)
110    });
111    let h_time = avg(nodes, |n| {
112        n.complexity
113            .as_ref()
114            .and_then(|c| c.halstead.as_ref())
115            .map(|h| h.time)
116    });
117    let h_bugs = avg(nodes, |n| {
118        n.complexity
119            .as_ref()
120            .and_then(|c| c.halstead.as_ref())
121            .map(|h| h.bugs)
122    });
123    let halstead = (h_volume > 0.0).then_some(Halstead {
124        length: h_length,
125        vocabulary: h_vocabulary,
126        volume: h_volume,
127        effort: h_effort,
128        time: h_time,
129        bugs: h_bugs,
130    });
131
132    if cyclomatic == 0.0
133        && cognitive == 0.0
134        && coupling.is_none()
135        && maintainability.is_none()
136        && loc.is_none()
137        && halstead.is_none()
138    {
139        return;
140    }
141
142    graph.stats = Some(GraphStats {
143        cyclomatic,
144        cognitive,
145        coupling,
146        maintainability,
147        loc,
148        halstead,
149    });
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::graph::{Complexity, Coupling, NodeKind};
156
157    fn node(id: &str, complexity: Option<Complexity>) -> Node {
158        Node {
159            id: id.into(),
160            kind: NodeKind::File,
161            name: id.into(),
162            path: String::new(),
163            parent: None,
164            external: None,
165            version: None,
166            visibility: None,
167            loc: None,
168            line: None,
169            item_count: None,
170            method_count: None,
171            complexity,
172            cycle_kind: None,
173        }
174    }
175
176    fn graph_of(nodes: Vec<Node>) -> Graph {
177        Graph {
178            nodes,
179            edges: Vec::new(),
180            cycles: Vec::new(),
181            stats: None,
182        }
183    }
184
185    #[test]
186    fn empty_graph_leaves_stats_none() {
187        let mut g = graph_of(vec![]);
188        annotate_stats(&mut g);
189        assert!(g.stats.is_none());
190    }
191
192    #[test]
193    fn nodes_without_complexity_leave_stats_none() {
194        let mut g = graph_of(vec![node("a", None), node("b", None)]);
195        annotate_stats(&mut g);
196        assert!(g.stats.is_none(), "no metrics → no stats block");
197    }
198
199    #[test]
200    fn all_zero_metrics_leave_stats_none() {
201        let mut g = graph_of(vec![node(
202            "a",
203            Some(Complexity {
204                cyclomatic: 0.0,
205                ..Default::default()
206            }),
207        )]);
208        annotate_stats(&mut g);
209        assert!(g.stats.is_none(), "all-zero metrics → early return");
210    }
211
212    #[test]
213    fn cyclomatic_average_excludes_zero_and_missing() {
214        // 2.0 and 4.0 count; the 0.0 node is filtered (avg over > 0 only) and
215        // the complexity-less node contributes nothing → (2+4)/2 = 3.0.
216        let mut g = graph_of(vec![
217            node(
218                "a",
219                Some(Complexity {
220                    cyclomatic: 2.0,
221                    ..Default::default()
222                }),
223            ),
224            node(
225                "b",
226                Some(Complexity {
227                    cyclomatic: 4.0,
228                    ..Default::default()
229                }),
230            ),
231            node(
232                "z",
233                Some(Complexity {
234                    cyclomatic: 0.0,
235                    ..Default::default()
236                }),
237            ),
238            node("n", None),
239        ]);
240        annotate_stats(&mut g);
241        let stats = g.stats.expect("metrics present → stats block");
242        assert_eq!(stats.cyclomatic, 3.0, "(2+4)/2, zero excluded");
243    }
244
245    #[test]
246    fn coupling_is_averaged_and_attached() {
247        let mut g = graph_of(vec![
248            node(
249                "a",
250                Some(Complexity {
251                    coupling: Some(Coupling {
252                        fan_in: 2,
253                        fan_out: 4,
254                        hk: 10.0,
255                        ..Default::default()
256                    }),
257                    ..Default::default()
258                }),
259            ),
260            node(
261                "b",
262                Some(Complexity {
263                    coupling: Some(Coupling {
264                        fan_in: 4,
265                        fan_out: 8,
266                        hk: 30.0,
267                        ..Default::default()
268                    }),
269                    ..Default::default()
270                }),
271            ),
272        ]);
273        annotate_stats(&mut g);
274        let c = g.stats.unwrap().coupling.expect("coupling averaged");
275        assert_eq!(c.fan_in, 3.0);
276        assert_eq!(c.fan_out, 6.0);
277        assert_eq!(c.hk, 20.0);
278    }
279
280    #[test]
281    fn maintainability_attached_when_mi_positive() {
282        let mut g = graph_of(vec![node(
283            "a",
284            Some(Complexity {
285                maintainability: Some(Maintainability {
286                    mi: 80.0,
287                    mi_sei: 70.0,
288                }),
289                ..Default::default()
290            }),
291        )]);
292        annotate_stats(&mut g);
293        let m = g.stats.unwrap().maintainability.expect("mi > 0 → attached");
294        assert_eq!(m.mi, 80.0);
295        assert_eq!(m.mi_sei, 70.0);
296    }
297
298    #[test]
299    fn halstead_attached_when_volume_positive() {
300        let mut g = graph_of(vec![node(
301            "a",
302            Some(Complexity {
303                halstead: Some(Halstead {
304                    length: 10.0,
305                    vocabulary: 6.0,
306                    volume: 40.0,
307                    effort: 100.0,
308                    time: 5.0,
309                    bugs: 0.1,
310                }),
311                ..Default::default()
312            }),
313        )]);
314        annotate_stats(&mut g);
315        let h = g.stats.unwrap().halstead.expect("volume > 0 → attached");
316        assert_eq!(h.volume, 40.0);
317        assert_eq!(h.length, 10.0);
318    }
319
320    #[test]
321    fn halstead_absent_when_volume_zero_but_other_metrics_present() {
322        // cyclomatic keeps the stats block alive; volume 0 closes the halstead gate.
323        let mut g = graph_of(vec![node(
324            "a",
325            Some(Complexity {
326                cyclomatic: 3.0,
327                halstead: Some(Halstead {
328                    length: 5.0,
329                    vocabulary: 0.0,
330                    volume: 0.0,
331                    effort: 0.0,
332                    time: 0.0,
333                    bugs: 0.0,
334                }),
335                ..Default::default()
336            }),
337        )]);
338        annotate_stats(&mut g);
339        let stats = g.stats.expect("cyclomatic keeps stats present");
340        assert_eq!(stats.cyclomatic, 3.0);
341        assert!(stats.halstead.is_none(), "volume 0 → halstead gate closed");
342    }
343}