fix: batch 2026-08-05 (#1204, #1209, #1212) - #1216
Merged
Merged
Conversation
#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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-outputsubmodule bump.What's here
8ab58c3dSearch::first_occurrenceand its#[allow(dead_code)]dd34b1c11c18af87_with_codesubsection togrammar-dispatch.md, with a fixture sweep303509f5/audit-tests)cb4945b0#1212 — remove
Search::first_occurrence#1208 moved the four C-family
get_func_space_nameimpls — its onlyproduction 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 nothingshipped.
Searchispub(crate), so there is no public API impact andSTABILITY.mdneeds no change.The one remaining utility use in
src/spaces_tests.rslocated a nestedfunction_definition, sofirst_childwas not a drop-in (it scans onelevel). It now uses
root.preorder().find(…), which is the sameleftmost-first, root-inclusive walk —
Preorderhas its own order pin inpreorder_matches_recursive_document_order.The
child_scan_cursorsconsumer count drops six to five in both placesthat state it.
docs/development/benchmarking.md's historical #1112measurement is re-attributed to
act_on_noderather than restated with aninvented number — and that re-attribution was checked by reverting
act_on_nodetochildren(), 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
CorObjective-C: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
ERRORnode: thedeclaration's wrapping declaratorswallows the first parameter declaration,
int b;becomes a secondtop-level
declaration, and the body is orphaned as a barecompound_statement. Intree-sitter-c0.24.2 the old-style functiondefinition nests its declarator inside the old-style declarator, so an
outer
pointer_declaratoris unreachable.The impact is not uniform invisibility — the orphaned body's decisions are
charged to the file's unit space (
cyclomatic.sum2 withnom.functions0), a file-level number no function-level row accounts for.Two facts not in the original report:
LANG::Objcshares the defect, andCpp/Mozcppopen no space for either K&R form, so.h/.mmrouting is not a mitigating factor.
Resolution is option 1 — document, do not work around.
src/checker/c.rsalready enumerates both
function_definitionaliases correctly; the nodeit 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
ifrather than being aparse-failure constant.
#1204 —
grammar-dispatch§7 and the_with_codespelling§7 listed three predicate pairs and not
is_func_space↔get_space_kind. That pair is worth its own entry because the threeexisting 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
Unknownis not a member scope, so its spaceserializes no
npm/npablock, which reads exactly like a languagewith no containers.
The new subsection states why the
_with_codetrap is silent: thedefaults 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_innerused the byte-less form and returneda 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 inac437bb1the daybefore. The hazard survived — it now runs through
spaces::compute::open_func_space— so this fixes it on a correctedpremise, and the issue body has been updated. The issue's prescribed test
shape was also not buildable:
Checker/Gettermethods are static andmonomorphised per parser, and
AstInneroffers no LANG-generic way toinvoke 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:
open_func_spaceto the byte-lessget_space_kindfailsexactly 12 lib tests, all Elixir.
containers_emit_npm_and_npaandfunction_spaces_emit_neitherare among them, so the new sweep isnot uniquely responsible for that case — its independent value is a
partial-degradation selector hole, and the doc comment says so rather
than overclaiming.
/audit-testsfound the new sweep had the vacuity hole its threesiblings already guard against: it iterated every emitted space without
checking any existed below the always-
Unitroot. Neutering the Elixirfixture to
x = 1made the unguarded sweep pass. Fixed in303509f5./code-reviewpasses returned no findings, eachre-measuring the K&R claims against real parser output.
make pre-commit→BCA_GATE: pass (gate=pre-commit).Coverage: the batch adds 135 lines of Rust, of which 123 are in test-only
files
cargo llvm-covdoes not place in the denominator and 12 arecomment 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