fix(java): capture methods and calls inside anonymous class bodies

A Java/Groovy anonymous class (`new Runnable() { … }`) keeps its body as a
`class_body` hanging off an `object_creation_expression`. That node is not
a class_type and the function branch never recurses into bodies, so the
anonymous class's methods — and every call made inside them — got no nodes
at all. Anonymous listeners/callbacks are routine in Java, so this silently
dropped a large slice of the call graph (e.g. a listener's `onClick`
calling a sibling method vanished).

Mirror the Kotlin anonymous-object handling (#2347): scan a function body
for anonymous classes (without crossing a nested method/lambda boundary or
descending into a found one), emit an owner node per anonymous class, and
walk its class_body like the class branch so members and their calls flow
through the normal machinery. The base type is recorded only as the node's
label, not an implements/inherits edge — an anonymous class usually
realises a library interface/class whose kind is unknowable from this file,
so a guessed subtype relation is withheld (fail-closed).
This commit is contained in:
rajatnagda45 committed 2026-10-06 08:56:05 +05:30
1 parent 5c7b84792f
commit 0a6990c0ee
2 files changed
+152

No files matched your search

+50
View File
@@ -6397,6 +6397,56 @@ def _extract_generic(
if lit_body is not None:
for child in lit_body.children:
walk(child, parent_class_nid=obj_nid)
if config.ts_module in ("tree_sitter_java", "tree_sitter_groovy"):
# Java/Groovy anonymous classes (`new Runnable() { … }`): the
# body hangs off an `object_creation_expression` as a
# `class_body`. The function branch never recurses into
# bodies and object_creation_expression is not a class_type,
# so the anonymous class's methods — and every call inside
# them — got no nodes at all (the Java/Groovy twin of Kotlin
# #2347). Scan this body for anonymous classes without
# crossing a nested method/lambda boundary and without
# descending into a found one, then emit an owner node per
# anonymous class and walk its class_body like the class
# branch so members and their calls flow through the normal
# machinery. The base type is recorded only as the node's
# label, not an implements/inherits edge: an anonymous class
# usually realises a library interface/class whose kind is
# unknowable from this file, so a guessed subtype relation is
# withheld (fail-closed).
_jv_anon = []
_jv_stack = list(body.children)
while _jv_stack:
_jv_node = _jv_stack.pop()
if _jv_node.type in ("method_declaration",
"lambda_expression"):
continue
if _jv_node.type == "object_creation_expression" and any(
c.type == "class_body" for c in _jv_node.children
):
_jv_anon.append(_jv_node)
continue
_jv_stack.extend(_jv_node.children)
_jv_anon.sort(key=lambda n: n.start_byte)
for anon in _jv_anon:
anon_line = anon.start_point[0] + 1
type_node = anon.child_by_field_name("type")
base = _read_text(type_node, source).strip() if type_node else ""
obj_label = base or f"object@L{anon_line}"
obj_nid = _make_id(
func_nid, f"object:{obj_label}", f"L{anon_line}"
)
add_node(obj_nid, obj_label, anon_line)
add_edge(func_nid, obj_nid, "contains", anon_line)
callable_def_nids.add(obj_nid)
callable_class_nids.add(obj_nid)
anon_body = next(
(c for c in anon.children if c.type == "class_body"),
None,
)
if anon_body is not None:
for child in anon_body.children:
walk(child, parent_class_nid=obj_nid)
return
# JS/TS arrow functions and C# namespaces — language-specific extra handling
+102
View File
@@ -0,0 +1,102 @@
"""Java/Groovy anonymous-class members (the Java twin of Kotlin #2347).
`new Runnable() { ... }` keeps its body as a `class_body` hanging off an
`object_creation_expression`. That node is not a class_type and the function
branch never recurses into bodies, so the anonymous class's methods AND every
call inside them were silently dropped — a large loss given how routine
anonymous listeners/callbacks are in Java. The extractor now emits an owner
node per anonymous class (labeled after the base type), a `contains` edge from
the enclosing method, and walks the body like a class so members and their
calls flow normally. The subtype relation is deliberately NOT asserted: an
anonymous class usually realises a library interface/class whose kind is
unknowable from this file (fail-closed).
"""
from __future__ import annotations
import os
from pathlib import Path
from graphify.extract import extract
def _extract(tmp_path, files: dict[str, str]):
for name, body in files.items():
p = tmp_path / name
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(body)
old = os.getcwd()
try:
os.chdir(tmp_path)
r = extract([Path(n) for n in files], cache_root=tmp_path / ".cache")
finally:
os.chdir(old)
return r
def _edges(r, relation):
return {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == relation}
def _find(r, label, id_contains=""):
return next(n["id"] for n in r["nodes"]
if n["label"] == label and id_contains in n["id"])
_APP = {
"App.java": (
"class App {\n"
" void setup() {\n"
" Runnable r = new Runnable() {\n"
" public void run() { helper(); }\n"
" };\n"
" }\n"
" void helper() {}\n"
"}\n"
),
}
def test_anonymous_class_members_get_nodes_and_method_edges(tmp_path):
r = _extract(tmp_path, _APP)
obj_nid = _find(r, "Runnable", "object")
run = _find(r, ".run()", "object")
methods = _edges(r, "method")
assert (obj_nid, run) in methods, "anonymous-class member must hang off the owner"
# The owner itself is contained by the enclosing method.
setup = _find(r, ".setup()", "app_setup")
assert (setup, obj_nid) in _edges(r, "contains"), \
"the enclosing method contains the anonymous class"
def test_anonymous_class_member_call_resolves_to_enclosing_method(tmp_path):
r = _extract(tmp_path, _APP)
run = _find(r, ".run()", "object")
helper = _find(r, ".helper()", "app_helper")
assert (run, helper) in _edges(r, "calls"), \
"a call made inside an anonymous-class method was dropped"
def test_two_anonymous_classes_in_one_method_do_not_collide(tmp_path):
r = _extract(tmp_path, {
"App.java": (
"import java.util.Comparator;\n"
"class App {\n"
" void setup() {\n"
" Runnable a = new Runnable() {\n"
" public void run() { doWork(); }\n"
" };\n"
" Comparator<Integer> c = new Comparator<Integer>() {\n"
" public int compare(Integer x, Integer y) { return rank(x); }\n"
" };\n"
" }\n"
" void doWork() {}\n"
" int rank(Integer x) { return 0; }\n"
"}\n"
),
})
run = _find(r, ".run()", "object")
compare = _find(r, ".compare()", "object")
assert run != compare, "two anonymous classes collapsed onto one node"
calls = _edges(r, "calls")
assert (run, _find(r, ".doWork()", "app_dowork")) in calls
assert (compare, _find(r, ".rank()", "app_rank")) in calls