code_ranker_graph/
stats.rs1use crate::attrs::{attr_f64, is_external, num_attr};
7use code_ranker_plugin_api::{attrs::AttrValue, graph::Graph};
8use std::collections::BTreeMap;
9
10const STAT_KEYS: &[&str] = &[
14 "cyclomatic",
15 "cognitive",
16 "fan_in",
17 "fan_out",
18 "hk",
19 "mi",
20 "mi_sei",
21 "sloc",
22 "cloc",
23 "blank",
24 "tloc",
25 "length",
26 "vocabulary",
27 "volume",
28 "effort",
29 "time",
30 "bugs",
31];
32
33pub fn compute_stats(graph: &Graph) -> BTreeMap<String, AttrValue> {
36 let mut stats = BTreeMap::new();
37 for key in STAT_KEYS {
38 let vals: Vec<f64> = graph
39 .nodes
40 .iter()
41 .filter(|n| !is_external(n))
42 .filter_map(|n| attr_f64(n, key))
43 .filter(|v| v.is_finite() && *v > 0.0)
44 .collect();
45 if vals.is_empty() {
46 continue;
47 }
48 let avg = vals.iter().sum::<f64>() / vals.len() as f64;
49 if avg > 0.0 {
50 stats.insert((*key).to_string(), num_attr(avg));
51 }
52 }
53 stats
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use code_ranker_plugin_api::node::Node;
60
61 fn file(id: &str, cyclomatic: Option<i64>) -> Node {
62 let mut n = Node {
63 id: id.into(),
64 kind: "file".into(),
65 name: id.into(),
66 parent: None,
67 attrs: Default::default(),
68 };
69 if let Some(c) = cyclomatic {
70 n.attrs.insert("cyclomatic".into(), AttrValue::Int(c));
71 }
72 n
73 }
74
75 #[test]
76 fn average_excludes_zero_and_missing() {
77 let g = Graph {
78 nodes: vec![
79 file("a", Some(2)),
80 file("b", Some(4)),
81 file("z", Some(0)),
82 file("n", None),
83 ],
84 edges: vec![],
85 };
86 let s = compute_stats(&g);
87 assert_eq!(s.get("cyclomatic"), Some(&AttrValue::Int(3)));
88 }
89
90 #[test]
91 fn empty_graph_has_no_stats() {
92 let g = Graph::default();
93 assert!(compute_stats(&g).is_empty());
94 }
95}