fix: exclude bodiless tags-path declarations from call targets - #7
Conversation
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>
|
|
Scope note, so this PR's claim is precisely bounded. This correctly closes #5's diagnosis (declaration and implementation competing as two candidates I verified that separately and it is not interface-specific, not framework-specific, and not Now tracked as #8, with the reproduction and a suggested direction. #5 has been corrected to explain Two independent reviews found #8 the same day from opposite directions (an adversarial review of Worth recording why the suite was blind to #8: every Java call site in |
1. Summary
This PR changes:
Classifier::is_call_target'sTaggedarm (src/graph/emit.rs) now excludes a captureddefinition from the resolver's call-target candidate set when its node kind is a verified
bodiless-declaration shape (Java
method_declaration, TypeScriptmethod_signature/abstract_method_signature/function_signature) AND it has nobodychild.tests/fixtures/java-interface-repo/andtests/fixtures/typescript-interface-repo/, with committed goldens, proving asingle-implementation interface method now resolves end-to-end.
src/graph/emit.rsandsrc/graph/tests.rsmirroring the Rust fix's testsuite from fix: extract bodiless trait methods and stop binary content reaching chunks #4, plus one documenting a related, un-fixed limitation (see Scope below).
It solves:
implementation produced no
callsedge at all: the declaration and the implementation bothregistered as call targets under the same bare name, so the resolver saw two candidates with
no disambiguating qualifier and dropped the call as ambiguous.
2. Intent
The intent of this PR is:
3. Scope
In Scope
method_declaration(interface members,abstractmethods) — the reported case.method_signature(interface members),abstract_method_signature(abstract-classmembers), and
function_signature(ambient/overload declarations) — verified to have the exactsame bodiless-node-kind shape against
tree-sitter-typescript'snode-types.json, so includedper the issue's "worth checking in the same pass" note.
const/let-assigned arrow function (variable_declarator,assignment_expression,pairwrapper nodes) anchor
@definition.functionon a node with nobodyfield concept at all.A blind "no body child ⇒ declaration" rule misclassified those as bodiless declarations and
dropped them as call targets — caught by the
javascript-repogolden gaining a spuriousButton()-call drop during development, before the check was scoped to a verified node-kindallowlist. Not a new fixture (the drift showed up directly against the existing golden), but
worth flagging as in-scope collateral this PR had to get right.
Out of Scope
pass/...), sofunction_definition.bodyis a required field and there is no bodiless shape to detect. Confirmed via
tree-sitter-python'snode-types.json, not assumed.resolve::pick's ambiguity policy. This surfaced a real, separate, pre-existingfinding during development (see Verification below) that is deliberately left alone and
documented with a test, not fixed, per this PR's explicit scope boundary.
4. Verification
I verified this change by:
Commands run:
Results:
Golden changes, hand-verified against fixture source (not just regenerated): the ONLY
goldens that changed are the two brand-new ones (
java-interface-repo,typescript-interface-repo) — every one of the six pre-existing per-language goldens(
java-repo,python-repo,javascript-repo,typescript-repo,tsx-repo,polyglot-repo)came back byte-identical to
main, confirmed viagit status --shortafter theUPDATE_GOLDEN=1regeneration (not merely re-running the diff assertion). This means the fix isadditive-only for every already-committed fixture: no previously-resolving
callsedge changedor disappeared.
For the two new goldens, I read every node/edge against the fixture source line by line:
java-interface-repo:Greeter.java#2:greet(the interface declaration, line 2 ofinterface Greeter { String greet(); }) has amethodedge fromGreeterbut is the targetof zero
callsedges.EnglishGreeter.java#2:greet(the implementation, line 2 ofclass EnglishGreeter implements Greeter { public String greet() {...} }) is the target ofBOTH
Main.java#3:run(qualified callEnglishGreeter.greet()) andMain.java#11:runBare(bare call
greet()) — this isMain.run -> EnglishGreeter.greet, the exact edge the issuesays should exist.
typescript-interface-repo: identical shape —src/greeter.ts#2:greet(declaration) has amethodedge and zero incomingcalls;src/english-greeter.ts#2:greet(implementation) isthe target of both
run()'s qualified call andrunBare()'s bare call.Negative case verified not to regress:
java_repo_run_bare_ambiguous_build_is_dropped(the existing
runBare()ambiguity intests/fixtures/java-repo, wherebuildis defined onBOTH
WidgetandGadget) still passes — confirmed by thejava-repogolden beingbyte-identical to before this change. Two real implementations remain genuinely ambiguous; this
fix narrows the candidate set only by removing bodiless declarations, never by guessing between
multiple real implementations.
A genuine finding, deliberately not fixed here (see Scope): the issue's own literal
reproduction —
void run(Greeter g) { g.greet(); }, calling through an interface-typedvariable rather than a class name — still produces no
callsedge after this fix. I tracedthis to
resolve::pick's single-candidate branch: it rejects a candidate whosescopedoesn'ttextually equal the call's qualifier, and the tags path's
qualifier_from_callee_nodesets thatqualifier to the raw receiver identifier (here
g, the parameter name — notEnglishGreeter,the implementing type). There is no type inference on the tags path, so a receiver variable can
essentially never textually match the type that defines the method it calls — this is a
separate, pre-existing gap unrelated to the declaration/call-target distinction this PR fixes,
and touching
resolve::pickis explicitly out of scope for #5. I addedjava_call_through_an_interface_typed_variable_needs_a_qualifier_matchinsrc/graph/tests.rsto document this precisely (asserting the CURRENT, unresolved behaviour,not desired behaviour) rather than leave it as a silent gap. Happy to file this as a follow-up
issue if desired.
5. Screenshots / Evidence
Not applicable — this is a library-internal graph-extraction fix with no UI surface. Evidence is
the command output above, plus the two new committed golden files
(
tests/golden/java-interface-repo.graph.json,tests/golden/typescript-interface-repo.graph.json).6. Risk Assessment
Risk level:
Potential risks:
callsedges that were previously (incorrectly) dropped — any downstreamconsumer relying on the absence of these edges would see new data. This is the intended fix,
not a side effect.
is_call_target'sTaggedarm is deliberately narrow (Javamethod_declaration; TSmethod_signature/abstract_method_signature/function_signature)rather than a blanket "no body ⇒ declaration" rule, specifically because the blanket version
was proven wrong during development (see Scope) — a future grammar/language addition with a
genuinely bodiless definition shape will need its own explicit entry in this list, not silent
coverage.
Mitigation:
git status, notjust the comparison assertion) — zero regression to already-resolving calls.
container_repos(real-world Java/TS repo extraction) anddeterminism_holds_across_all_committed_language_goldensboth pass, exercising the changeagainst code this repo doesn't control.
graph/emit.rsitself at 99.84% region / 100% function coverage),well above the 85% CI floor.
7. AI Usage Declaration
AI was used for:
Human verification:
8. Reviewer Focus
Please focus your review on: