fix: extract bodiless trait methods and stop binary content reaching chunks - #4
Merged
Conversation
…chunks Closes #1 and #2 — two gaps in the extraction path, both pre-existing. #1 Trait methods declared without a default body parse as `function_signature_item`, not `function_item`, and were classified as nothing at all — so a Rust trait interface contributed zero callable symbols to the graph AND no chunk to semantic search. They are now definitions: a node, a `method` edge from the trait, and a chunk, with the same `chunk_type` as a bodied function (a new value would leak into every consumer's stored data for no reader benefit). They are deliberately NOT call targets. The resolver is precision-favouring: several same-named candidates with no qualifier are dropped, not fanned out. Had declarations joined the candidate set, a trait with exactly one impl would have gone from one candidate to two and every call to that method would have stopped resolving — fixing a missing-symbol bug by silently deleting `calls` edges. A call dispatches to an implementation, never to a declaration. #2 `chunk_file` rejected binary content via a NUL sniff, but the graph-enabled walk never calls it — it parses the tree itself and falls back straight to `chunk_text`/`window_chunks`, which had no guard. A binary blob that happens to be valid UTF-8 (NUL is a legal codepoint) was windowed with raw NUL bytes in `Chunk::content`, on the one path production runs. PostgreSQL's `text` rejects NUL outright, so this failed at persist time far from its cause. The guard is now a shared `is_binary` applied where a chunk is produced (`chunk_text`) and in the walk before either consumer — so the graph never ingests binary either, not just the chunker. The sample-repo golden gains exactly one node and one `method` edge (the `Shape::describe` declaration it always contained). Additions only: no edge removed, no `calls` edge lost. Coverage 93.8% -> 95.4%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Raised by adversarial review of this PR: a file rejected by the new is_binary guard incremented no counter at all, so it vanished from the walk summary. Before this PR the same file WAS counted into files_chunked (with NUL-contaminated content), so the fix traded a data bug for an observability gap. Without the counter, 'this repo has fewer indexable files than expected' and 'a binary asset is misnamed with a source extension' look identical from the summary line, and only the second is actionable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 7, 2026
stephane-segning
added a commit
that referenced
this pull request
Aug 8, 2026
Closes #5 — the tags-path twin of the Rust fix in #4. A Java interface method (or abstract method) and a TypeScript interface/abstract-class member both parse as an ordinary tagged definition, so they registered as call targets under the same bare name as their implementation. A single-implementation interface therefore had TWO same-named candidates and no qualifier to disambiguate them, so the precision-favouring resolver dropped every call to it as ambiguous. `Classifier::is_call_target`'s `Tagged` arm now excludes a definition when BOTH (a) its node kind is one of the specific shapes verified (via each grammar's `node-types.json`) to be legitimately bodiless — Java `method_declaration`, TypeScript `method_signature` / `abstract_method_signature` / `function_signature` — AND (b) it has no `body` child. The check is scoped to that node-kind allowlist rather than applied to every `Tagged` definition, because several JS `tags.scm` patterns anchor `@definition.function` on a wrapper node with no `body` field concept of its own (`variable_declarator` for `const f = () => {}`, `assignment_expression`, `pair`) even though the function value they wrap always has a body — treating those as bodiless would have wrongly dropped every const-arrow-function call target (caught by the `javascript-repo` golden gaining a spurious drop before the list was narrowed). Python is unaffected: `function_definition.body` is a required field even for a `pass`/`...`-bodied abstract method, so there is no bodiless shape to detect there. Two new fixtures (`java-interface-repo`, `typescript-interface-repo`) prove the fix end-to-end: a single-implementation interface method now resolves, both via a qualified call (matching this suite's existing `Widget.build()` house style) and a bare call. All six pre-existing per-language goldens are byte-identical to before this change — confirmed via `git status` after `UPDATE_GOLDEN=1`, not just re-running the comparison — so this is additive only, no regression to already-resolving calls. FINDING, not fixed here (out of scope per the resolver-ambiguity-policy exclusion): the issue's own literal reproduction — calling through an interface-typed variable, `g.greet()` — still does not resolve after this fix. `resolve::pick`'s single-candidate branch rejects a candidate whose `scope` doesn't textually equal the call's qualifier, and the tags path sets that qualifier to the raw receiver identifier (`g`, the parameter name) — there is no type inference, so a receiver variable can never textually match the type that defines the method it calls. Documented with a dedicated test (`java_call_through_an_interface_typed_variable_needs_a_qualifier_match`) rather than silently worked around. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
1. Summary
This PR changes:
fn greet(&self);) is now extracted: it gets a graph node, amethodedge from its trait, and a chunk. It parses asfunction_signature_item, notfunction_item, and was classified as nothing at all.chunk_text) and into the walk before either consumer, so it can no longer be bypassed by entering through a different door.tests/golden/sample-repo.graph.json(additions only, hand-verified — see §4).It solves:
2. Intent
The intent of this PR is:
3. Scope
In Scope
function_signature_itemclassification, and the call-target seam that keeps it out of resolutionis_binaryguard applied at chunk-production and in the walkOut of Scope
method_signature/abstract members go through thetags.scmpath, notinteresting_node, and are left exactly as they are. All 6 other golden files are unchanged by this PR, which is the evidence they were not touched. Whether a tags-driven language has the same declaration-vs-implementation ambiguity was left open here —Classifier::is_call_targetleavesTaggedas-is rather than guessing per-grammar. Adversarial review has since answered it: Java does have the identical bug, today.tree-sitter-java'stags.scmcaptures a bodilessmethod_declarationexactly like a concrete one, so a single-impl interface call is dropped as ambiguous. Confirmed pre-existing (byte-identical againstmain), so it is not a regression from this PR — tracked as Java interface methods: single-impl calls are dropped as ambiguous (the tags-path twin of #1) #5.interface_declarationchunking, whichinteresting_nodealso omits. Noticed while here; genuinely a different bug, not filed as part of these two.4. Verification
I verified this change by:
Commands run:
Results:
The golden diff, hand-verified.
sample-repois the only fixture containing a Rust trait, and it always containedtrait Shape { fn describe(&self) -> f64; }at line 31 with an impl at line 35. The regenerated golden is additions only:Nothing was removed. The pre-existing
impl --method--> src/shapes.rs#35:describeis untouched, and nocallsedge changed — which is exactly the property the design in §2 exists to protect. The other 6 golden files (java-repo,javascript-repo,polyglot-repo,python-repo,tsx-repo,typescript-repo) are byte-identical — they are asserted by the 13 test functions intests/language_goldens.rs, which is a different number and was originally miscited here as “13 goldens”.Tests added:
5. Screenshots / Evidence
Add evidence here:
tests/robustness.rs's NUL test andsrc/graph/emit.rs's trait-method test both used to document the gap on purpose.Adversarial review outcome. An independent reviewer drove the public API against hand-built fixtures on this branch and on a clean clone of
main, to separate "introduced here" from "pre-existing". It could not break theis_call_targetdesign for Rust (generic bounds, cross-file calls, two unrelated traits with a colliding method name, default-body-plus-override all resolve identically pre/post), and independently re-derived the golden diff as complete and correct. It produced two acted-on findings:WalkStatshad no counter for the new binary-skip path, so a rejected file vanished from the summary line entirely — a fair hit, since before this PR that file was counted intofiles_chunked(with contaminated content). Fixed in this PR:files_skipped_binary, logged and asserted.It also caught a factual imprecision in this description ("13 language goldens" — 13 is the test-function count; there are 6 other golden files), corrected above.
6. Risk Assessment
Risk level:
Potential risks:
graph_find_symbolwill return declarations alongside implementations for the same name. Consumers that assumed one node per method name will now see two.Mitigation:
7. AI Usage Declaration
AI was used for:
Human verification:
Both bugs were independently reproduced with a minimal driver before any code was changed, and the golden diff was read line by line against the fixture source rather than regenerated and trusted.
8. Reviewer Focus
Please focus your review on:
Specifically:
pick(), which is more precise but pushes language-specific knowledge into the language-agnostic resolver.pick().is_binarysniff is a 512-byte prefix. A file with clean text followed by a NUL at byte 600 still gets chunked. Pre-existing, now tested and documented — but worth deciding whether the bound is right rather than inherited.