#![allow(clippy::doc_markdown)]
use crate::spaces::SpaceKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MetricScope {
File,
Function,
Container,
}
impl MetricScope {
#[must_use]
pub fn admits(self, kind: SpaceKind) -> bool {
match self {
Self::File => matches!(kind, SpaceKind::Unit),
Self::Function => matches!(kind, SpaceKind::Function),
Self::Container => matches!(
kind,
SpaceKind::Class
| SpaceKind::Struct
| SpaceKind::Trait
| SpaceKind::Impl
| SpaceKind::Namespace
| SpaceKind::Interface
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Direction {
HigherIsWorse,
LowerIsWorse,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct MetricInfo {
pub id: &'static str,
pub family: &'static str,
pub long_description: &'static str,
pub direction: Direction,
pub skip_at_unit: bool,
pub scope: MetricScope,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct MetricRow {
pub name: &'static str,
pub summary: &'static str,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct MetricFamily {
pub name: &'static str,
pub rows: &'static [MetricRow],
}
#[rustfmt::skip]
pub const METRICS: &[MetricInfo] = &[
MetricInfo { id: "cognitive", family: "cognitive", long_description: "Cognitive Complexity exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: true, scope: MetricScope::Function },
MetricInfo { id: "cyclomatic", family: "cyclomatic", long_description: "Cyclomatic Complexity exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: true, scope: MetricScope::Function },
MetricInfo { id: "cyclomatic.modified", family: "cyclomatic", long_description: "Modified Cyclomatic Complexity exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: true, scope: MetricScope::Function },
MetricInfo { id: "halstead.volume", family: "halstead", long_description: "Halstead volume exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "halstead.difficulty", family: "halstead", long_description: "Halstead difficulty exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "halstead.effort", family: "halstead", long_description: "Halstead effort exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "halstead.time", family: "halstead", long_description: "Halstead time-to-program exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "halstead.bugs", family: "halstead", long_description: "Estimated Halstead bugs exceed the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "loc.sloc", family: "loc", long_description: "Source lines of code exceed the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::File },
MetricInfo { id: "loc.ploc", family: "loc", long_description: "Physical lines of code exceed the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::File },
MetricInfo { id: "loc.lloc", family: "loc", long_description: "Logical lines of code exceed the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::File },
MetricInfo { id: "loc.cloc", family: "loc", long_description: "Comment lines of code exceed the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::File },
MetricInfo { id: "loc.blank", family: "loc", long_description: "Blank lines of code exceed the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::File },
MetricInfo { id: "nom", family: "nom", long_description: "Number of methods/functions exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Container },
MetricInfo { id: "tokens", family: "tokens", long_description: "Number of tokens exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "nexits", family: "nexits", long_description: "Number of exit points exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "nargs", family: "nargs", long_description: "Number of function arguments exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "mi.original", family: "mi", long_description: "Maintainability Index falls below the configured threshold.", direction: Direction::LowerIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "mi.sei", family: "mi", long_description: "Maintainability Index (SEI) falls below the configured threshold.", direction: Direction::LowerIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "mi.visual_studio", family: "mi", long_description: "Maintainability Index (Visual Studio) falls below the configured threshold.", direction: Direction::LowerIsWorse, skip_at_unit: false, scope: MetricScope::Function },
MetricInfo { id: "abc", family: "abc", long_description: "ABC magnitude exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: true, scope: MetricScope::Function },
MetricInfo { id: "wmc", family: "wmc", long_description: "Weighted Methods per Class exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Container },
MetricInfo { id: "npm", family: "npm", long_description: "Number of public methods exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Container },
MetricInfo { id: "npa", family: "npa", long_description: "Number of public attributes exceeds the configured threshold.", direction: Direction::HigherIsWorse, skip_at_unit: false, scope: MetricScope::Container },
];
pub const FAMILIES: &[MetricFamily] = &[
MetricFamily {
name: "cognitive",
rows: &[MetricRow {
name: "cognitive",
summary: "Cognitive Complexity: how difficult code is to understand.",
}],
},
MetricFamily {
name: "cyclomatic",
rows: &[MetricRow {
name: "cyclomatic",
summary: "Cyclomatic Complexity: linearly independent paths through the code; the modified variant collapses switch/match/when arms in a single switch statement into one decision point.",
}],
},
MetricFamily {
name: "halstead",
rows: &[MetricRow {
name: "halstead",
summary: "Halstead suite: vocabulary, length, volume, difficulty, effort, time, bugs.",
}],
},
MetricFamily {
name: "loc",
rows: &[
MetricRow {
name: "sloc",
summary: "Source lines of code: total lines in a source file.",
},
MetricRow {
name: "ploc",
summary: "Physical lines of code: instruction lines.",
},
MetricRow {
name: "lloc",
summary: "Logical lines of code: statement count.",
},
MetricRow {
name: "cloc",
summary: "Comment lines of code.",
},
MetricRow {
name: "blank",
summary: "Blank lines.",
},
],
},
MetricFamily {
name: "nom",
rows: &[MetricRow {
name: "nom",
summary: "Number of methods and closures.",
}],
},
MetricFamily {
name: "tokens",
rows: &[MetricRow {
name: "tokens",
summary: "Per-function token count: AST leaves excluding comments.",
}],
},
MetricFamily {
name: "nexits",
rows: &[MetricRow {
name: "nexits",
summary: "Number of exit points from a function or method.",
}],
},
MetricFamily {
name: "nargs",
rows: &[MetricRow {
name: "nargs",
summary: "Number of arguments to a function or method.",
}],
},
MetricFamily {
name: "mi",
rows: &[MetricRow {
name: "mi",
summary: "Maintainability Index suite.",
}],
},
MetricFamily {
name: "abc",
rows: &[MetricRow {
name: "abc",
summary: "ABC: assignments, branches, and conditions.",
}],
},
MetricFamily {
name: "wmc",
rows: &[MetricRow {
name: "wmc",
summary: "Weighted Methods per Class.",
}],
},
MetricFamily {
name: "npm",
rows: &[MetricRow {
name: "npm",
summary: "Number of public methods of a class.",
}],
},
MetricFamily {
name: "npa",
rows: &[MetricRow {
name: "npa",
summary: "Number of public attributes of a class.",
}],
},
];
#[must_use]
pub fn lookup(id: &str) -> Option<&'static MetricInfo> {
METRICS.iter().find(|m| m.id == id)
}
#[must_use]
pub fn lower_is_worse(id: &str) -> bool {
lookup(id).is_some_and(|m| matches!(m.direction, Direction::LowerIsWorse))
}
#[must_use]
pub fn scope(id: &str) -> Option<MetricScope> {
lookup(id).map(|m| m.scope)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn metric_ids_are_unique() {
let mut seen = HashSet::new();
for m in METRICS {
assert!(seen.insert(m.id), "duplicate metric id {:?}", m.id);
}
}
#[test]
fn family_names_are_unique() {
let mut seen = HashSet::new();
for f in FAMILIES {
assert!(seen.insert(f.name), "duplicate family name {:?}", f.name);
}
}
#[test]
fn every_metric_family_is_declared() {
let families: HashSet<&str> = FAMILIES.iter().map(|f| f.name).collect();
for m in METRICS {
assert!(
families.contains(m.family),
"metric {:?} references undeclared family {:?}",
m.id,
m.family,
);
}
}
#[test]
fn every_family_has_a_metric() {
let metric_families: HashSet<&str> = METRICS.iter().map(|m| m.family).collect();
for f in FAMILIES {
assert!(
metric_families.contains(f.name),
"family {:?} has no METRICS entry",
f.name,
);
}
}
#[test]
fn lookup_round_trips_and_rejects_unknown() {
for m in METRICS {
assert_eq!(lookup(m.id).map(|i| i.id), Some(m.id));
}
assert!(lookup("not.a.metric").is_none());
}
#[test]
fn lower_is_worse_iff_mi_family() {
for m in METRICS {
let expect_lower = m.family == "mi";
assert_eq!(
matches!(m.direction, Direction::LowerIsWorse),
expect_lower,
"metric {:?} has the wrong direction",
m.id,
);
}
}
#[test]
fn lower_is_worse_helper_matches_catalog_and_defaults_false() {
assert!(lower_is_worse("mi.original"), "mi.* is lower-is-worse");
assert!(
!lower_is_worse("cyclomatic"),
"cyclomatic is higher-is-worse"
);
assert!(!lower_is_worse("not_a_metric"));
}
#[test]
fn sentence_phrasing_matches_direction() {
for m in METRICS {
match m.direction {
Direction::LowerIsWorse => assert!(
m.long_description.contains("falls below"),
"{:?} should use `falls below`: {:?}",
m.id,
m.long_description,
),
Direction::HigherIsWorse => assert!(
m.long_description.contains("exceed"),
"{:?} should use `exceed(s)`: {:?}",
m.id,
m.long_description,
),
}
}
}
#[test]
fn skip_at_unit_is_the_sum_vs_per_space_divergence_set() {
let mut skip: Vec<&str> = METRICS
.iter()
.filter(|m| m.skip_at_unit)
.map(|m| m.id)
.collect();
skip.sort_unstable();
assert_eq!(
skip,
["abc", "cognitive", "cyclomatic", "cyclomatic.modified"],
"skip_at_unit set drifted from the JSON-aggregate-vs-CLI-accessor \
property; review against the CLI EXTRACTORS accessors before editing",
);
}
#[test]
fn scope_partitions_metrics_by_measured_kind() {
let by_scope = |want: MetricScope| {
let mut ids: Vec<&str> = METRICS
.iter()
.filter(|m| m.scope == want)
.map(|m| m.id)
.collect();
ids.sort_unstable();
ids
};
assert_eq!(
by_scope(MetricScope::File),
["loc.blank", "loc.cloc", "loc.lloc", "loc.ploc", "loc.sloc"],
"only the loc.* size family is File-scoped",
);
assert_eq!(
by_scope(MetricScope::Container),
["nom", "npa", "npm", "wmc"],
"only the OO size metrics are Container-scoped",
);
for id in [
"cognitive",
"cyclomatic",
"halstead.effort",
"nargs",
"nexits",
"abc",
"mi.original",
] {
assert_eq!(
scope(id),
Some(MetricScope::Function),
"{id} should be Function-scoped"
);
}
let counted = by_scope(MetricScope::File).len()
+ by_scope(MetricScope::Function).len()
+ by_scope(MetricScope::Container).len();
assert_eq!(
counted,
METRICS.len(),
"every metric must have exactly one scope"
);
}
#[test]
fn scope_admits_only_its_kinds() {
assert!(MetricScope::File.admits(SpaceKind::Unit));
assert!(!MetricScope::File.admits(SpaceKind::Function));
assert!(!MetricScope::File.admits(SpaceKind::Class));
assert!(MetricScope::Function.admits(SpaceKind::Function));
assert!(!MetricScope::Function.admits(SpaceKind::Unit));
assert!(!MetricScope::Function.admits(SpaceKind::Impl));
for kind in [
SpaceKind::Class,
SpaceKind::Struct,
SpaceKind::Trait,
SpaceKind::Impl,
SpaceKind::Namespace,
SpaceKind::Interface,
] {
assert!(
MetricScope::Container.admits(kind),
"{kind:?} is a container"
);
}
assert!(!MetricScope::Container.admits(SpaceKind::Unit));
assert!(!MetricScope::Container.admits(SpaceKind::Function));
for scope in [
MetricScope::File,
MetricScope::Function,
MetricScope::Container,
] {
assert!(
!scope.admits(SpaceKind::Unknown),
"{scope:?} must not admit Unknown"
);
}
}
}