Skip to content

fix: batch 2026-08-05 (#1204, #1209, #1212) - #1216

Merged
dekobon merged 5 commits into
mainfrom
fix/batch-2026-08-05
Aug 6, 2026
Merged

fix: batch 2026-08-05 (#1204, #1209, #1212)#1216
dekobon merged 5 commits into
mainfrom
fix/batch-2026-08-05

Conversation

@dekobon

@dekobon dekobon commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Three low-priority follow-ups from #1197, #1200 and #1208, each closing a
gap where something was unguarded, undocumented, or dead. No metric value
moves and no public API changes, so there is no snapshot churn and no
big-code-analysis-output submodule bump.

What's here

Commit Issue Change
8ab58c3d #1212 Remove Search::first_occurrence and its #[allow(dead_code)]
dd34b1c1 #1209 Document the K&R wrapped-return-type parse limitation, pinned by a drift-marker test
1c18af87 #1204 Add §7's fourth cross-walk pair and a _with_code subsection to grammar-dispatch.md, with a fixture sweep
303509f5 #1204 Guard that sweep against a bare root (found by /audit-tests)
cb4945b0 Consolidated CHANGELOG entry

#1212 — remove Search::first_occurrence

#1208 moved the four C-family get_func_space_name impls — its only
production callers — onto the shared declarator walk in
src/c_declarator.rs, leaving the method alive only under an
#[allow(dead_code)] and guarded by three tests that protected nothing
shipped. Search is pub(crate), so there is no public API impact and
STABILITY.md needs no change.

The one remaining utility use in src/spaces_tests.rs located a nested
function_definition, so first_child was not a drop-in (it scans one
level). It now uses root.preorder().find(…), which is the same
leftmost-first, root-inclusive walk — Preorder has its own order pin in
preorder_matches_recursive_document_order.

The child_scan_cursors consumer count drops six to five in both places
that state it. docs/development/benchmarking.md's historical #1112
measurement is re-attributed to act_on_node rather than restated with an
invented number — and that re-attribution was checked by reverting
act_on_node to children(), which reproduces exactly the recorded
"50 cursors over 50 nodes".

#1209 — K&R functions with a wrapped return type

A pre-ANSI K&R definition whose return type wraps the declarator opens no
function space under C or Objective-C:

int  krplain(a, b) int a; int b; { if (a) { return 0; } return 1; }  /* space, nargs 2 */
int *krptr(a, b)   int a; int b; { if (a) { return 0; } return 1; }  /* no space at all */

The report's root-cause section was partly wrong and has been corrected on
the issue. The parse is re-associated, not merely reclassified, and
produces no ERROR node: the declaration's wrapping declarator
swallows the first parameter declaration, int b; becomes a second
top-level declaration, and the body is orphaned as a bare
compound_statement. In tree-sitter-c 0.24.2 the old-style function
definition nests its declarator inside the old-style declarator, so an
outer pointer_declarator is unreachable.

The impact is not uniform invisibility — the orphaned body's decisions are
charged to the file's unit space (cyclomatic.sum 2 with
nom.functions 0), a file-level number no function-level row accounts for.

Two facts not in the original report: LANG::Objc shares the defect, and
Cpp / Mozcpp open no space for either K&R form, so .h / .mm
routing is not a mitigating factor.

Resolution is option 1 — document, do not work around. src/checker/c.rs
already enumerates both function_definition aliases correctly; the node
it would match is never produced, so there is no dispatch arm to add.
Re-implementing K&R declarator recognition would be a slice of the grammar
rewritten for a form C23 removed. The grammar pin is untouched, and
nothing was filed upstream.

The paired test is a drift marker: it pins cyclomatic.sum == 2
where the decision currently lands — so a grammar bump that fixes the
parse fails loudly rather than shifting metrics silently. Its doc comment
says explicitly that the values are a bug-lock, not an endorsement.
A control fixture (char **kr2, same defect, no decision) measures 1,
proving the pinned 2 tracks the orphaned if rather than being a
parse-failure constant.

#1204grammar-dispatch §7 and the _with_code spelling

§7 listed three predicate pairs and not is_func_space
get_space_kind. That pair is worth its own entry because the three
existing ones disagree into a wrong count, which a snapshot diff
shows you, while this one disagrees into an absent key — a promoted
node the getter left Unknown is not a member scope, so its space
serializes no npm / npa block, which reads exactly like a language
with no containers.

The new subsection states why the _with_code trap is silent: the
defaults forward to the byte-less form, so both spellings compile and
behave identically
everywhere except where a language overrides — which
is precisely where the answer matters. Elixir is that language, and this
has already landed once: ops_inner used the byte-less form and returned
a bare file-level space for every Elixir input (#1130).

Note for reviewers: the issue argued from metrics::opens_member_scope
(src/metrics/mod.rs:106), which #1203 deleted in ac437bb1 the day
before. The hazard survived — it now runs through
spaces::compute::open_func_space — so this fixes it on a corrected
premise, and the issue body has been updated. The issue's prescribed test
shape was also not buildable: Checker / Getter methods are static and
monomorphised per parser, and AstInner offers no LANG-generic way to
invoke them against a node, so building it meant growing production macro
surface to host a test. The sweep asserts the same implication one step
downstream, on the serialized space kind.

Verification

Every measured claim was checked by perturbation rather than assertion:

  • Reverting open_func_space to the byte-less get_space_kind fails
    exactly 12 lib tests, all Elixir. containers_emit_npm_and_npa and
    function_spaces_emit_neither are among them, so the new sweep is
    not uniquely responsible for that case — its independent value is a
    partial-degradation selector hole, and the doc comment says so rather
    than overclaiming.
  • /audit-tests found the new sweep had the vacuity hole its three
    siblings already guard against: it iterated every emitted space without
    checking any existed below the always-Unit root. Neutering the Elixir
    fixture to x = 1 made the unguarded sweep pass. Fixed in
    303509f5.
  • Two independent /code-review passes returned no findings, each
    re-measuring the K&R claims against real parser output.

make pre-commitBCA_GATE: pass (gate=pre-commit).

Coverage: the batch adds 135 lines of Rust, of which 123 are in test-only
files cargo llvm-cov does not place in the denominator and 12 are
comment lines in src/node.rs (98.79%, above the 96.62% project figure),
with zero overlap against that file's uncovered set. No measured
production line ships, so patch coverage cannot sit below project
coverage.

Fixes #1204
Fixes #1209
Fixes #1212

dekobon added 5 commits August 5, 2026 14:28
#1208 moved the four C-family `get_func_space_name` impls — the
method's only production callers — onto the shared declarator walk in
`crate::c_declarator`, leaving `first_occurrence` alive only under an
`#[allow(dead_code)]` pending this decision. Remove it rather than
re-justify the allow: a walk with no production caller is a contract
nothing depends on, and its two dedicated tests existed solely to pin
that contract.

The one remaining use was a test utility in `spaces_tests.rs` locating
a nested `function_definition`. `preorder().find(..)` serves it with no
loss: `Preorder` seeds its stack with the subtree root and keeps the
same `stack[first_child..].reverse()` bookkeeping, so it is the same
root-inclusive pre-order and `find` short-circuits at the same node.
`first_child` is not a substitute — it scans one level only.

Also corrects the `child_scan_cursors` consumer count in `src/node.rs`
and `docs/development/benchmarking.md` from six to five. The #1112
figures in that doc are historical measurements and are left intact,
re-attributed to `act_on_node`.

Fixes #1212
A pre-ANSI (K&R) function definition whose return type wraps the
declarator -- `int *f(a) int a; { ... }`, and likewise `char **`,
`struct S *`, or a `static` pointer return -- opens no function space
under C or Objective-C.

This is upstream, not a dispatch gap. In tree-sitter-c 0.24.2 the
old-style function definition nests its declarator inside the old-style
declarator, so an outer `pointer_declarator` is unreachable and GLR
prefers a plain `declaration` that swallows the first parameter
declaration, orphaning the body as a bare `compound_statement`. No
ERROR node is produced -- the parse silently succeeds wrongly.
`src/checker/c.rs` already enumerates both `function_definition`
aliases; the node it would match is never emitted, so there is nothing
to add there and no workaround worth a hand-rolled slice of a grammar
for a form C23 removed.

The consequence worth recording is that the orphaned body's decisions
are charged to the file's unit space: the wrapped fixture reports
`cyclomatic.sum` 2 with `spaces == []`, a file-level count that no
function-level row accounts for.

The paired test is a drift marker, not an endorsement. It pins the
unwrapped K&R form as a working control and the wrapped form's current
numbers as the bug, so a grammar bump that fixes the parse fails in CI
rather than shifting metrics silently. C/C++ and Mozcpp open no space
for either form -- tree-sitter-cpp has no K&R rule at all -- so file
routing is not a mitigation.

Fixes #1209
Grammar-dispatch rule 7 listed three predicate pairs to cross-walk and
omitted the pair that fails most quietly. The three it named disagree
into a wrong count, which a snapshot diff surfaces. `is_func_space` vs
`get_space_kind` disagree into an absent key: a node the walker promoted
but the getter left `Unknown` is not a member scope, so its space
serializes no npm/npa block at all, which reads exactly like a language
that legitimately has no containers.

Add that fourth bullet plus a subsection on the `_with_code` spelling.
The trap there is silent because the trait defaults forward: both
`is_func_space_with_code` and `get_space_kind_with_code` discard `code`
and `ancestors` and call the byte-less form, so the two spellings behave
identically everywhere except where a language overrides one, which is
precisely where the answer matters. Elixir is that language and
currently the only one, since `defmodule` / `def` parse as plain `Call`
nodes told apart by their target text. This has already landed once:
`ops_inner` opened on the byte-less pair and returned a bare file-level
space for every Elixir input (#1130).

A sweep over the existing fixtures asserts no emitted space carries
`SpaceKind::Unknown`. That is the same implication one step downstream,
where it is observable without growing `AstInner` a dispatch arm to host
a test. It also closes a selector hole: `function_spaces_emit_neither`
filters on `kind == Function`, so a space that should be a function but
classifies `Unknown` drops out of the filter rather than failing.

Reverting `open_func_space` to the byte-less call fails 12 lib tests,
all on Elixir. No production code changes.

Fixes #1204
`no_space_is_emitted_with_an_unknown_kind` iterated every emitted space
without checking any existed below the file root. `flatten` always
pushes that root and the root is always `Unit`, so a fixture whose
grammar stopped opening nested spaces would have satisfied the sweep
while exercising no classifier decision at all — the vacuity its three
siblings already guard against, via `!functions.is_empty()` and
`only_space`'s exactly-one check.

Measured rather than assumed: neutering the Elixir fixture to `x = 1`
leaves `[(Unit, None)]`, and the sweep passed. With the guard it fails
naming the spaces it saw.
Consolidated entry from the #1204 / #1209 / #1212 batch. Only #1209 is
user-facing: #1212 removes a `pub(crate)` method with no behaviour
change, and #1204 is agent guidance plus a `#[cfg(test)]` sweep.
@dekobon
dekobon merged commit 0f1c432 into main Aug 6, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant