Skip to content

refactor(metrics): unify average-divisor guard, cyclomatic per-function#527

Merged
dekobon merged 1 commit into
mainfrom
refactor/512-unify-average-divisor
Jun 5, 2026
Merged

refactor(metrics): unify average-divisor guard, cyclomatic per-function#527
dekobon merged 1 commit into
mainfrom
refactor/512-unify-average-divisor

Conversation

@dekobon
Copy link
Copy Markdown
Owner

@dekobon dekobon commented Jun 5, 2026

Summary

Resolves #512 (part of #505; folded into the 2.0 re-baseline).

Unifies the "average over a count" divisor guarding behind one helper and
makes cyclomatic's average per function, reconciling it with the
cognitive / exit / nargs convention.

Changes

  1. Shared helper crate::metrics::average(sum, count) applies the
    .max(1) divide-by-zero guard (fix(metrics): Cognitive/Exit/NArgs averages depend on undeclared Nom dependency → inf/NaN #428) in one place. Every metric
    average now routes through it — cognitive, exit, nargs, nom,
    and the previously-unguarded per-space averages loc / abc /
    tokens (which divided by space_count with no guard at all).
  2. Cyclomatic is per-function. cyclomatic.average /
    cyclomatic.modified.average divide by a new function_spaces
    accumulator (count of SpaceKind::Function spaces in the subtree),
    seeded in FuncSpace::new from the space kind and summed on merge.
    It is sourced independently of the Nom metric, so
    metrics=["cyclomatic"] still emits exactly {cyclomatic} — no nom
    block leaks into a cyclomatic-only selection.

Denominator convention (documented)

  • Per-function (cognitive, cyclomatic, exit, nargs): divide by
    the function/closure count.
  • Per-space (nom, loc, abc, tokens): divide by the total space
    count (per-function would be circular for nom).

Value impact

Only cyclomatic.average and cyclomatic.modified.average change value
(larger for files with classes/structs/units). sum / min / max and
every other metric — including the Maintainability Index and WMC, which
consume the cyclomatic sum — are unchanged. All 25,088 changed
submodule snapshot lines are under cyclomatic:.

Known nuance (pinned, intentional)

function_spaces == nom.total() wherever a closure opens its own space
(verified across Ruby / Go / Kotlin / C# / Java). A Python lambda is
counted by nom but opens no space, so it is not a separate cyclomatic
divisor unit — the divisor counts the spaces that actually carry a
cyclomatic value. Pinned by
cyclomatic_python_lambda_divisor_excludes_spaceless_closure. Exact
lambda-level reconciliation with cognitive would require nom-coupling or
independent lambda-node counting and is left as a deliberate follow-up.

Tests / validation

  • New tests: per-function divisor, no-nom contract, zero-divisor guard,
    and the Python-lambda edge case. Verified via production-mutation
    (revert → tests fail → restore).
  • Quality skills run (simplify-rust, rust-optimize, audit-tests,
    code-review, review); findings resolved.
  • Snapshots re-baselined: inline, tests/snapshots, and the
    big-code-analysis-output submodule.
  • Gates green: fmt, clippy (default + all-features), workspace tests
    (all features), doc, snapshot-anchors, self-scan + headroom, rumdl.

Submodule

Requires dekobon/big-code-analysis-output at 65f5dccf (pushed to
main); the parent records the bumped SHA in this PR.

Extract a single `crate::metrics::average(sum, count)` helper that applies
the `.max(1)` divide-by-zero guard (#428) in one place, and route every
metric average through it. This removes the former reliance on a counter
that merely defaulted to 1 for cyclomatic and nom, and adds the guard to
the previously-unguarded per-space averages (loc, abc, tokens).

Make `cyclomatic.average` / `cyclomatic.modified.average` per-function:
the divisor is now the count of function/closure spaces in the subtree
(`function_spaces`), the per-function convention cognitive/exit/nargs use,
instead of the per-space count (which also divided by classes, structs,
and the file unit). The count is seeded in `FuncSpace::new` from the space
kind and summed on merge, so it is sourced independently of the Nom metric
and a cyclomatic-only selection still divides per function without pulling
a nom block into the output.

Metric values change for cyclomatic.average and modified.average only;
sum/min/max and every other metric (including MI and WMC, which consume
the cyclomatic sum) are unchanged. The denominator equals nom.total()
wherever a closure opens its own space; a spaceless closure such as a
Python lambda is counted by cognitive but not as a separate cyclomatic
divisor unit (pinned by a regression test). Snapshots — including the
big-code-analysis-output submodule — are re-baselined accordingly.

Part of #505. Folded into the 2.0 re-baseline.

Fixes #512
@dekobon dekobon merged commit a1761c9 into main Jun 5, 2026
28 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.04%. Comparing base (4c02c89) to head (14794bc).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #527      +/-   ##
==========================================
+ Coverage   86.01%   86.04%   +0.03%     
==========================================
  Files          74       75       +1     
  Lines       56528    56666     +138     
  Branches    56488    56626     +138     
==========================================
+ Hits        48621    48759     +138     
  Misses       7401     7401              
  Partials      506      506              
Flag Coverage Δ
rust 86.03% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/metrics/abc.rs 97.37% <100.00%> (ø)
src/metrics/cognitive.rs 99.64% <100.00%> (ø)
src/metrics/cyclomatic.rs 99.29% <ø> (+0.02%) ⬆️
src/metrics/exit.rs 99.24% <100.00%> (ø)
src/metrics/loc.rs 99.33% <100.00%> (ø)
src/metrics/mod.rs 100.00% <100.00%> (ø)
src/metrics/nargs.rs 98.41% <100.00%> (+<0.01%) ⬆️
src/metrics/nom.rs 99.06% <100.00%> (ø)
src/metrics/tokens.rs 93.38% <100.00%> (ø)
src/spaces.rs 97.49% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(metrics): unify average-divisor guarding and denominator convention

1 participant