#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use std::collections::HashMap;
use serde::Serialize;
use serde::ser::SerializeStruct;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::langs::LANG;
use crate::metric_set::{Metric, MetricSet};
use crate::preproc::PreprocResults;
use crate::checker::Checker;
use crate::error::MetricsError;
use crate::node::Node;
use crate::suppression::{
Suppression, SuppressionKind, SuppressionScope, parse_marker as parse_suppression_marker,
};
use crate::abc::{self, Abc};
use crate::cognitive::{self, Cognitive};
use crate::cyclomatic::{self, Cyclomatic};
use crate::exit::{self, Exit};
use crate::getter::Getter;
use crate::halstead::{self, Halstead, HalsteadMaps};
use crate::loc::{self, Loc};
use crate::mi::{self, Mi};
use crate::nargs::{self, NArgs};
use crate::nom::{self, Nom};
use crate::npa::{self, Npa};
use crate::npm::{self, Npm};
use crate::tokens::{self, Tokens};
use crate::wmc::{self, Wmc};
use crate::output::dump_metrics::*;
use crate::traits::*;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SpaceKind {
#[default]
Unknown,
Function,
Class,
Struct,
Trait,
Impl,
Unit,
Namespace,
Interface,
}
impl fmt::Display for SpaceKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
SpaceKind::Unknown => "unknown",
SpaceKind::Function => "function",
SpaceKind::Class => "class",
SpaceKind::Struct => "struct",
SpaceKind::Trait => "trait",
SpaceKind::Impl => "impl",
SpaceKind::Unit => "unit",
SpaceKind::Namespace => "namespace",
SpaceKind::Interface => "interface",
};
write!(f, "{s}")
}
}
#[derive(Default, Debug, Clone)]
pub struct CodeMetrics {
pub nargs: nargs::Stats,
pub nexits: exit::Stats,
pub cognitive: cognitive::Stats,
pub cyclomatic: cyclomatic::Stats,
pub halstead: halstead::Stats,
pub loc: loc::Stats,
pub nom: nom::Stats,
pub tokens: tokens::Stats,
pub mi: mi::Stats,
pub abc: abc::Stats,
pub wmc: wmc::Stats,
pub npm: npm::Stats,
pub npa: npa::Stats,
pub selected: MetricSet,
}
impl Serialize for CodeMetrics {
#[allow(clippy::similar_names)] fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let sel = self.selected;
let emit_wmc = sel.contains(Metric::Wmc) && !self.wmc.is_disabled();
let emit_npm = sel.contains(Metric::Npm) && !self.npm.is_disabled();
let emit_npa = sel.contains(Metric::Npa) && !self.npa.is_disabled();
let always_on = [
Metric::NArgs,
Metric::Exit,
Metric::Cognitive,
Metric::Cyclomatic,
Metric::Halstead,
Metric::Loc,
Metric::Nom,
Metric::Tokens,
Metric::Mi,
Metric::Abc,
];
let field_count = always_on.iter().filter(|m| sel.contains(**m)).count()
+ usize::from(emit_wmc)
+ usize::from(emit_npm)
+ usize::from(emit_npa);
let mut st = serializer.serialize_struct("CodeMetrics", field_count)?;
macro_rules! emit_if {
($cond:expr, $key:literal, $field:expr) => {
if $cond {
st.serialize_field($key, $field)?;
}
};
}
emit_if!(sel.contains(Metric::NArgs), "nargs", &self.nargs);
emit_if!(sel.contains(Metric::Exit), "nexits", &self.nexits);
emit_if!(
sel.contains(Metric::Cognitive),
"cognitive",
&self.cognitive
);
emit_if!(
sel.contains(Metric::Cyclomatic),
"cyclomatic",
&self.cyclomatic
);
emit_if!(sel.contains(Metric::Halstead), "halstead", &self.halstead);
emit_if!(sel.contains(Metric::Loc), "loc", &self.loc);
emit_if!(sel.contains(Metric::Nom), "nom", &self.nom);
emit_if!(sel.contains(Metric::Tokens), "tokens", &self.tokens);
emit_if!(sel.contains(Metric::Mi), "mi", &self.mi);
emit_if!(sel.contains(Metric::Abc), "abc", &self.abc);
emit_if!(emit_wmc, "wmc", &self.wmc);
emit_if!(emit_npm, "npm", &self.npm);
emit_if!(emit_npa, "npa", &self.npa);
st.end()
}
}
impl CodeMetrics {
#[inline]
#[must_use]
pub fn with_selected(selected: MetricSet) -> Self {
Self {
selected,
..Self::default()
}
}
#[inline]
#[must_use]
pub fn selected(&self) -> MetricSet {
self.selected
}
}
impl fmt::Display for CodeMetrics {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{}", self.nargs)?;
writeln!(f, "{}", self.nexits)?;
writeln!(f, "{}", self.cognitive)?;
writeln!(f, "{}", self.cyclomatic)?;
writeln!(f, "{}", self.halstead)?;
writeln!(f, "{}", self.loc)?;
writeln!(f, "{}", self.nom)?;
writeln!(f, "{}", self.tokens)?;
write!(f, "{}", self.mi)
}
}
impl CodeMetrics {
pub fn merge(&mut self, other: &CodeMetrics) {
self.cognitive.merge(&other.cognitive);
self.cyclomatic.merge(&other.cyclomatic);
self.halstead.merge(&other.halstead);
self.loc.merge(&other.loc);
self.nom.merge(&other.nom);
self.tokens.merge(&other.tokens);
self.mi.merge(&other.mi);
self.nargs.merge(&other.nargs);
self.nexits.merge(&other.nexits);
self.abc.merge(&other.abc);
self.wmc.merge(&other.wmc);
self.npm.merge(&other.npm);
self.npa.merge(&other.npa);
self.selected = self.selected.union(other.selected);
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FuncSpace {
pub name: Option<String>,
pub start_line: usize,
pub end_line: usize,
pub kind: SpaceKind,
pub spaces: Vec<FuncSpace>,
pub metrics: CodeMetrics,
#[serde(default, skip_serializing_if = "SuppressionScope::is_empty")]
pub suppressed: SuppressionScope,
}
impl FuncSpace {
fn new<T: Getter>(node: &Node, code: &[u8], kind: SpaceKind, selected: MetricSet) -> Self {
let (start_position, end_position) = match kind {
SpaceKind::Unit => {
if node.child_count() == 0 {
(0, 0)
} else {
(node.start_row() + 1, node.end_row())
}
}
_ => (node.start_row() + 1, node.end_row() + 1),
};
let name = (kind != SpaceKind::Unit)
.then(|| {
T::get_func_space_name(node, code)
.map(|name| name.split_whitespace().collect::<Vec<_>>().join(" "))
})
.flatten();
Self {
name,
spaces: Vec::new(),
metrics: CodeMetrics::with_selected(selected),
kind,
start_line: start_position,
end_line: end_position,
suppressed: SuppressionScope::default(),
}
}
}
#[inline]
fn compute_halstead_mi_and_wmc<T: ParserTrait>(state: &mut State, selected: MetricSet) {
if selected.contains(Metric::Halstead) {
state
.halstead_maps
.finalize(&mut state.space.metrics.halstead);
}
if selected.contains(Metric::Mi) {
T::Mi::compute(
&state.space.metrics.loc,
&state.space.metrics.cyclomatic,
&state.space.metrics.halstead,
&mut state.space.metrics.mi,
);
}
if selected.contains(Metric::Wmc) {
T::Wmc::compute(
state.space.kind,
&state.space.metrics.cyclomatic,
&mut state.space.metrics.wmc,
);
}
}
#[inline]
fn compute_averages(state: &mut State, selected: MetricSet) {
let nom_functions = state.space.metrics.nom.functions_sum() as usize;
let nom_closures = state.space.metrics.nom.closures_sum() as usize;
let nom_total = state.space.metrics.nom.total() as usize;
if selected.contains(Metric::Cognitive) {
state.space.metrics.cognitive.finalize(nom_total);
}
if selected.contains(Metric::Exit) {
state.space.metrics.nexits.finalize(nom_total);
}
if selected.contains(Metric::NArgs) {
state
.space
.metrics
.nargs
.finalize(nom_functions, nom_closures);
}
}
#[inline]
fn compute_minmax(state: &mut State, selected: MetricSet) {
if selected.contains(Metric::Cyclomatic) {
state.space.metrics.cyclomatic.compute_minmax();
}
if selected.contains(Metric::Exit) {
state.space.metrics.nexits.compute_minmax();
}
if selected.contains(Metric::Cognitive) {
state.space.metrics.cognitive.compute_minmax();
}
if selected.contains(Metric::NArgs) {
state.space.metrics.nargs.compute_minmax();
}
if selected.contains(Metric::Nom) {
state.space.metrics.nom.compute_minmax();
}
if selected.contains(Metric::Loc) {
state.space.metrics.loc.compute_minmax();
}
if selected.contains(Metric::Abc) {
state.space.metrics.abc.compute_minmax();
}
if selected.contains(Metric::Tokens) {
state.space.metrics.tokens.compute_minmax();
}
}
#[inline]
fn compute_sum(state: &mut State, selected: MetricSet) {
if selected.contains(Metric::Wmc) {
state.space.metrics.wmc.compute_sum();
}
if selected.contains(Metric::Npm) {
state.space.metrics.npm.compute_sum();
}
if selected.contains(Metric::Npa) {
state.space.metrics.npa.compute_sum();
}
}
fn finalize<T: ParserTrait>(state_stack: &mut Vec<State>, diff_level: usize, selected: MetricSet) {
if state_stack.is_empty() {
return;
}
for _ in 0..diff_level {
if state_stack.len() == 1 {
let last_state = state_stack
.last_mut()
.expect("invariant: state_stack has exactly one element");
compute_minmax(last_state, selected);
compute_sum(last_state, selected);
compute_halstead_mi_and_wmc::<T>(last_state, selected);
compute_averages(last_state, selected);
break;
}
let mut state = state_stack
.pop()
.expect("invariant: state_stack has more than one element");
compute_minmax(&mut state, selected);
compute_sum(&mut state, selected);
compute_halstead_mi_and_wmc::<T>(&mut state, selected);
compute_averages(&mut state, selected);
let last_state = state_stack
.last_mut()
.expect("invariant: state_stack has remaining elements after pop");
last_state.halstead_maps.merge(&state.halstead_maps);
compute_halstead_mi_and_wmc::<T>(last_state, selected);
last_state.space.metrics.merge(&state.space.metrics);
last_state.space.spaces.push(state.space);
}
}
#[derive(Debug, Clone)]
struct State<'a> {
space: FuncSpace,
halstead_maps: HalsteadMaps<'a>,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct Source<'a> {
pub lang: LANG,
pub code: &'a [u8],
pub name: Option<String>,
pub preproc_path: Option<&'a Path>,
pub preproc: Option<Arc<PreprocResults>>,
}
impl<'a> Source<'a> {
#[inline]
#[must_use]
pub fn new(lang: LANG, code: &'a [u8]) -> Self {
Self {
lang,
code,
name: None,
preproc_path: None,
preproc: None,
}
}
#[inline]
#[must_use]
pub fn with_name(mut self, name: Option<String>) -> Self {
self.name = name;
self
}
#[inline]
#[must_use]
pub fn with_preproc_path(mut self, preproc_path: Option<&'a Path>) -> Self {
self.preproc_path = preproc_path;
self
}
#[inline]
#[must_use]
pub fn with_preproc(mut self, preproc: Option<Arc<PreprocResults>>) -> Self {
self.preproc = preproc;
self
}
}
pub struct Ast {
inner: crate::langs::AstInner,
name: Option<String>,
}
impl fmt::Debug for Ast {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Ast")
.field("language", &self.language())
.field("name", &self.name)
.finish_non_exhaustive()
}
}
impl Ast {
pub fn parse(source: Source<'_>) -> Result<Self, MetricsError> {
let Source {
lang,
code,
name,
preproc_path,
preproc,
} = source;
let inner = crate::langs::ast_parse_dispatch(lang, code, preproc_path, preproc)?;
Ok(Self { inner, name })
}
pub fn from_tree_sitter(
lang: LANG,
tree: tree_sitter::Tree,
code: Vec<u8>,
name: Option<String>,
) -> Result<Self, MetricsError> {
let inner = crate::langs::ast_from_tree_dispatch(lang, tree, code)?;
Ok(Self { inner, name })
}
pub fn metrics(&self, options: MetricsOptions) -> Result<FuncSpace, MetricsError> {
self.inner.run_metrics(self.name.clone(), options)
}
#[must_use]
#[inline]
pub fn language(&self) -> LANG {
self.inner.language()
}
#[must_use]
#[inline]
pub fn source(&self) -> &[u8] {
self.inner.code_bytes()
}
#[must_use]
#[inline]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
#[inline]
pub fn as_tree_sitter(&self) -> &tree_sitter::Tree {
self.inner.ts_tree()
}
}
pub fn analyze(source: Source<'_>, options: MetricsOptions) -> Result<FuncSpace, MetricsError> {
Ast::parse(source)?.metrics(options)
}
#[deprecated(
since = "0.0.26",
note = "Use `analyze(Source::new(lang, code).with_name(Some(name)), MetricsOptions::default())` instead — the path-positional shim derives the top-level FuncSpace name via lossy UTF-8 conversion."
)]
#[doc(hidden)]
pub fn metrics<'a, T: ParserTrait>(
parser: &'a T,
path: &'a Path,
) -> Result<FuncSpace, MetricsError> {
#[allow(deprecated)]
metrics_with_options(parser, path, MetricsOptions::default())
}
#[deprecated(
since = "0.0.26",
note = "Use `analyze(Source::new(lang, code).with_name(Some(name)), options)` instead — the path-positional shim derives the top-level FuncSpace name via lossy UTF-8 conversion and will be removed in a future release."
)]
#[doc(hidden)]
pub fn metrics_with_options<'a, T: ParserTrait>(
parser: &'a T,
path: &'a Path,
options: MetricsOptions,
) -> Result<FuncSpace, MetricsError> {
metrics_inner(parser, Some(path.to_string_lossy().into_owned()), options)
}
#[inline]
fn compute_per_node<'a, T: ParserTrait>(
state: &mut State<'a>,
node: &Node<'a>,
code: &'a [u8],
selected: MetricSet,
func_space: bool,
unit: bool,
nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
) {
let last = &mut state.space;
if selected.contains(Metric::Cognitive) {
T::Cognitive::compute(node, code, &mut last.metrics.cognitive, nesting_map);
}
if selected.contains(Metric::Cyclomatic) {
T::Cyclomatic::compute(node, code, &mut last.metrics.cyclomatic);
}
if selected.contains(Metric::Halstead) {
T::Halstead::compute(node, code, &mut state.halstead_maps);
}
if selected.contains(Metric::Loc) {
T::Loc::compute(node, &mut last.metrics.loc, func_space, unit);
}
if selected.contains(Metric::Nom) {
T::Nom::compute(node, &mut last.metrics.nom);
}
if selected.contains(Metric::Tokens) {
T::Tokens::compute(node, &mut last.metrics.tokens);
}
if selected.contains(Metric::NArgs) {
T::NArgs::compute(node, &mut last.metrics.nargs);
}
if selected.contains(Metric::Exit) {
T::Exit::compute(node, code, &mut last.metrics.nexits);
}
if selected.contains(Metric::Abc) {
T::Abc::compute(node, code, &mut last.metrics.abc);
}
if selected.contains(Metric::Npm) {
T::Npm::compute(node, code, &mut last.metrics.npm);
}
if selected.contains(Metric::Npa) {
T::Npa::compute(node, code, &mut last.metrics.npa);
}
}
pub(crate) fn metrics_inner<T: ParserTrait>(
parser: &T,
name: Option<String>,
options: MetricsOptions,
) -> Result<FuncSpace, MetricsError> {
let diagnostic_path = name.as_deref().unwrap_or("<input>");
let selected = options.metrics;
let code = parser.get_code();
let node = parser.get_root();
let mut cursor = node.cursor();
let mut stack = Vec::new();
let mut children = Vec::new();
let mut state_stack: Vec<State> = Vec::new();
let mut last_level = 0;
let mut nesting_map = HashMap::<usize, (usize, usize, usize)>::default();
nesting_map.insert(node.id(), (0, 0, 0));
if T::Getter::get_space_kind_with_code(&node, code) != SpaceKind::Unit {
let mut synthetic = FuncSpace::new::<T::Getter>(&node, code, SpaceKind::Unit, selected);
synthetic
.metrics
.loc
.init_unit_span(node.start_row(), node.end_row());
state_stack.push(State {
space: synthetic,
halstead_maps: HalsteadMaps::new(),
});
}
stack.push((node, 0));
while let Some((node, level)) = stack.pop() {
if options.exclude_tests && T::Checker::should_skip_subtree(&node, code) {
continue;
}
if level < last_level {
finalize::<T>(&mut state_stack, last_level - level, selected);
last_level = level;
}
let kind = T::Getter::get_space_kind_with_code(&node, code);
let func_space = T::Checker::promotes_to_func_space_with_code(&node, code);
let unit = kind == SpaceKind::Unit;
let new_level = if func_space {
let state = State {
space: FuncSpace::new::<T::Getter>(&node, code, kind, selected),
halstead_maps: HalsteadMaps::new(),
};
state_stack.push(state);
last_level = level + 1;
last_level
} else {
level
};
if T::Checker::is_comment(&node)
&& let Some(text) = node.utf8_text(code)
{
match parse_suppression_marker(text) {
Ok(Some(s)) => apply_suppression(&mut state_stack, &s),
Ok(None) => {}
Err(e) => {
eprintln!("warning: {}:{}: {e}", diagnostic_path, node.start_row() + 1);
}
}
}
if let Some(state) = state_stack.last_mut() {
compute_per_node::<T>(
state,
&node,
code,
selected,
func_space,
unit,
&mut nesting_map,
);
}
cursor.reset(&node);
if cursor.goto_first_child() {
loop {
children.push((cursor.node(), new_level));
if !cursor.goto_next_sibling() {
break;
}
}
for child in children.drain(..).rev() {
stack.push(child);
}
}
}
finalize::<T>(&mut state_stack, usize::MAX, selected);
let mut state = state_stack.pop().ok_or(MetricsError::EmptyRoot)?;
state.space.name = name;
Ok(state.space)
}
fn apply_suppression(state_stack: &mut [State], suppression: &Suppression) {
let target = match suppression.kind {
SuppressionKind::File => state_stack
.iter_mut()
.find(|s| matches!(s.space.kind, SpaceKind::Unit)),
SuppressionKind::Function => state_stack
.iter_mut()
.rev()
.find(|s| matches!(s.space.kind, SpaceKind::Function)),
};
if let Some(state) = target {
state.space.suppressed.merge(&suppression.scope);
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct MetricsOptions {
pub exclude_tests: bool,
pub metrics: MetricSet,
}
impl MetricsOptions {
#[inline]
#[must_use]
pub fn with_exclude_tests(mut self, exclude_tests: bool) -> Self {
self.exclude_tests = exclude_tests;
self
}
#[inline]
#[must_use]
pub fn with_only(mut self, metrics: &[Metric]) -> Self {
self.metrics = MetricSet::from_slice_with_deps(metrics);
self
}
#[inline]
#[must_use]
pub fn with_metric_set(mut self, metrics: MetricSet) -> Self {
self.metrics = metrics;
self
}
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct MetricsCfg {
pub path: PathBuf,
pub options: MetricsOptions,
}
impl MetricsCfg {
#[inline]
#[must_use]
pub fn new(path: PathBuf) -> Self {
Self {
path,
..Default::default()
}
}
#[inline]
#[must_use]
pub fn with_options(mut self, options: MetricsOptions) -> Self {
self.options = options;
self
}
}
pub struct Metrics {
_guard: (),
}
impl Callback for Metrics {
type Res = std::io::Result<()>;
type Cfg = MetricsCfg;
fn call<T: ParserTrait>(cfg: Self::Cfg, parser: &T) -> Self::Res {
let name = Some(cfg.path.to_string_lossy().into_owned());
match metrics_inner(parser, name, cfg.options) {
Ok(space) => dump_root(&space),
Err(_) => Ok(()),
}
}
}
#[cfg(test)]
#[allow(deprecated)]
#[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 crate::MetricsOptions;
use crate::metrics;
use crate::{CppParser, ParserTrait, SpaceKind, check_func_space};
#[test]
fn cpp_function_definition_is_classified_as_function() {
use crate::Cpp;
use crate::checker::Checker;
use crate::getter::Getter;
use crate::langs::CppCode;
use crate::traits::Search;
let source = "int the_func(int x) { return x; }\n";
let path = std::path::PathBuf::from("fd.cc");
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
let root = parser.get_root();
let fn_node = root
.first_occurrence(|id| {
Cpp::FunctionDefinition == id
|| Cpp::FunctionDefinition2 == id
|| Cpp::FunctionDefinition3 == id
|| Cpp::FunctionDefinition4 == id
})
.expect("parse must produce a function_definition node");
assert!(
CppCode::is_func(&fn_node),
"is_func must return true for a function_definition"
);
assert!(
CppCode::is_func_space(&fn_node),
"is_func_space must return true for a function_definition"
);
assert_eq!(
CppCode::get_space_kind(&fn_node),
SpaceKind::Function,
"get_space_kind must classify function_definition as Function"
);
assert_eq!(
CppCode::get_func_space_name(&fn_node, source.as_bytes()),
Some("the_func"),
"get_func_space_name must extract the declarator identifier"
);
}
#[test]
fn c_scope_resolution_operator() {
check_func_space::<CppParser, _>(
"void Foo::bar(){
return;
}",
"foo.c",
|func_space| {
insta::assert_json_snapshot!(
func_space.spaces[0].name,
@r###""Foo::bar""###
);
},
);
}
#[test]
fn cpp_error_root_yields_unit_top_level_space() {
let source = "#ifndef A\n\
namespace a { namespace b { namespace c {\n\
template <class S, class V> class C : publi\n";
let path = std::path::PathBuf::from("error_root.cc");
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
assert!(
parser.get_root().0.is_error(),
"test premise broken: grammar must yield ERROR root for this snippet"
);
let space = metrics(&parser, &path).unwrap();
assert_eq!(
space.kind,
SpaceKind::Unit,
"top-level FuncSpace must be Unit, not {:?}",
space.kind
);
let loc = &space.metrics.loc;
let sloc = loc.sloc();
let ploc = loc.ploc();
let blank = loc.blank();
let line_count = source.lines().count();
assert!(
sloc >= ploc,
"sloc ({sloc}) must be >= ploc ({ploc}) for the file-level space"
);
assert!(blank >= 0.0, "blank ({blank}) must be >= 0");
assert_eq!(
sloc as usize, line_count,
"sloc ({sloc}) should match the file's line count ({line_count})"
);
}
fn assert_top_level_space_is_unit_contract<P: ParserTrait>(source: &str, filename: &str) {
let path = std::path::PathBuf::from(filename);
let parser = P::new(source.as_bytes().to_vec(), &path, None);
let space = metrics(&parser, &path).expect("metrics must yield a top-level space");
assert_eq!(
space.kind,
SpaceKind::Unit,
"top-level FuncSpace for {filename:?} must be Unit, not {:?}",
space.kind
);
let loc = &space.metrics.loc;
let sloc = loc.sloc();
let ploc = loc.ploc();
let blank = loc.blank();
assert!(
sloc >= ploc,
"sloc ({sloc}) must be >= ploc ({ploc}) for the file-level space of {filename:?}",
);
assert!(
blank >= 0.0,
"blank ({blank}) must be >= 0 for the file-level space of {filename:?}",
);
}
fn assert_partial_input_yields_synthetic_unit_wrapper<P: ParserTrait>(
source: &str,
filename: &str,
) {
let path = std::path::PathBuf::from(filename);
let parser = P::new(source.as_bytes().to_vec(), &path, None);
assert!(
parser.get_root().0.is_error(),
"test premise broken: grammar must yield ERROR root for {filename:?}",
);
assert_top_level_space_is_unit_contract::<P>(source, filename);
}
#[test]
fn python_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::PythonParser>(
"def foo(x):\n return x +\n",
"partial.py",
);
}
#[test]
fn javascript_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::JavascriptParser>(
"function foo(x) {\n return x +\n",
"partial.js",
);
}
#[test]
fn mozjs_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::MozjsParser>(
"function foo(x) {\n return x +\n",
"partial.js",
);
}
#[test]
fn typescript_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::TypescriptParser>(
"function foo(x: number): number {\n return x +\n",
"partial.ts",
);
}
#[test]
fn tsx_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::TsxParser>(
"function Foo(x: number): JSX.Element {\n return <div>{x +\n",
"partial.tsx",
);
}
#[test]
fn java_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::JavaParser>(
"class Foo {\n void bar(int x) {\n return x +\n",
"Partial.java",
);
}
#[test]
fn kotlin_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::KotlinParser>(
"class Foo {\n fun bar(x: Int): Int {\n return x +\n",
"Partial.kt",
);
}
#[test]
fn go_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::GoParser>(
"package main\nfunc foo(x int) int {\n return x +\n",
"partial.go",
);
}
#[test]
fn rust_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::RustParser>(
"fn foo(x: i32) -> i32 {\n return x +\n",
"partial.rs",
);
}
#[test]
fn csharp_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::CsharpParser>(
"class Foo {\n void Bar(int x) {\n return x +\n",
"Partial.cs",
);
}
#[test]
fn bash_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::BashParser>(
"function foo() {\n echo \"x +\n",
"partial.sh",
);
}
#[test]
fn lua_partial_input_yields_synthetic_unit_wrapper() {
assert_partial_input_yields_synthetic_unit_wrapper::<crate::LuaParser>(
"function foo(x)\n return x +\n",
"partial.lua",
);
}
#[test]
fn tcl_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::TclParser>(
"proc foo {x} {\n return [expr {$x +\n",
"partial.tcl",
);
}
#[test]
fn perl_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::PerlParser>(
"sub foo {\n my $x = shift;\n return $x +\n",
"partial.pl",
);
}
#[test]
fn php_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::PhpParser>(
"<?php\nfunction foo($x) {\n return $x +\n",
"partial.php",
);
}
#[test]
fn elixir_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::ElixirParser>(
"defmodule Foo do\n def bar(x) do\n x +\n",
"partial.ex",
);
}
#[test]
fn elixir_func_space_names_resolve_through_arguments_wrapper() {
let src = "defmodule Foo.Bar do\n def hello(x), do: x\n defp helper, do: :ok\n defmodule Inner do\n def i, do: 1\n end\nend\n";
let path = std::path::PathBuf::from("foo.ex");
let parser = crate::ElixirParser::new(src.as_bytes().to_vec(), &path, None);
let space = metrics(&parser, &path).expect("metrics must yield a top-level space");
assert_eq!(space.name.as_deref(), Some("foo.ex"));
let outer = space.spaces.first().expect("outer class space");
assert_eq!(outer.kind, SpaceKind::Class);
assert_eq!(outer.name.as_deref(), Some("Foo.Bar"));
let names: Vec<&str> = outer
.spaces
.iter()
.map(|s| s.name.as_deref().unwrap_or("?"))
.collect();
assert_eq!(names, vec!["hello", "helper", "Inner"]);
let inner = outer
.spaces
.iter()
.find(|s| s.kind == SpaceKind::Class)
.expect("nested class");
let inner_names: Vec<&str> = inner
.spaces
.iter()
.map(|s| s.name.as_deref().unwrap_or("?"))
.collect();
assert_eq!(inner_names, vec!["i"]);
}
#[test]
fn preproc_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::PreprocParser>(
"#ifdef FOO\n#define BAR(x) (x +\n",
"partial.h",
);
}
#[test]
fn ccomment_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::CcommentParser>(
"/* unterminated comment\n spanning several\n",
"partial.c",
);
}
#[test]
fn ruby_top_level_space_is_unit_contract() {
assert_top_level_space_is_unit_contract::<crate::RubyParser>(
"class Foo\n def bar(\n x\n ",
"partial.rb",
);
}
#[cfg(unix)]
#[test]
fn non_utf8_path_yields_lossy_top_level_name() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
let raw_bytes: &[u8] = b"foo_\xFF\xFE_bar.rs";
let path = PathBuf::from(OsStr::from_bytes(raw_bytes));
assert!(
path.to_str().is_none(),
"test premise broken: path must be non-UTF-8 for this test to be meaningful"
);
let source = "int a = 42;";
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
#[allow(deprecated)]
let space = metrics(&parser, &path).expect("metrics must yield a top-level space");
let name = space
.name
.as_deref()
.expect("top-level FuncSpace name must be Some, not the parse-error sentinel None");
assert!(
name.contains('\u{FFFD}'),
"expected U+FFFD replacement char in lossy name, got {name:?}"
);
assert!(
name.starts_with("foo_") && name.ends_with("_bar.rs"),
"lossy name must preserve the surrounding ASCII bytes, got {name:?}"
);
}
#[test]
fn analyze_in_memory_snippet_carries_caller_supplied_name() {
use crate::{Source, analyze};
let source = Source::new(crate::LANG::Cpp, b"int a = 42;")
.with_name(Some("in-memory.cpp".to_owned()));
let space = analyze(source, MetricsOptions::default())
.expect("analyze must yield a top-level space");
assert_eq!(
space.name.as_deref(),
Some("in-memory.cpp"),
"top-level name must be the caller-supplied string, byte-for-byte"
);
}
#[test]
fn analyze_without_name_leaves_top_level_name_none() {
use crate::{Source, analyze};
let space = analyze(
Source::new(crate::LANG::Cpp, b"int a = 42;"),
MetricsOptions::default(),
)
.expect("analyze must yield a top-level space");
assert!(
space.name.is_none(),
"top-level name must be None when Source::name is None, got {:?}",
space.name
);
}
fn make_state<'a>(kind: SpaceKind) -> super::State<'a> {
super::State {
space: super::FuncSpace {
name: None,
start_line: 0,
end_line: 0,
kind,
spaces: Vec::new(),
metrics: super::CodeMetrics::default(),
suppressed: super::SuppressionScope::default(),
},
halstead_maps: crate::metrics::halstead::HalsteadMaps::new(),
}
}
fn file_suppression_all() -> crate::suppression::Suppression {
crate::suppression::Suppression {
kind: crate::suppression::SuppressionKind::File,
scope: crate::suppression::SuppressionScope::All,
source: crate::suppression::SuppressionSource::Native,
}
}
#[test]
fn file_suppression_attaches_to_unit_frame() {
let mut stack = vec![make_state(SpaceKind::Unit), make_state(SpaceKind::Function)];
super::apply_suppression(&mut stack, &file_suppression_all());
assert!(
stack[0].space.suppressed.is_all(),
"file marker (scope=All) must attach to the Unit root frame"
);
assert!(
stack[1].space.suppressed.is_empty(),
"file marker must not attach to a non-Unit frame"
);
}
#[test]
fn file_suppression_skips_non_unit_root_frame() {
let mut stack = vec![
make_state(SpaceKind::Function),
make_state(SpaceKind::Class),
];
super::apply_suppression(&mut stack, &file_suppression_all());
assert!(
stack.iter().all(|s| s.space.suppressed.is_empty()),
"file marker must be silently dropped when no Unit frame exists"
);
}
#[test]
fn file_suppression_finds_unit_deeper_in_stack() {
let mut stack = vec![make_state(SpaceKind::Function), make_state(SpaceKind::Unit)];
super::apply_suppression(&mut stack, &file_suppression_all());
assert!(
stack[0].space.suppressed.is_empty(),
"non-Unit frame above the Unit must not absorb the file marker"
);
assert!(
stack[1].space.suppressed.is_all(),
"file marker must land on the Unit frame even when not at index 0"
);
}
#[test]
fn file_suppression_empty_stack_is_silent_noop() {
let mut stack: Vec<super::State<'_>> = Vec::new();
super::apply_suppression(&mut stack, &file_suppression_all());
}
mod exclude_tests_rust {
use crate::metrics_with_options;
use crate::{MetricsOptions, ParserTrait, RustParser};
use std::path::PathBuf;
fn analyse(source: &str, exclude_tests: bool) -> crate::FuncSpace {
let path = PathBuf::from("lib.rs");
let parser = RustParser::new(source.as_bytes().to_vec(), &path, None);
metrics_with_options(
&parser,
&path,
MetricsOptions::default().with_exclude_tests(exclude_tests),
)
.expect("metrics must yield a top-level space")
}
#[test]
fn outer_test_attribute_elides_function() {
let source = "\
fn prod() -> i32 { 1 + 2 }
#[test]
fn t() { assert_eq!(1 + 1, 2); }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
assert!(
pruned.metrics.cyclomatic.cyclomatic_sum()
<= baseline.metrics.cyclomatic.cyclomatic_sum()
);
}
#[test]
fn cfg_test_mod_elides_entire_module() {
let source = "\
fn prod() -> i32 { 1 }
#[cfg(test)]
mod tests {
fn helper() -> i32 { 2 }
fn another_helper() -> i32 { 3 }
#[test] fn t() { assert_eq!(1, 1); }
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 4);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn tokio_test_attribute_is_elided() {
let source = "\
fn prod() -> i32 { 1 }
#[tokio::test]
async fn async_t() { let _x = 1; }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn cfg_all_test_with_extras_is_elided() {
let source = "\
fn prod() -> i32 { 1 }
#[cfg(all(test, target_arch = \"x86_64\"))]
fn arch_specific_test() { let _x = 1; }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn pure_production_unaffected_by_flag() {
let source = "\
fn prod() -> i32 { 1 + 2 }
fn helper(x: i32) -> i32 { x * 2 }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 2);
assert_eq!(
baseline.metrics.cyclomatic.cyclomatic_sum(),
pruned.metrics.cyclomatic.cyclomatic_sum(),
);
}
#[test]
fn default_flag_off_preserves_baseline() {
let source = "\
fn prod() -> i32 { 1 }
#[test]
fn t() { assert_eq!(1, 1); }
";
let baseline_default = analyse(source, false);
assert_eq!(baseline_default.metrics.nom.functions_sum() as usize, 2);
}
#[test]
fn stacked_attributes_walk_all_siblings() {
let source = "\
fn prod() -> i32 { 1 }
#[cfg(target_arch = \"x86_64\")]
#[cfg(test)]
fn t() { let _x = 1; }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn cfg_with_test_not_first_is_elided() {
let source = "\
fn prod() -> i32 { 1 }
#[cfg(all(unix, test))]
fn unix_only_test() { let _x = 1; }
#[cfg(any(feature = \"slow\", test))]
fn slow_or_test() { let _x = 2; }
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 3);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
#[test]
fn lookalike_attributes_are_not_pruned() {
let source = "\
#[cfg(not(test))]
fn only_outside_tests() -> i32 { 1 }
#[cfg(feature = \"test\")]
fn behind_test_feature() -> i32 { 2 }
#[my_crate::test_helper]
fn decorated_helper() -> i32 { 3 }
#[cfg(all(unix, not(test)))]
fn unix_prod_only() -> i32 { 4 }
";
let pruned = analyse(source, true);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 4);
}
#[test]
fn inner_cfg_test_attribute_elides_module() {
let source = "\
fn prod() -> i32 { 1 }
mod tests {
#![cfg(test)]
fn helper() -> i32 { 2 }
#[test] fn t() { assert_eq!(1, 1); }
}
";
let baseline = analyse(source, false);
let pruned = analyse(source, true);
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 3);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 1);
}
}
mod exclude_tests_non_rust {
use crate::metrics_with_options;
use crate::{CppParser, MetricsOptions, ParserTrait};
use std::path::PathBuf;
#[test]
fn cpp_ignores_exclude_tests_flag() {
let source = "\
int prod() { return 1; }
int helper() { return 2; }
";
let path = PathBuf::from("foo.cpp");
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
let baseline = metrics_with_options(
&parser,
&path,
MetricsOptions::default().with_exclude_tests(false),
)
.expect("baseline must yield a top-level space");
let parser = CppParser::new(source.as_bytes().to_vec(), &path, None);
let pruned = metrics_with_options(
&parser,
&path,
MetricsOptions::default().with_exclude_tests(true),
)
.expect("pruned must yield a top-level space");
assert_eq!(baseline.metrics.nom.functions_sum() as usize, 2);
assert_eq!(pruned.metrics.nom.functions_sum() as usize, 2);
}
}
mod with_only {
use crate::{LANG, Metric, MetricSet, MetricsOptions, Source, analyze};
const SOURCE: &str = "\
fn prod(x: i32) -> i32 {
if x > 0 { x + 1 } else { x - 1 }
}
";
fn analyse(metrics: &[Metric]) -> crate::FuncSpace {
let opts = MetricsOptions::default().with_only(metrics);
analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
opts,
)
.expect("analyze must yield a top-level space")
}
#[test]
fn loc_only_skips_other_metrics() {
let full = analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
MetricsOptions::default(),
)
.expect("full analyze must yield a top-level space");
let pruned = analyse(&[Metric::Loc]);
assert_eq!(
pruned.metrics.selected(),
MetricSet::empty().with(Metric::Loc),
"with_only(&[Loc]) must record exactly the Loc bit"
);
assert!(pruned.metrics.loc.ploc() >= 1.0);
assert!(full.metrics.cognitive.cognitive_sum() > 0.0);
assert_eq!(pruned.metrics.cognitive.cognitive_sum(), 0.0);
assert!(full.metrics.cyclomatic.cyclomatic_sum() > 0.0);
assert_eq!(pruned.metrics.cyclomatic.cyclomatic_sum(), 0.0);
assert_eq!(pruned.metrics.halstead.u_operators(), 0.0);
}
#[test]
fn mi_auto_pulls_dependencies() {
let pruned = analyse(&[Metric::Mi]);
let sel = pruned.metrics.selected();
assert!(sel.contains(Metric::Mi));
assert!(sel.contains(Metric::Loc), "Mi depends on Loc");
assert!(sel.contains(Metric::Cyclomatic), "Mi depends on Cyclomatic");
assert!(sel.contains(Metric::Halstead), "Mi depends on Halstead");
assert!(!sel.contains(Metric::Abc));
assert!(!sel.contains(Metric::Tokens));
assert!(
pruned.metrics.loc.ploc() > 0.0,
"Loc must have run (Mi dependency); got ploc=0"
);
assert!(
pruned.metrics.cyclomatic.cyclomatic_sum() > 0.0,
"Cyclomatic must have run (Mi dependency); got sum=0"
);
let mi_value = pruned.metrics.mi.mi_original();
assert!(
mi_value.is_finite() && mi_value != 0.0,
"MI must be finite and non-default when its dependencies were computed; got {mi_value}"
);
}
#[test]
fn wmc_auto_pulls_dependencies() {
let pruned = analyse(&[Metric::Wmc]);
let sel = pruned.metrics.selected();
assert!(sel.contains(Metric::Wmc));
assert!(
sel.contains(Metric::Cyclomatic),
"Wmc depends on Cyclomatic"
);
assert!(sel.contains(Metric::Nom), "Wmc depends on Nom");
assert!(!sel.contains(Metric::Halstead));
assert!(
pruned.metrics.cyclomatic.cyclomatic_sum() > 0.0,
"Cyclomatic must have run (Wmc dependency); got sum=0"
);
assert!(
pruned.metrics.nom.functions_sum() > 0.0,
"Nom must have run (Wmc dependency); got functions_sum=0"
);
}
#[test]
fn default_options_select_every_metric() {
let full = analyze(
Source::new(LANG::Rust, SOURCE.as_bytes()).with_name(Some("lib.rs".to_owned())),
MetricsOptions::default(),
)
.expect("analyze must yield a top-level space");
assert_eq!(full.metrics.selected(), MetricSet::all());
}
#[test]
fn unselected_metrics_are_skipped_in_json() {
let pruned = analyse(&[Metric::Loc]);
let json =
serde_json::to_value(&pruned.metrics).expect("CodeMetrics must serialize cleanly");
let metrics = json.as_object().expect("CodeMetrics serializes as object");
assert!(
metrics.contains_key("loc"),
"loc must be serialized when selected"
);
for skipped in [
"cognitive",
"cyclomatic",
"halstead",
"nom",
"tokens",
"nargs",
"nexits",
"abc",
"mi",
"wmc",
"npm",
"npa",
] {
assert!(
!metrics.contains_key(skipped),
"{skipped} must be elided when not selected"
);
}
}
#[test]
fn empty_slice_selects_nothing() {
let pruned = analyse(&[]);
assert_eq!(pruned.metrics.selected(), MetricSet::empty());
let json =
serde_json::to_value(&pruned.metrics).expect("CodeMetrics must serialize cleanly");
let metrics = json.as_object().expect("CodeMetrics serializes as object");
assert!(
metrics.is_empty(),
"with_only(&[]) must elide every metric, got keys {:?}",
metrics.keys().collect::<Vec<_>>()
);
}
}
}