mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
fix(java): resolve calls to inherited methods and super
Member calls were only matched against the receiver's own type, so this.start(), super.close() or worker.start() got no edge when the method lives in a superclass. Walk up the extends chain and take the first declaration. No edge if the chain leaves the project. Fixes #3931
This commit is contained in:
1 parent
1cd9a36c0c
commit
a2da367fc7
3 files changed
+141
-8
No files matched your search
+36
-6
@@ -4578,7 +4578,7 @@ def _resolve_java_member_calls(
|
||||
) -> None:
|
||||
"""Resolve Java member calls against the receiver's declared type.
|
||||
|
||||
Explicit type receivers and ``this`` are exact. Fields declared on the
|
||||
Explicit type receivers, ``this`` and ``super`` are exact. Fields declared on the
|
||||
caller's class plus method parameters and explicit locals are inferred from
|
||||
the extractor's method-scoped type table. A missing or ambiguous receiver
|
||||
type is skipped rather than falling back to a bare method-name match.
|
||||
@@ -4637,6 +4637,33 @@ def _resolve_java_member_calls(
|
||||
queue.extend(inherits_bases.get(cls, []))
|
||||
return None
|
||||
|
||||
def _jvm_bases(type_nid: str) -> list[str] | None:
|
||||
bases = inherits_bases.get(type_nid, [])
|
||||
for base in bases:
|
||||
family = _lang_family(node_by_id.get(base, {}).get("source_file"))
|
||||
if base == type_nid or family != "jvm":
|
||||
return None
|
||||
return bases
|
||||
|
||||
def _method_on_type_or_bases(type_nid: str, callee_key: str) -> str | None:
|
||||
hits: set[str] = set()
|
||||
seen: set[str] = set()
|
||||
frontier = [type_nid]
|
||||
while frontier:
|
||||
nid = frontier.pop()
|
||||
if nid in seen:
|
||||
continue
|
||||
seen.add(nid)
|
||||
declared = method_index.get((nid, callee_key))
|
||||
if declared:
|
||||
hits |= declared
|
||||
continue
|
||||
bases = _jvm_bases(nid)
|
||||
if bases is None:
|
||||
return None
|
||||
frontier.extend(bases)
|
||||
return next(iter(hits)) if len(hits) == 1 else None
|
||||
|
||||
for result in per_file:
|
||||
for raw_call in result.get("raw_calls", []):
|
||||
if raw_call.get("lang") != "java" or not raw_call.get("is_member_call"):
|
||||
@@ -4653,6 +4680,12 @@ def _resolve_java_member_calls(
|
||||
exact = True
|
||||
if not type_nid:
|
||||
continue
|
||||
elif receiver == "super":
|
||||
superclasses = _jvm_bases(enclosing_type.get(caller, ""))
|
||||
if not superclasses or len(superclasses) != 1:
|
||||
continue
|
||||
type_nid = superclasses[0]
|
||||
exact = True
|
||||
else:
|
||||
type_name = raw_call.get("receiver_type")
|
||||
if not type_name and receiver[:1].isupper():
|
||||
@@ -4680,11 +4713,8 @@ def _resolve_java_member_calls(
|
||||
continue
|
||||
type_nid = type_defs[0]
|
||||
|
||||
method_nids = method_index.get((type_nid, key(callee)), set())
|
||||
if len(method_nids) != 1:
|
||||
continue
|
||||
method_nid = next(iter(method_nids))
|
||||
if method_nid == caller or (caller, method_nid) in existing_pairs:
|
||||
method_nid = _method_on_type_or_bases(type_nid, key(callee))
|
||||
if not method_nid or method_nid == caller or (caller, method_nid) in existing_pairs:
|
||||
continue
|
||||
existing_pairs.add((caller, method_nid))
|
||||
all_edges.append({
|
||||
|
||||
@@ -6427,8 +6427,8 @@ def _extract_generic(
|
||||
is_member_call = True
|
||||
if receiver.type == "identifier":
|
||||
member_receiver = _read_text(receiver, source)
|
||||
elif receiver.type == "this":
|
||||
member_receiver = "this"
|
||||
elif receiver.type in ("this", "super"):
|
||||
member_receiver = receiver.type
|
||||
elif receiver.type == "field_access":
|
||||
owner = receiver.child_by_field_name("object")
|
||||
field = receiver.child_by_field_name("field")
|
||||
|
||||
@@ -282,3 +282,106 @@ def test_unqualified_call_still_resolves(tmp_path: Path):
|
||||
other = _find(result, ".other()", "checkout")
|
||||
assert (run, helper) in calls
|
||||
assert (run, other) in calls
|
||||
|
||||
|
||||
def test_inherited_method_resolves_for_this_field_and_parameter_receivers(tmp_path: Path):
|
||||
calls, result = _calls(tmp_path, {
|
||||
"Base.java": "class Base { void start() {} }\n",
|
||||
"Worker.java": (
|
||||
"class Worker extends Base {\n"
|
||||
" void viaThis() { this.start(); }\n"
|
||||
"}\n"
|
||||
),
|
||||
"Service.java": (
|
||||
"class Service {\n"
|
||||
" Worker worker;\n"
|
||||
" void viaField() { worker.start(); }\n"
|
||||
" void viaParam(Worker w) { w.start(); }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
|
||||
start = _find(result, ".start()", "base_start")
|
||||
assert (_find(result, ".viaThis()", "worker_viathis"), start) in calls
|
||||
assert (_find(result, ".viaField()", "service_viafield"), start) in calls
|
||||
assert (_find(result, ".viaParam()", "service_viaparam"), start) in calls
|
||||
|
||||
|
||||
def test_nearest_declaration_on_the_superclass_chain_wins(tmp_path: Path):
|
||||
calls, result = _calls(tmp_path, {
|
||||
"Chain.java": (
|
||||
"class Root { void start() {} void stop() {} }\n"
|
||||
"class Middle extends Root { void stop() {} }\n"
|
||||
"class Leaf extends Middle {}\n"
|
||||
"class Client { void run(Leaf leaf) { leaf.start(); leaf.stop(); } }\n"
|
||||
),
|
||||
})
|
||||
|
||||
run = _find(result, ".run()", "client_run")
|
||||
assert (run, _find(result, ".start()", "root_start")) in calls
|
||||
assert (run, _find(result, ".stop()", "middle_stop")) in calls
|
||||
assert (run, _find(result, ".stop()", "root_stop")) not in calls
|
||||
|
||||
|
||||
def test_super_call_resolves_to_the_superclass_declaration(tmp_path: Path):
|
||||
calls, result = _calls(tmp_path, {
|
||||
"Base.java": "class Base { void close() {} }\n",
|
||||
"Worker.java": (
|
||||
"class Worker extends Base {\n"
|
||||
" void close() { super.close(); }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
|
||||
worker_close = _find(result, ".close()", "worker_close")
|
||||
base_close = _find(result, ".close()", "base_close")
|
||||
assert (worker_close, base_close) in calls
|
||||
assert any(
|
||||
edge.get("relation") == "calls"
|
||||
and (edge["source"], edge["target"]) == (worker_close, base_close)
|
||||
and edge.get("confidence") == "EXTRACTED"
|
||||
for edge in result["edges"]
|
||||
)
|
||||
|
||||
|
||||
def test_ancestor_outside_the_corpus_leaves_inherited_call_unresolved(tmp_path: Path):
|
||||
calls, result = _calls(tmp_path, {
|
||||
"Named.java": "interface Named { String name(); }\n",
|
||||
"Entity.java": (
|
||||
"import org.example.Identified;\n"
|
||||
"interface Entity extends Identified, Named {}\n"
|
||||
),
|
||||
"Client.java": "class Client { void run(Entity e) { e.name(); } }\n",
|
||||
})
|
||||
|
||||
run = _find(result, ".run()", "client_run")
|
||||
assert not any(source == run for source, _ in calls)
|
||||
|
||||
|
||||
def test_super_call_is_not_bound_back_to_the_calling_class(tmp_path: Path):
|
||||
calls, result = _calls(tmp_path, {
|
||||
"lib/Handler.java": "package lib;\npublic class Handler { public void handle() {} }\n",
|
||||
"app/Handler.java": (
|
||||
"package app;\n"
|
||||
"public class Handler extends lib.Handler {\n"
|
||||
" public void handle() {}\n"
|
||||
" public void retry() { super.handle(); }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
|
||||
retry = _find(result, ".retry()", "handler_retry")
|
||||
assert not any(source == retry for source, _ in calls)
|
||||
|
||||
|
||||
def test_inherited_call_is_not_bound_to_another_language(tmp_path: Path):
|
||||
calls, result = _calls(tmp_path, {
|
||||
"Worker.java": (
|
||||
"import org.lib.*;\n"
|
||||
"class Worker extends Base { void run() { this.start(); super.start(); } }\n"
|
||||
),
|
||||
"tools/base.py": "class Base:\n def start(self):\n pass\n",
|
||||
})
|
||||
|
||||
run = _find(result, ".run()", "worker_run")
|
||||
assert not any(source == run for source, _ in calls)
|
||||
Reference in new issue
Block a user