use std::io::{self, Write};
pub(crate) struct MetricEntry {
pub(crate) name: &'static str,
pub(crate) description: &'static str,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ListMetricsMode {
Names,
Descriptions,
}
pub(crate) const METRICS: &[MetricEntry] = &[
MetricEntry {
name: "cognitive",
description: "Cognitive Complexity: how difficult code is to understand.",
},
MetricEntry {
name: "cyclomatic",
description: "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.",
},
MetricEntry {
name: "halstead",
description: "Halstead suite: vocabulary, length, volume, difficulty, effort, time, bugs.",
},
MetricEntry {
name: "sloc",
description: "Source lines of code: total lines in a source file.",
},
MetricEntry {
name: "ploc",
description: "Physical lines of code: instruction lines.",
},
MetricEntry {
name: "lloc",
description: "Logical lines of code: statement count.",
},
MetricEntry {
name: "cloc",
description: "Comment lines of code.",
},
MetricEntry {
name: "blank",
description: "Blank lines.",
},
MetricEntry {
name: "nom",
description: "Number of methods and closures.",
},
MetricEntry {
name: "tokens",
description: "Per-function token count: AST leaves excluding comments.",
},
MetricEntry {
name: "nexits",
description: "Number of exit points from a function or method.",
},
MetricEntry {
name: "nargs",
description: "Number of arguments to a function or method.",
},
MetricEntry {
name: "mi",
description: "Maintainability Index suite.",
},
MetricEntry {
name: "abc",
description: "ABC: assignments, branches, and conditions.",
},
MetricEntry {
name: "wmc",
description: "Weighted Methods per Class.",
},
MetricEntry {
name: "npm",
description: "Number of public methods of a class.",
},
MetricEntry {
name: "npa",
description: "Number of public attributes of a class.",
},
];
pub(crate) fn write_metrics(out: &mut dyn Write, mode: ListMetricsMode) -> io::Result<()> {
match mode {
ListMetricsMode::Names => {
for m in METRICS {
writeln!(out, "{}", m.name)?;
}
}
ListMetricsMode::Descriptions => {
let width = METRICS
.iter()
.map(|m| m.name.len())
.max()
.expect("METRICS is non-empty");
for m in METRICS {
writeln!(
out,
"{name:<width$} {desc}",
name = m.name,
desc = m.description
)?;
}
}
}
Ok(())
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
mod tests {
use super::*;
#[test]
fn names_unique_and_lowercase() {
let mut seen = std::collections::HashSet::new();
for m in METRICS {
let name = m.name;
assert!(!name.is_empty(), "metric name must be non-empty");
assert!(
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
"metric name {name:?} must be ascii lowercase",
);
assert!(seen.insert(name), "duplicate metric name {name:?}");
assert!(
!m.description.is_empty(),
"metric {name:?} missing description",
);
}
}
#[test]
fn names_mode_prints_one_per_line() {
let mut buf = Vec::new();
write_metrics(&mut buf, ListMetricsMode::Names).expect("write");
let out = String::from_utf8(buf).expect("utf8");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), METRICS.len());
for (line, m) in lines.iter().zip(METRICS.iter()) {
assert_eq!(*line, m.name);
}
}
#[test]
fn descriptions_mode_includes_descriptions() {
let mut buf = Vec::new();
write_metrics(&mut buf, ListMetricsMode::Descriptions).expect("write");
let out = String::from_utf8(buf).expect("utf8");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), METRICS.len());
for (line, m) in lines.iter().zip(METRICS.iter()) {
assert!(
line.starts_with(m.name),
"line {line:?} should start with name"
);
assert!(
line.contains(m.description),
"line {line:?} missing description"
);
}
}
#[test]
fn catalog_covers_library_output() {
use big_code_analysis::CodeMetrics;
use std::collections::HashSet;
let json = serde_json::to_value(CodeMetrics::default()).expect("CodeMetrics serializes");
let mut expected: HashSet<String> = json
.as_object()
.expect("object")
.keys()
.filter(|k| *k != "loc")
.cloned()
.collect();
expected.extend(
["sloc", "ploc", "lloc", "cloc", "blank", "wmc", "npm", "npa"].map(String::from),
);
let catalog: HashSet<&str> = METRICS.iter().map(|m| m.name).collect();
for name in &expected {
assert!(
catalog.contains(name.as_str()),
"catalog missing metric {name:?}; CodeMetrics emits it but --list-metrics does not"
);
}
}
}