use petgraph::Graph;
use serde::{Deserialize, Serialize};
use srcgraph_core::{ClassNode, EdgeKind};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Halstead {
pub class_id: String,
pub eta: u32,
pub n: u32,
pub volume: f64,
pub difficulty: f64,
pub effort: f64,
pub bugs: f64,
}
pub fn halstead_metrics(eta1: u32, eta2: u32, n1: u32, n2: u32) -> (f64, f64, f64, f64, u32, u32) {
let eta = eta1 + eta2;
let n = n1 + n2;
let volume = if eta > 0 {
n as f64 * (eta as f64).log2()
} else {
0.0
};
let difficulty = if eta2 > 0 {
(eta1 as f64 / 2.0) * (n2 as f64 / eta2 as f64)
} else {
0.0
};
let effort = difficulty * volume;
let bugs = volume / 3000.0;
(volume, difficulty, effort, bugs, eta, n)
}
pub fn compute_halstead<N, E>(graph: &Graph<N, E>) -> Vec<Halstead>
where
N: ClassNode,
E: EdgeKind,
{
graph
.node_indices()
.map(|nx| {
let node = &graph[nx];
let (volume, difficulty, effort, bugs, eta, n) = halstead_metrics(
node.halstead_eta1(),
node.halstead_eta2(),
node.halstead_n1(),
node.halstead_n2(),
);
Halstead {
class_id: node.id().to_owned(),
eta,
n,
volume,
difficulty,
effort,
bugs,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use srcgraph_core::{EdgeType, OwnedClassNode, OwnedGraph};
use petgraph::Graph;
fn class(id: &str, eta1: u32, eta2: u32, n1: u32, n2: u32) -> OwnedClassNode {
OwnedClassNode {
id: id.to_owned(),
name: id.to_owned(),
namespace: "test".to_owned(),
line_count: 10,
method_count: 1,
halstead_eta1: eta1,
halstead_eta2: eta2,
halstead_n1: n1,
halstead_n2: n2,
method_connectivity: None,
method_fingerprints: None,
method_tokens: None,
call_sequences: None,
cyclomatic_complexity: None,
path_conditions: None,
invariants: None,
error_messages: None,
magic_numbers: None,
dead_code: None,
tenant_branches: None,
state_transitions: None,
}
}
#[test]
fn halstead_basic_formula() {
let (v, d, e, b, eta, n) = halstead_metrics(10, 7, 27, 15);
assert_eq!(eta, 17);
assert_eq!(n, 42);
assert!((v - 42.0 * 17_f64.log2()).abs() < 1e-9);
assert!((d - (5.0 * 15.0 / 7.0)).abs() < 1e-9);
assert!((e - d * v).abs() < 1e-9);
assert!((b - v / 3000.0).abs() < 1e-9);
}
#[test]
fn halstead_zero_vocabulary_yields_zero_volume() {
let (v, d, e, b, eta, n) = halstead_metrics(0, 0, 0, 0);
assert_eq!(eta, 0);
assert_eq!(n, 0);
assert_eq!(v, 0.0);
assert_eq!(d, 0.0);
assert_eq!(e, 0.0);
assert_eq!(b, 0.0);
}
#[test]
fn halstead_zero_distinct_operands_difficulty_zero() {
let (v, d, ..) = halstead_metrics(3, 0, 5, 0);
assert!(v > 0.0);
assert_eq!(d, 0.0);
}
#[test]
fn halstead_walks_graph() {
let mut g: OwnedGraph = Graph::new();
let a = g.add_node(class("A", 10, 7, 27, 15));
let b = g.add_node(class("B", 0, 0, 0, 0)); g.add_edge(a, b, EdgeType::MethodCall);
let rows = compute_halstead(&g);
assert_eq!(rows.len(), 2);
let ra = rows.iter().find(|r| r.class_id == "A").unwrap();
assert_eq!(ra.eta, 17);
assert_eq!(ra.n, 42);
assert!(ra.volume > 0.0);
let rb = rows.iter().find(|r| r.class_id == "B").unwrap();
assert_eq!(rb.eta, 0);
assert_eq!(rb.volume, 0.0);
assert_eq!(rb.difficulty, 0.0);
}
}