Repository navigation
fix(npm): go and rust still enable npm/npa on function spaces #1203
Description
Fix open in #1206, not yet merged — this issue closes when that
PR lands (itsFixes #1203trailer does it automatically). Submodule
snapshots are already pushed as
big-code-analysis-output@a23ea6a8. Emission becomes a pure function
ofSpaceKindfor every language, not just Go and Rust. Two corrections to the analysis below, both verified by
measurement — see
the closing comment:
- The resolution plan's claim that clearing the flag "suppresses that
space's block without losing the parent roll-up" is false for
the shapes this issue reports. Go's root enables only on a direct
MethodDeclarationchild and Rustnpaonly on a module-scope
struct, so in 4 of the 9 affectedserdefiles the root carries
no block and a plain clear would have deleted the counts from
output rather than relocating them.- The deviation was wider than Go and Rust: all 18,064 C++
namespacespaces and 13,703 file roots in the corpora were member
scopes emitting nothing.The Go container-
SpaceKindquestion in Scope was decided
no — it is a space-tree shape change for the next major. Its
consequence is documented instead: nonpm/npathreshold can fire
on Go source.
Summary
#1197 gave npm / npa a single emission rule —
SpaceKind::is_member_scope: containers and the file root carry the
block, a function space never does — and routed ten languages through
metrics::opens_member_scope to enforce it.
Go and Rust were deliberately left on their own node-kind gates, on the
grounds that those gates target real containers. For Go that is not
true, and for Rust it is only half true.
Go
GoCode::get_space_kind has no container kind at all — Go spaces are
only Function and Unit. The enable therefore targets the file root
and, in one path, whatever space encloses a type literal:
src/metrics/npm/go.rs:25enables onG::SourceFile, which is
SpaceKind::Unit.src/metrics/npa/go.rs:116enables onG::StructType, which opens no
space, so the flag lands on the enclosing space. For a struct declared
inside a function body that is aFunctionspace — precisely the
shape fix(npm): the #1184 function spaces emit npm/npa blocks their sibling methods do not #1197 removed everywhere else.src/metrics/npm/go.rs:90does the same forG::InterfaceType; its
own comment says "the count lands on the enclosing space (typically
Unit)".
A consequence worth stating separately: bca check --threshold npm=… is
MetricScope::Container-scoped, and Go has no container space, so a Go
file's npm can never be gated by a threshold no matter what it
measures.
Rust
src/metrics/npa/rust.rs:128 (rust_count_struct_attrs) sets the flag
on the enclosing func_space, and RustCode::is_func_space includes
SourceFile, FunctionItem and ClosureExpression. So
struct S { pub a: u8 } at module scope enables npa on the Unit root
— fine — but a struct declared inside a function enables it on a
Function space.
npm is not affected: src/metrics/npm/rust.rs:37 gates on
ImplItem | TraitItem, both real containers.
Why this was not folded into #1197
Both are pre-existing and neither is the reported symptom. Fixing them
is not a matter of routing them through opens_member_scope: Go's
counts are deliberately attributed to the enclosing space because Go
has no container to attach them to, so the fix needs a decision about
what a Go npm means before it needs code. Rust's npa has the same
question for a bare struct at module scope, which today is the file
root's roll-up and arguably correct.
Scope
- Decide whether Go should gain a container
SpaceKindfor
type … struct/type … interface, or whether its OO metrics should
stay Unit-scoped and be documented as such. - Either way, stop both languages enabling on a
Functionspace. - The doc claims in
src/metrics/npm.rs,src/metrics/npa.rsand
big-code-analysis-book/src/metrics.mdcurrently carry an explicit
Go/Rust caveat added by fix(npm): the #1184 function spaces emit npm/npa blocks their sibling methods do not #1197; remove it when this lands. - Extend
src/metrics/container_scope_tests.rsto cover Go and Rust
once they obey the shared rule.
Resolution Plan
Confirmed against main (39674df6, which corrected the docs to admit
the deviation but did not change the behaviour). Every cited site is
live: src/metrics/npm/go.rs:25 (G::SourceFile), :89 (the
InterfaceType arm's if stats.is_disabled() { stats.is_class_space = true; }), src/metrics/npa/go.rs (the StructType arm's identical
enable), and src/metrics/npa/rust.rs:128.
The load-bearing finding: the flag only controls emission
is_class_space is read in exactly one place —
src/wire.rs:354-356, which gates whether the npm / npa block is
serialized. Neither Stats::merge (src/metrics/npm.rs:79) nor
compute_sum (:216) consults it. Clearing the flag on a function
space therefore suppresses that space's block without losing anything
from the parent roll-up — the sums have already been accumulated into
class_npm_sum and friends by the time the space is finalized.
That makes the fix much smaller than the issue anticipates, and it
decouples the two halves: stopping the function-space emission needs no
decision about what a Go npm means. Only the second half does.
- Enforce the rule once, at the walker, not per language. Clear
npm.is_class_space/npa.is_class_spaceinfinalize_state
(src/spaces/compute.rs:150) whenever
!state.space.kind.is_member_scope(). Both the spacekindand the
populated metrics are in hand there, andfinalize's pop arm already
runs a kind-aware step (compute_wmc) at the same point, so this is
the established seam rather than a new one. Doing it here rather than
editing Go's and Rust's enables means the rule holds for every
language, including the next one added, and a language can no longer
opt out of it by gating on its own node kinds. Keep
opens_member_scopeas the enable — it stays the right predicate for
the ten languages that route through it; the walker step becomes the
backstop that makes the invariant unconditional.- Note this does not subsume docs(rules): grammar-dispatch §7 misses the is_func_space ↔ get_space_kind pair #1204's proposed test: the walker
step fixes over-emission, while docs(rules): grammar-dispatch §7 misses the is_func_space ↔ get_space_kind pair #1204's hazard is under-emission
(anis_func_spacenode thatget_space_kind_with_codeleaves
Unknownemits nothing at all). They are opposite failures and need
separate coverage.
- Note this does not subsume docs(rules): grammar-dispatch §7 misses the is_func_space ↔ get_space_kind pair #1204's proposed test: the walker
- Decide the Go container question — and the recommendation is to say
no. Giving Go a containerSpaceKindfortype … struct/
type … interfacemeans adding those kinds toGoCode::is_func_space
andget_space_kind, which creates new nested spaces in every Go
file's output. That is a far larger change than moving a metric block:
every per-space metric (loc,cyclomatic,nom, …) re-partitions,
the space tree changes shape for every Go consumer, and the whole Go
integration corpus churns. Weighed against what it buys — the ability
to gatenpmon a Go file — it is not worth doing as a bug fix. Keep
Go's OO metrics Unit-scoped, document that explicitly (see step 4),
and if container spaces for Go are wanted, raise them as a separate
design change against the next major, where a space-tree shape change
belongs. - Rust
npa.rust_count_struct_attrs
(src/metrics/npa/rust.rs:89, enable at:128) attributes a struct's
fields to the enclosingis_func_space, which includes
SourceFile,FunctionItemandClosureExpression. Step 1 already
suppresses the block on the two function kinds while leaving the
module-scopeSourceFileroll-up intact, which the issue itself calls
"arguably correct". No Rust-specific code change is needed —
confirm that with a fixture rather than assuming it.npmfor Rust
gates onImplItem | TraitItem(src/metrics/npm/rust.rs:37), both
real containers, and is unaffected. - Documentation. Remove the Go/Rust caveat that
39674df6added to
src/metrics/npm.rs,src/metrics/npa.rsand
big-code-analysis-book/src/metrics.md— once step 1 lands, the
shared rule is true without exception. Replace it with the positive
statement that Go'snpm/npaare file-scoped because Go has no
container space, and its corollary:bca check --threshold npm=…is
MetricScope::Container-scoped, so a Go file'snpmcan never trip
a threshold. That consequence is the one a user is most likely to
hit and least likely to derive; it belongs in the book's metrics page
and in the thresholds recipe, not only in a source comment. - Tests. Extend
src/metrics/container_scope_tests.rsto Go and
Rust: a Go struct and interface declared inside a function body
must leave the enclosing function space with nonpm/npablock
while the file root still carries the roll-up; a Ruststructinside
a function likewise. Assert both directions — the presence of the root
block and the absence of the function-space one — because a rule that
deleted the roll-up would pass an absence-only test, which is the
regressionc028976ehad to correct on fix(npm): the #1184 function spaces emit npm/npa blocks their sibling methods do not #1197. - Verify by revert per
.claude/rules/testing.md: with the
finalize_stateclearing removed, the new Go and Rust tests must fail
and the existing container tests must still pass. If the existing ten
languages' tests also fail, the walker step is doing work
opens_member_scopewas already doing and the two need reconciling
rather than layering. - Snapshots and changelog. Go and Rust files in
tests/repositories/big-code-analysis-outputlosenpm/npa
blocks on function spaces; all four AGENTS.md submodule steps apply.
CHANGELOG.md## [Unreleased]→Fixed, framed as the completion
of fix(npm): the #1184 function spaces emit npm/npa blocks their sibling methods do not #1197: the emission rule now holds for every language, with the
STABILITY.mdnote that which space carries which block is not part
of the shape contract already covering it.
Assessment
| Dimension | Rating |
|---|---|
| Difficulty | Low |
| Complexity | Medium |
| Priority | Medium |
Difficulty — Low. Downgraded from what the issue implies. Because
is_class_space gates emission only and the roll-up is independent of
it, the behavioural fix is a few lines in finalize_state rather than a
rework of Go's and Rust's enables. The bulk of the work is tests, docs
and snapshots.
Complexity — Medium. One crate, one new line in the walker, but it
changes serialized output for two languages, churns integration
snapshots with a submodule bump, and carries a design decision (step 2)
that should be recorded even though the recommendation is to decline it.
Priority — Medium. No count is wrong — the numbers on containers and
on the file root are correct, and the roll-up is intact. What is wrong is
which space carries the block, which #1197 already declared the rule for,
so this is an unfinished fix rather than a new defect. The Go threshold
gap is a real user-visible limitation but a narrow one, and documenting
it (step 4) addresses most of the harm.
Activity
dekobon commentedon Aug 4, 2026
Added resolution plan and assessment ratings (difficulty Low / complexity Medium / priority Medium).
Confirmed every cited site is still live on
main(39674df6corrected the docs to admit the deviation but did not change behaviour).The plan turns on one finding that shrinks the fix considerably:
is_class_spaceis read in exactly one place —src/wire.rs:354-356, which gates serialization. NeitherStats::mergenorcompute_sumconsults it. So clearing the flag on a function space suppresses that space's block without losing the parent roll-up, because the sums were already accumulated. That means the two halves of this issue decouple: stopping the function-space emission needs no decision about what a Gonpmmeans, and can be done once infinalize_state(src/spaces/compute.rs:150, wherekindand the populated metrics are both in hand andcompute_wmcalready runs) rather than by editing Go's and Rust's enables. Doing it at the walker also makes the rule hold for languages added later, which a per-language edit cannot.On the scope question the recommendation is to decline giving Go container
SpaceKinds: it would create new nested spaces in every Go file, re-partitioning every per-space metric and changing the space tree shape for all consumers — a major-version design change, not a bug fix. Keep Go Unit-scoped and document the consequence that a Go file'snpmcan never trip aMetricScope::Containerthreshold.No
low-prioritylabel: difficulty and complexity are not both Low, and priority is Medium.dekobon commentedon Aug 4, 2026
Fixed in
91ac4213(+e165abf4,c6c4885e). Submodule snapshots indekobon/big-code-analysis-output@a23ea6a8, recorded in the same parentcommit. Not yet pushed — awaiting authorization.
Two things in this issue's analysis did not survive verification
1. The resolution plan's load-bearing finding was half right. It is
true that
is_class_spacegates emission only — it is read in exactlyone place,
is_disabled()→src/wire.rs:354-356, and neitherStats::mergenorcompute_sumconsults it. But the conclusion drawnfrom that — that clearing the flag on a function space "suppresses that
space's block without losing the parent roll-up" — is false for
exactly the shapes this issue is about:
Go enables
npmat the root only for a file with a directMethodDeclarationchild, and Rustnpaonly for a module-scopestruct. In 4 of the 9 affectedserdefiles the root has nonpablock at all, so a plain clear would have deleted the counts from output
rather than relocating them — they reach the root's
_sumfields, andnothing serializes those.
2. The deviation is wider than Go and Rust. A kind × emission census
over
serde+DeepSpeech+pdf.js(14,438 files) before the fix:Every C++
namespaceand 13,703 file roots were member scopes emittingnothing. Four different
unitrows is the clearest single sign that therule was a convention rather than an invariant.
What was done instead
Emission is now a pure function of the space's kind, for every language —
the option the issue frames as "either way, stop both languages enabling
on a
Functionspace", taken to its end rather than applied to twolanguages.
is_class_spaceis replaced byspace_kind: SpaceKind,recorded once per space by the walker beside
compute_wmc;opens_member_scope, bothenable_for_member_scopemethods, 18 callsites and 12 per-language enables are deleted. Net −106 lines of
non-comment production code.
After:
One row per kind. The 527 units emitting nothing are the non-OO
languages — shell, C, Perl — held out by a new
Npm::HAS_MEMBERS/Npa::HAS_MEMBERS, because a file unit is a member scope like any otherand would otherwise give every shell script an all-zero block.
wmcreaches the same place through a no-op
computethat records no kind.No metric value changed. Thresholds are unaffected:
bca checkreadsmetrics.npmthroughMetricScope, which never consulted the gate.Scope decisions
Go container
SpaceKind— declined. Addingtype … struct/type … interfacetoGoCode::is_func_spaceandget_space_kindcreates new nested spaces in every Go file, re-partitioning every
per-space metric and changing the tree shape for every consumer. That is
a major-version design change, not a bug fix. Documented instead, in the
metrics guide and the thresholds recipe: a Go file's
npm/npaliveon the unit root,
bca checkgates them on container spaces, thereforeno
npmornpathreshold can fire on Go source.A pre-existing front-end divergence closed as a side effect.
MetricScope::ContaineradmitsNamespace, sobca checkhas alwaysflagged namespace-scoped offenders (verified:
N: npm = 4 (limit 3)).But the Python
to_sarifbinding walks serialized JSON and skips ametric whose key is absent (
sarif.rs:508) — and no namespace carriedthe key, so it silently dropped every one of them, contradicting its own
claim to match the CLI gate. SARIF output may now gain findings.
Verification
make pre-commitgreen. Library suite 3,317 tests.container_scope_testsextended from 10 languages to all 17,including Mozcpp, which owns no file extension and so had no coverage
at all.
.claude/rules/testing.md—reverting was not available, since the fix deletes the old path.
Reinstating the defect fails 3 tests; deleting the
npaseed fails 4;flipping
HAS_MEMBERSfails exactly 1.count rather than a key, was confirmed inert by running a
pre-change binary on the new fixture:
Outer3/2/1,Inner1/0,Sibling1 — bit-identical.added keys are exactly
npm/npaand their sub-keys, removals exactly39 whole
npablocks on Rust function spaces, no other metric keytouched on either side.
dekobon commentedon Aug 4, 2026
Reopened — I closed this on commit rather than on merge, which was premature. The fix is in #1206 and this issue will close automatically when that lands, via the PR body's
Fixes #1203trailer.Nothing about the analysis in the comment above changes; only the status did.
refactor(npm): tighten the #1203 emission change after review
docs(npm): correct the #1203 figures and three stale claims
fix(nargs): stop counting comments as parameters