Skip to content

Java interface methods: single-impl calls are dropped as ambiguous (the tags-path twin of #1) #5

Description

@stephane-segning

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions