mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
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:
1 parent
5c7b84792f
commit
0a6990c0ee
2 files changed
+152
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user