use std::fmt;
use std::str::FromStr;
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum Metric {
Cognitive,
Cyclomatic,
Halstead,
Loc,
Nom,
Tokens,
NArgs,
Exit,
Abc,
Npm,
Npa,
Mi,
Wmc,
}
impl Metric {
#[inline]
const fn bit(self) -> u16 {
1 << (self as u32)
}
#[must_use]
pub const fn dependencies(self) -> &'static [Metric] {
match self {
Self::Mi => &[Self::Loc, Self::Cyclomatic, Self::Halstead],
Self::Wmc => &[Self::Cyclomatic, Self::Nom],
_ => &[],
}
}
pub const NAMES: &'static [&'static str] = &[
"abc",
"cognitive",
"cyclomatic",
"halstead",
"loc",
"mi",
"nargs",
"nexits",
"nom",
"npa",
"npm",
"tokens",
"wmc",
];
}
impl fmt::Display for Metric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Cognitive => "cognitive",
Self::Cyclomatic => "cyclomatic",
Self::Halstead => "halstead",
Self::Loc => "loc",
Self::Nom => "nom",
Self::Tokens => "tokens",
Self::NArgs => "nargs",
Self::Exit => "exit",
Self::Abc => "abc",
Self::Npm => "npm",
Self::Npa => "npa",
Self::Mi => "mi",
Self::Wmc => "wmc",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseMetricError(String);
impl fmt::Display for ParseMetricError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown metric: {}", self.0)
}
}
impl std::error::Error for ParseMetricError {}
impl FromStr for Metric {
type Err = ParseMetricError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cognitive" => Ok(Self::Cognitive),
"cyclomatic" => Ok(Self::Cyclomatic),
"halstead" => Ok(Self::Halstead),
"loc" => Ok(Self::Loc),
"nom" => Ok(Self::Nom),
"tokens" => Ok(Self::Tokens),
"nargs" => Ok(Self::NArgs),
"exit" | "nexits" => Ok(Self::Exit),
"abc" => Ok(Self::Abc),
"npm" => Ok(Self::Npm),
"npa" => Ok(Self::Npa),
"mi" => Ok(Self::Mi),
"wmc" => Ok(Self::Wmc),
_ => Err(ParseMetricError(s.to_owned())),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct MetricSet(u16);
impl MetricSet {
const ALL_BITS: u16 = Metric::Cognitive.bit()
| Metric::Cyclomatic.bit()
| Metric::Halstead.bit()
| Metric::Loc.bit()
| Metric::Nom.bit()
| Metric::Tokens.bit()
| Metric::NArgs.bit()
| Metric::Exit.bit()
| Metric::Abc.bit()
| Metric::Npm.bit()
| Metric::Npa.bit()
| Metric::Mi.bit()
| Metric::Wmc.bit();
#[inline]
#[must_use]
pub const fn empty() -> Self {
Self(0)
}
#[inline]
#[must_use]
pub const fn all() -> Self {
Self(Self::ALL_BITS)
}
#[inline]
#[must_use]
pub const fn contains(self, metric: Metric) -> bool {
(self.0 & metric.bit()) != 0
}
#[inline]
#[must_use]
pub const fn with(self, metric: Metric) -> Self {
Self(self.0 | metric.bit())
}
#[inline]
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[inline]
pub fn insert(&mut self, metric: Metric) {
self.0 |= metric.bit();
}
#[must_use]
pub fn from_slice_with_deps(metrics: &[Metric]) -> Self {
let mut set = Self::empty();
let mut worklist: Vec<Metric> = metrics.to_vec();
while let Some(m) = worklist.pop() {
if set.contains(m) {
continue;
}
set.insert(m);
for &dep in m.dependencies() {
if !set.contains(dep) {
worklist.push(dep);
}
}
}
set
}
}
impl Default for MetricSet {
#[inline]
fn default() -> Self {
Self::all()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_contains_nothing() {
let set = MetricSet::empty();
assert!(!set.contains(Metric::Loc));
assert!(!set.contains(Metric::Halstead));
assert!(!set.contains(Metric::Mi));
}
#[test]
fn all_contains_every_variant() {
let set = MetricSet::all();
for m in [
Metric::Cognitive,
Metric::Cyclomatic,
Metric::Halstead,
Metric::Loc,
Metric::Nom,
Metric::Tokens,
Metric::NArgs,
Metric::Exit,
Metric::Abc,
Metric::Npm,
Metric::Npa,
Metric::Mi,
Metric::Wmc,
] {
assert!(set.contains(m), "MetricSet::all() must contain {m}");
}
}
#[test]
fn with_dependencies_pulls_in_mi_inputs() {
let set = MetricSet::from_slice_with_deps(&[Metric::Mi]);
assert!(set.contains(Metric::Mi));
assert!(set.contains(Metric::Loc), "Mi depends on Loc");
assert!(set.contains(Metric::Cyclomatic), "Mi depends on Cyclomatic");
assert!(set.contains(Metric::Halstead), "Mi depends on Halstead");
assert!(!set.contains(Metric::Abc));
assert!(!set.contains(Metric::Tokens));
}
#[test]
fn with_dependencies_pulls_in_wmc_inputs() {
let set = MetricSet::from_slice_with_deps(&[Metric::Wmc]);
assert!(set.contains(Metric::Wmc));
assert!(
set.contains(Metric::Cyclomatic),
"Wmc depends on Cyclomatic"
);
assert!(set.contains(Metric::Nom), "Wmc depends on Nom");
}
#[test]
fn closure_is_idempotent_for_mixed_input() {
let a = MetricSet::from_slice_with_deps(&[Metric::Mi, Metric::Loc]);
let b = MetricSet::from_slice_with_deps(&[Metric::Mi]);
assert_eq!(a, b);
}
#[test]
fn closure_handles_duplicate_input() {
let set = MetricSet::from_slice_with_deps(&[Metric::Mi, Metric::Mi, Metric::Mi]);
assert_eq!(set, MetricSet::from_slice_with_deps(&[Metric::Mi]));
}
#[test]
fn empty_slice_yields_empty_set() {
assert_eq!(MetricSet::from_slice_with_deps(&[]), MetricSet::empty());
}
const ALL_VARIANTS: &[Metric] = &[
Metric::Cognitive,
Metric::Cyclomatic,
Metric::Halstead,
Metric::Loc,
Metric::Nom,
Metric::Tokens,
Metric::NArgs,
Metric::Exit,
Metric::Abc,
Metric::Npm,
Metric::Npa,
Metric::Mi,
Metric::Wmc,
];
#[test]
fn from_str_round_trips_every_variant_display_name() {
for &m in ALL_VARIANTS {
let parsed: Metric = m
.to_string()
.parse()
.unwrap_or_else(|e| panic!("Display->FromStr round-trip failed for {m}: {e}"));
assert_eq!(parsed, m, "round-trip mismatch for {m}");
}
}
#[test]
fn from_str_accepts_nexits_alias_for_exit() {
assert_eq!("exit".parse::<Metric>().unwrap(), Metric::Exit);
assert_eq!("nexits".parse::<Metric>().unwrap(), Metric::Exit);
}
#[test]
fn from_str_rejects_uppercase() {
let err = "Loc".parse::<Metric>().unwrap_err();
assert_eq!(err.to_string(), "unknown metric: Loc");
}
#[test]
fn names_table_parses_to_every_variant() {
use std::collections::HashSet;
let mut seen: HashSet<Metric> = HashSet::new();
for name in Metric::NAMES {
let parsed = name.parse::<Metric>().unwrap_or_else(|_| {
panic!("Metric::NAMES contains {name:?} but FromStr rejects it")
});
seen.insert(parsed);
}
for &m in ALL_VARIANTS {
assert!(
seen.contains(&m),
"Metric::{m:?} is not represented in Metric::NAMES; \
add the canonical spelling to the table",
);
}
}
#[test]
fn names_table_is_alphabetised() {
let mut sorted: Vec<&str> = Metric::NAMES.to_vec();
sorted.sort_unstable();
assert_eq!(
Metric::NAMES,
sorted.as_slice(),
"Metric::NAMES must stay alphabetised",
);
}
#[test]
fn with_metric_set_does_not_resolve_dependencies() {
let resolved = MetricSet::from_slice_with_deps(&[Metric::Mi]);
assert!(resolved.contains(Metric::Mi));
assert!(resolved.contains(Metric::Loc));
assert!(resolved.contains(Metric::Cyclomatic));
assert!(resolved.contains(Metric::Halstead));
let bare = MetricSet::empty().with(Metric::Mi);
assert!(bare.contains(Metric::Mi));
assert!(!bare.contains(Metric::Loc), "with(Mi) must NOT pull Loc");
assert!(
!bare.contains(Metric::Cyclomatic),
"with(Mi) must NOT pull Cyclomatic",
);
assert!(
!bare.contains(Metric::Halstead),
"with(Mi) must NOT pull Halstead",
);
}
#[test]
fn from_str_rejects_unknown_name() {
let err = "bogus".parse::<Metric>().unwrap_err();
assert_eq!(err.to_string(), "unknown metric: bogus");
}
#[test]
fn distinct_bits_per_variant() {
let mut seen: u16 = 0;
for m in [
Metric::Cognitive,
Metric::Cyclomatic,
Metric::Halstead,
Metric::Loc,
Metric::Nom,
Metric::Tokens,
Metric::NArgs,
Metric::Exit,
Metric::Abc,
Metric::Npm,
Metric::Npa,
Metric::Mi,
Metric::Wmc,
] {
let bit = m.bit();
assert_eq!(seen & bit, 0, "duplicate bit for {m}: {bit:#b}");
seen |= bit;
}
assert_eq!(seen, MetricSet::ALL_BITS);
}
}