1use petgraph::Graph;
15use serde::{Deserialize, Serialize};
16
17use srcgraph_core::{ClassNode, EdgeKind};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Halstead {
22 pub class_id: String,
23 pub eta: u32,
25 pub n: u32,
27 pub volume: f64,
29 pub difficulty: f64,
30 pub effort: f64,
31 pub bugs: f64,
32}
33
34pub fn halstead_metrics(eta1: u32, eta2: u32, n1: u32, n2: u32) -> (f64, f64, f64, f64, u32, u32) {
36 let eta = eta1 + eta2;
37 let n = n1 + n2;
38 let volume = if eta > 0 {
39 n as f64 * (eta as f64).log2()
40 } else {
41 0.0
42 };
43 let difficulty = if eta2 > 0 {
44 (eta1 as f64 / 2.0) * (n2 as f64 / eta2 as f64)
45 } else {
46 0.0
47 };
48 let effort = difficulty * volume;
49 let bugs = volume / 3000.0;
50 (volume, difficulty, effort, bugs, eta, n)
51}
52
53pub fn compute_halstead<N, E>(graph: &Graph<N, E>) -> Vec<Halstead>
55where
56 N: ClassNode,
57 E: EdgeKind,
58{
59 graph
60 .node_indices()
61 .map(|nx| {
62 let node = &graph[nx];
63 let (volume, difficulty, effort, bugs, eta, n) = halstead_metrics(
64 node.halstead_eta1(),
65 node.halstead_eta2(),
66 node.halstead_n1(),
67 node.halstead_n2(),
68 );
69 Halstead {
70 class_id: node.id().to_owned(),
71 eta,
72 n,
73 volume,
74 difficulty,
75 effort,
76 bugs,
77 }
78 })
79 .collect()
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use srcgraph_core::{EdgeType, OwnedClassNode, OwnedGraph};
86 use petgraph::Graph;
87
88 fn class(id: &str, eta1: u32, eta2: u32, n1: u32, n2: u32) -> OwnedClassNode {
89 OwnedClassNode {
90 id: id.to_owned(),
91 name: id.to_owned(),
92 namespace: "test".to_owned(),
93 line_count: 10,
94 method_count: 1,
95 halstead_eta1: eta1,
96 halstead_eta2: eta2,
97 halstead_n1: n1,
98 halstead_n2: n2,
99 method_connectivity: None,
100 method_fingerprints: None,
101 method_tokens: None,
102 call_sequences: None,
103 cyclomatic_complexity: None,
104 path_conditions: None,
105 invariants: None,
106 error_messages: None,
107 magic_numbers: None,
108 dead_code: None,
109 tenant_branches: None,
110 state_transitions: None,
111 }
112 }
113
114 #[test]
115 fn halstead_basic_formula() {
116 let (v, d, e, b, eta, n) = halstead_metrics(10, 7, 27, 15);
119 assert_eq!(eta, 17);
120 assert_eq!(n, 42);
121 assert!((v - 42.0 * 17_f64.log2()).abs() < 1e-9);
122 assert!((d - (5.0 * 15.0 / 7.0)).abs() < 1e-9);
124 assert!((e - d * v).abs() < 1e-9);
125 assert!((b - v / 3000.0).abs() < 1e-9);
126 }
127
128 #[test]
129 fn halstead_zero_vocabulary_yields_zero_volume() {
130 let (v, d, e, b, eta, n) = halstead_metrics(0, 0, 0, 0);
131 assert_eq!(eta, 0);
132 assert_eq!(n, 0);
133 assert_eq!(v, 0.0);
134 assert_eq!(d, 0.0);
135 assert_eq!(e, 0.0);
136 assert_eq!(b, 0.0);
137 }
138
139 #[test]
140 fn halstead_zero_distinct_operands_difficulty_zero() {
141 let (v, d, ..) = halstead_metrics(3, 0, 5, 0);
143 assert!(v > 0.0);
144 assert_eq!(d, 0.0);
145 }
146
147 #[test]
148 fn halstead_walks_graph() {
149 let mut g: OwnedGraph = Graph::new();
150 let a = g.add_node(class("A", 10, 7, 27, 15));
151 let b = g.add_node(class("B", 0, 0, 0, 0)); g.add_edge(a, b, EdgeType::MethodCall);
153
154 let rows = compute_halstead(&g);
155 assert_eq!(rows.len(), 2);
156
157 let ra = rows.iter().find(|r| r.class_id == "A").unwrap();
158 assert_eq!(ra.eta, 17);
159 assert_eq!(ra.n, 42);
160 assert!(ra.volume > 0.0);
161
162 let rb = rows.iter().find(|r| r.class_id == "B").unwrap();
163 assert_eq!(rb.eta, 0);
164 assert_eq!(rb.volume, 0.0);
165 assert_eq!(rb.difficulty, 0.0);
166 }
167}