Skip to content

[Bug]: An unqualified call in Java, C#, Scala and C++ is bound to the last same-named method of the file, not the one in the caller's class #4243

Closed

Description

@Yyunozor

Pre-flight checks

  • I have checked the Troubleshooting section in the README

What happened?

Inside a method, a call written b() gets a calls edge to whichever method named b the file declares last, whatever class it belongs to, and the edge is EXTRACTED. With a nested Inner.b() declared after Outer.b(), the b() in Outer.a() points at Inner.b(). Declare Inner first and the edge is right, so the result depends on declaration order. A later unrelated class that repeats the name captures the call the same way.

On a real project: in Guava's Synchronized.java, 195 of the 213 delegate() call edges point at SynchronizedTable.delegate, the class declared last, instead of the delegate() of their own class.

Expected: the b of the nearest class that declares one: the caller's own class and its in-file bases, then the classes around it.

Cause, in graphify/extractors/engine.py: for these languages the unqualified call falls to tgt_nid = label_to_nid.get(callee_name) in walk_calls, and label_to_nid[normalised] = n["id"] is overwritten by each node with the same label. The same defect was fixed for self/this calls in Python, JS/TS, Swift and Ruby (_self_call_target: e519288, 60d905a, c470cb5, 74fa510); PHP joined that set in d2b5e19. method_owner and methods_by_owner are only built for the languages of _SELF_CALL_LANGUAGES.

Steps to reproduce

mkdir demo && cd demo
cat > Outer.java <<'EOF'
public class Outer {
    void a() { b(); }
    void b() {}
    static class Inner {
        void b() {}
    }
}
EOF
cat > Outer.cs <<'EOF'
public class Outer
{
    public void A() { B(); }
    public void B() {}
    public class Inner
    {
        public void B() {}
    }
}
EOF
cat > Outer.scala <<'EOF'
class Outer {
  def a(): Unit = b()
  def b(): Unit = ()
  class Inner {
    def b(): Unit = ()
  }
}
EOF
cat > Outer.cpp <<'EOF'
class Outer {
public:
    void a() { b(); }
    void b() {}
    class Inner {
    public:
        void b() {}
    };
};
EOF
graphify update .
python - <<'EOF'
import json
g = json.load(open("graphify-out/graph.json"))
nodes = {n["id"]: n for n in g["nodes"]}
links = g.get("links") or g.get("edges")
owner = {e["target"]: e["source"] for e in links if e.get("relation") == "method"}
for e in links:
    if e.get("relation") == "calls" and nodes[e["source"]]["label"].strip(".()").lower() == "a":
        print(nodes[e["source"]]["source_file"], "Outer.a() ->", nodes[owner[e["target"]]]["label"] + "." + nodes[e["target"]]["label"].strip(".()") + "()")
EOF

Error output or graph output

Outer.cpp Outer.a() -> Inner.b()
Outer.cs Outer.a() -> Inner.B()
Outer.java Outer.a() -> Inner.b()
Outer.scala Outer.a() -> Inner.b()

All four should read Outer.b() (Outer.B() in C#).

Graphify version

0.9.80 (v8 @ 6478eb7)

Operating System

macOS

Python Version

3.12

Installation Method

built from source (git clone), uv sync --all-extras

Additional Environment Details

AST-only path (graphify update), no API key involved.

Additional context

I have a fix with tests and can open a PR (three commits: the caller's own class, its in-file bases, the classes around it). An unqualified call looks in the caller's class and its in-file bases (also a base declared further down the file, and the type after the new of an anonymous class), then in each class around it (an anonymous class steps out through the method that holds it), and keeps the file-wide lookup when no class on that path declares the name. Kotlin is not part of it: Kotlin falls back to a top-level function when a member's parameters do not fit the call, which a name-only lookup cannot tell.

Related: #2041 (umbrella), point 4: bare calls resolve by label text. This issue is limited to unqualified calls made from inside a class in Java, C#, Scala and C++; calls with a receiver are not touched.

Activity

  1. github-actions commented on Oct 8, 2026

    @github-actions

    Thanks for opening this issue, @Yyunozor. A maintainer will take a look soon.

    If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.

    To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.

  2. Yyunozor commented on Oct 8, 2026

    @Yyunozor
    ContributorAuthor

    PR opened: #4252

  3. safishamsi commented on Oct 9, 2026

    @safishamsi
    Member

    Fixed by #4252 (thanks @Yyunozor), shipped in v0.9.83. Closing as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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