Skip to main content

srcgraph_metrics/
halstead.rs

1//! Halstead complexity — V (volume), D (difficulty), E (effort), B (bug
2//! estimate) computed from the four raw token counts (η₁, η₂, N₁, N₂)
3//! already extracted onto each class node.
4//!
5//!   V = N × log₂(η)              information content in bits
6//!   D = (η₁ / 2) × (N₂ / η₂)    mental effort to understand
7//!   E = D × V                    total mental effort
8//!   B = V / 3000                 expected defects
9//!
10//! Edge cases mirror the Python reference: η = 0 → V = 0, η₂ = 0 → D = 0.
11//!
12//! See `DESIGN.md` (Phase 2) at the workspace root.
13
14use petgraph::Graph;
15use serde::{Deserialize, Serialize};
16
17use srcgraph_core::{ClassNode, EdgeKind};
18
19/// Halstead readout for a single class.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct Halstead {
22    pub class_id: String,
23    /// Vocabulary η = η₁ + η₂.
24    pub eta: u32,
25    /// Length N = N₁ + N₂.
26    pub n: u32,
27    /// Volume in bits.
28    pub volume: f64,
29    pub difficulty: f64,
30    pub effort: f64,
31    pub bugs: f64,
32}
33
34/// Compute Halstead metrics from raw counts.
35pub 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
53/// Compute Halstead V/D/E/B for every class node.
54pub 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        // Canonical worked example: η₁=10, η₂=7, N₁=27, N₂=15.
117        // η=17, N=42, V = 42·log2(17) ≈ 171.6.
118        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        // D = (10/2) × (15/7) = 5 × 2.142857… ≈ 10.714
123        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        // No operands at all — D undefined, treated as 0 per Python reference.
142        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)); // syntax-only / no data
152        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}