Skip to content

refactor(cognitive): thread Nesting end to end - #1104

Merged
dekobon merged 5 commits into
mainfrom
fix/1086-thread-nesting
Jul 30, 2026
Merged

refactor(cognitive): thread Nesting end to end#1104
dekobon merged 5 commits into
mainfrom
fix/1086-thread-nesting

Conversation

@dekobon

@dekobon dekobon commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Closes #1086.

increase_nesting was the last bare-positional-usize triple in the
cognitive walk — (stats, &mut nesting, depth, lambda) at 43 call sites
across the 19 per-language modules and the js_cognitive! macro. It now
takes &mut Nesting, and every compute body holds the struct end to
end instead of destructuring it into three same-typed locals and
rebuilding it on the way out. Adds Nesting::total() for the
conditional + function_depth + lambda sum, which was open-coded at two
sites.

Net −120 lines across the cognitive modules. No behaviour change:
cargo test --workspace --all-features is bit-identical.

Corrections to the issue's framing

The issue was wrong that a field transposition here is unobservable,
and that mattered — it was the stated justification for landing with no
test. Perturbing nesting.conditional += 1 to
nesting.function_depth += 1 fails six tests: the new unit test
plus java_nested_method_resets_nesting_and_adds_depth,
cpp_nested_function_resets_nesting_and_adds_depth,
groovy_nested_method_resets_nesting_and_adds_depth,
php_nested_function_resets_nesting_775, and
csharp_local_function_in_if_does_not_inherit_nesting.

The charge itself is symmetric so the mistake is invisible there, but a
function boundary resets conditional alone, so a misplaced
increment survives into function_depth and moves the score. This is
the opposite of #1070's case, where every transposition genuinely was
unobservable; the two should not be cited together.

Two smaller corrections: the call sites span 19 modules plus the
macro (not "21 of 23"), and total() replaced two open-coded sums
(not three).

Tests

Two new unit tests pin what the new shape makes newly expressible —
bumping the wrong channel, and dropping one from the sum. Both were
verified by perturbing the production line per
.claude/rules/testing.md, not merely by passing.

Reviewing the diff surfaced adjacent gaps in files this already touches,
fixed here rather than deferred — each also verified by perturbation:

  • python_comprehension_clauses_carry_inherited_depth_and_lambda
    covered only a trailing-for fixture, so the
    + usize::from(last_clause_is_if) term of the returned element depth
    contributed 0 and was untested — deleting it left the test green.
    Added a trailing-if fixture ([x for a in xs for b in ys if b],
    element depth 3).
  • The new increase_nesting test first asserted the boolean_seq reset
    from Stats::default(), where it cannot fail; deleting
    stats.boolean_seq.reset() was measured at zero failures
    suite-wide. Now seeded. Same for increment's += vs =, which was
    indistinguishable from a zeroed structural.
  • elixir.rs's header comment contradicted its own code twice: it
    documented try as adding +nesting on the container (deliberately
    excluded) and def as bumping the function depth (deliberately not —
    every def inside a defmodule has a Call ancestor, which would
    falsely raise it).

As a sanity check on the suite itself, gutting increase_nesting to a
near-no-op fails 263 of 3,130 lib tests.

Gates

make pre-commit green: clippy (both feature flavours), fmt, cargo doc -D warnings, udeps, workspace tests with zero failures and no
.snap.new drift, both self-scan tiers, and the
--no-default-features / --features rust,typescript CI matrix legs.

.bca-baseline.toml needed one refresh: RubyCode::compute's
halstead.effort dropped 53802 → 47981, a tightening consistent with
the smaller module.

Known trade-off

Threading the struct removes the compile-time guard that bash.rs,
c.rs, irules.rs, and tcl.rs got from binding lambda immutably —
a stray nesting.lambda += 1 there now compiles. The residual risk is
low and untestable in a meaningful way: those four languages have no
lambda or closure construct, so there is no arm that could plausibly
grow such a write, and the real protection was always "the grammar has
no such node" rather than the binding.

Follow-up

#1103 — the nesting.conditional = 0; +
increment_function_depth(...) pair now repeats verbatim in 17 of 23
modules and wants an enter_function_boundary helper (~−55 lines). Kept
out of this PR so the diff stays atomic and bit-identical by inspection.

Toolchain gotcha for reviewers

cargo fmt --check exits 0 over misformatted code in these modules.
Any comment inside a match pattern — a block comment before =>
(Else /* else-if also */ =>) or // lines between | alternatives
(perl.rs) — makes rustfmt emit the enclosing match verbatim.

Eight sites are affected: the modules c.rs, cpp.rs, java.rs,
mozcpp.rs, objc.rs, perl.rs, rust.rs, plus the js_cognitive!
macro body in cognitive.rs. macro_rules! bodies are not exempt —
rustfmt formats them by default, so the macro bails for the same reason
as the rest.

This bit the PR: a regex rewrite of 43 call sites left hanging-argument
calls and over-length lines that cargo fmt --all -- --check reported
as clean, all found by reading the diff. Written up with a
list-regeneration snippet in .claude/rules/formatting.md.

(Two earlier revisions of this section got the count and the cause
wrong — first eight-with-the-wrong-reason, then seven. The commit
cda402f1 records the corrected version and the two measurement traps
that produced the wrong answers.)

`increase_nesting` was the last bare-positional-`usize` triple in the
cognitive walk, spelled `(stats, &mut nesting, depth, lambda)` at 43
call sites across the 19 per-language modules and the `js_cognitive!`
macro. It now takes `&mut Nesting`, and every `compute` body holds the
struct end to end rather than destructuring it into three same-typed
locals and rebuilding it on the way out.

Adds `Nesting::total()` for the `conditional + function_depth + lambda`
sum, which was open-coded at its two sites.

No behaviour change: the full workspace suite is bit-identical. Ruby's
`halstead.effort` baseline drops to match the smaller module.

Two tests pin what the new shape makes newly expressible — bumping the
wrong channel, and dropping one from the sum. Both were verified by
perturbing the production line; a `function_depth += 1` slip fails the
new test plus the five nested-function tests that observe it
downstream, where a function boundary resets `conditional` alone.

Also fixed, in files this already touches: the Python comprehension
test covered only a trailing-`for` fixture, leaving the
`last_clause_is_if` term of the returned element depth untested
(deleting it left the test passing); the `increase_nesting` test
asserted the `boolean_seq` reset and `increment`'s accumulation from
zeroed state, where neither could fail; and `elixir.rs`'s header
comment contradicted its own code on both `try` nesting and the
deliberate absence of a function-depth bump.

Fixes #1086
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.63235% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.18%. Comparing base (b93bacc) to head (e3c22e6).

Files with missing lines Patch % Lines
src/metrics/cognitive/python.rs 98.18% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1104      +/-   ##
==========================================
+ Coverage   98.16%   98.18%   +0.01%     
==========================================
  Files         272      272              
  Lines       68295    68206      -89     
  Branches    67865    67776      -89     
==========================================
- Hits        67045    66968      -77     
+ Misses        858      848      -10     
+ Partials      392      390       -2     
Flag Coverage Δ
rust 98.17% <99.63%> (+0.01%) ⬆️

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

Files with missing lines Coverage Δ
src/metrics/cognitive.rs 99.85% <100.00%> (+<0.01%) ⬆️
src/metrics/cognitive/bash.rs 100.00% <100.00%> (ø)
src/metrics/cognitive/c.rs 100.00% <100.00%> (ø)
src/metrics/cognitive/cpp.rs 100.00% <100.00%> (ø)
src/metrics/cognitive/csharp.rs 100.00% <100.00%> (ø)
src/metrics/cognitive/elixir.rs 100.00% <100.00%> (ø)
src/metrics/cognitive/go.rs 97.43% <100.00%> (-0.49%) ⬇️
src/metrics/cognitive/groovy.rs 100.00% <100.00%> (ø)
src/metrics/cognitive/irules.rs 100.00% <100.00%> (+5.88%) ⬆️
src/metrics/cognitive/java.rs 100.00% <100.00%> (ø)
... and 11 more

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

dekobon added 2 commits July 30, 2026 14:07
An assertion that a function resets or accumulates something proves
nothing from a default-constructed fixture: `Default::default()` is the
state a reset produces and the value `+=` and `=` agree on, so the
assertion holds whether or not the line under test exists.

Both instances cited shipped in the same test during #1086 and were
measured rather than guessed — deleting the production `reset()` call
failed zero of 3,130 lib tests.
A comment inside a match pattern — before `=>` or between `|`
alternatives — makes rustfmt emit the entire enclosing item verbatim.
There is no diagnostic and `cargo fmt --check` still exits 0, so a bulk
or regex edit across sibling modules lands misformatted in exactly the
files the gate cannot see. This happened during #1086.

Affects seven modules under src/metrics/cognitive/. Records the
perturbation procedure for testing a file, including the stderr trap
that makes a `mod`-declaring file look like it bailed when rustfmt
actually failed to resolve the module tree.
dekobon added 2 commits July 30, 2026 14:28
Two factual errors in the rule as first written, both found by review
and confirmed by experiment:

`macro_rules!` bodies are not exempt from formatting and are not a
separate cause. rustfmt formats them by default (`format_macro_bodies`),
and `js_cognitive!` in cognitive.rs bails for exactly the in-pattern
comment reason — removing its `Else /* else-if also */` arm comment
makes rustfmt reformat the whole body. The rule previously told readers
the opposite, excluding the clearest example of the hazard. The affected
set is eight sites, not seven.

The bail is scoped to the enclosing match, not the whole item.
Statements after the match still format, which is what made an earlier
check on an unrelated function in the same file read as "unaffected".

Also replaces the hardcoded module list with a snippet that regenerates
it, and records the two measurement traps that produced the wrong answer
the first time.
Both arms measured zero coverage. iRules had nine cognitive tests but
none exercising `catch`, and mozcpp had no cognitive tests at all
despite the fork being expected to stay metric-equivalent to upstream
C++.

Neither gap was introduced by the Nesting refactor — patch coverage
surfaced them because that change rewrote every line in these modules,
which pulled pre-existing untested arms into the diff.

Expected values were derived before running, then confirmed by
perturbation: gutting the `Catch` arm scores 0 instead of 1, and
dropping mozcpp's lambda increment scores 1 instead of 2.
@dekobon
dekobon merged commit daccb6f into main Jul 30, 2026
39 checks passed
@dekobon
dekobon deleted the fix/1086-thread-nesting branch July 30, 2026 22:31
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(cognitive): thread Nesting through increase_nesting

1 participant