Summary
A Java interface method with a single implementation produces no calls edge. The declaration
and the implementation both register as call targets under the same bare name, so the resolver sees
two candidates, finds no disambiguating qualifier, and drops the call as ambiguous.
This is the exact asymmetry that #1 fixed for Rust. Classifier::is_call_target excludes Rust's
bodiless function_signature_item, but its Tagged(_) arm returns true unconditionally — and
tree-sitter-java's bundled tags.scm captures
(method_declaration name: (identifier) @name) @definition.method
unconditionally. An abstract interface method is a method_declaration exactly like a concrete one,
so nothing distinguishes them.
Reproduction
// Greeter.java
interface Greeter { String greet(); }
// EnglishGreeter.java
class EnglishGreeter implements Greeter {
public String greet() { return "hello"; }
}
// Main.java
class Main {
void run(Greeter g) { g.greet(); }
}
Observed: no calls edge referencing either greet node is emitted.
Expected: a calls edge from Main.run to EnglishGreeter.greet, matching the Rust behaviour
established in #1.
Status
Pre-existing, not a regression. Verified by running the identical fixture against this branch and
against a clean clone of main — byte-identical output. #1 neither introduced nor worsened it; it
declined to close it, on the grounds that guessing per-grammar semantics was out of scope for that
change. This issue is that scope, made explicit.
Why it was not fixed with #1
The Rust fix keys off a distinct tree-sitter node kind (function_signature_item vs
function_item), which is unambiguous. There is no equivalent signal in the tags query result as
currently consumed — tags::extract records (kind, name) per node id and discards whether the
captured method_declaration had a body child.
Suggested direction
The information is present in the tree; it is just dropped. A method_declaration with no body
field, or one whose ancestor is an interface_declaration, is a declaration. Options, roughly in
order of preference:
- Have
Classifier::is_call_target's Tagged arm inspect the node — e.g. treat a captured
definition with no body child as a declaration. This generalises across grammars that model
bodies the same way (Java, and TypeScript's method_signature) without hardcoding a language.
- Carry a
has_body flag through tags::TaggedSymbols at extraction time.
Either way it wants the same regression test #1 got: a single-impl interface whose call still
resolves to the implementation, so a future change cannot silently delete the edge again.
Also worth checking in the same pass
TypeScript method_signature / abstract_method_signature and Python abstractmethod bodies
(.../pass) plausibly have the same shape. They were not reproduced here — only Java was — so
treat them as candidates to verify, not as confirmed.
Credit
Found by an adversarial review of #1, which reproduced it with a driven fixture against both branches
rather than inferring it from the diff.
Summary
A Java interface method with a single implementation produces no
callsedge. The declarationand the implementation both register as call targets under the same bare name, so the resolver sees
two candidates, finds no disambiguating qualifier, and drops the call as ambiguous.
This is the exact asymmetry that #1 fixed for Rust.
Classifier::is_call_targetexcludes Rust'sbodiless
function_signature_item, but itsTagged(_)arm returnstrueunconditionally — andtree-sitter-java's bundledtags.scmcapturesunconditionally. An abstract interface method is a
method_declarationexactly like a concrete one,so nothing distinguishes them.
Reproduction
Observed: no
callsedge referencing eithergreetnode is emitted.Expected: a
callsedge fromMain.runtoEnglishGreeter.greet, matching the Rust behaviourestablished in #1.
Status
Pre-existing, not a regression. Verified by running the identical fixture against this branch and
against a clean clone of
main— byte-identical output. #1 neither introduced nor worsened it; itdeclined to close it, on the grounds that guessing per-grammar semantics was out of scope for that
change. This issue is that scope, made explicit.
Why it was not fixed with #1
The Rust fix keys off a distinct tree-sitter node kind (
function_signature_itemvsfunction_item), which is unambiguous. There is no equivalent signal in the tags query result ascurrently consumed —
tags::extractrecords(kind, name)per node id and discards whether thecaptured
method_declarationhad abodychild.Suggested direction
The information is present in the tree; it is just dropped. A
method_declarationwith nobodyfield, or one whose ancestor is an
interface_declaration, is a declaration. Options, roughly inorder of preference:
Classifier::is_call_target'sTaggedarm inspect the node — e.g. treat a captureddefinition with no
bodychild as a declaration. This generalises across grammars that modelbodies the same way (Java, and TypeScript's
method_signature) without hardcoding a language.has_bodyflag throughtags::TaggedSymbolsat extraction time.Either way it wants the same regression test #1 got: a single-impl interface whose call still
resolves to the implementation, so a future change cannot silently delete the edge again.
Also worth checking in the same pass
TypeScript
method_signature/abstract_method_signatureand Pythonabstractmethodbodies(
.../pass) plausibly have the same shape. They were not reproduced here — only Java was — sotreat them as candidates to verify, not as confirmed.
Credit
Found by an adversarial review of #1, which reproduced it with a driven fixture against both branches
rather than inferring it from the diff.