mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
dae602ccd1
Java member calls (`gw.charge()`) resolved by bare method name, so a call bound to any same-named method in the corpus — e.g. `PaymentGateway.charge` and `AuditLog.charge` were indistinguishable, producing phantom cross-class edges and a false god node. The extractor now preserves the receiver and its static type, and _resolve_java_member_calls binds the call against the receiver's declared type: explicit-type receivers and `this` are exact; current-class fields, method parameters, and explicitly-typed locals resolve via a method-scoped type table; a missing/ambiguous/inherited/chained receiver is skipped rather than falling back to a bare name match (same single-owner god-node guard as the C#/Swift/Ruby resolvers). Fully-qualified and nested-type receivers are deferred since they need package- and nesting-aware type identity. Verified: `gw.ping()/gw.charge()` (gw: PaymentGateway) bind to PaymentGateway, the three charge() calls dedup to one edge, and no edge targets the same-named AuditLog methods. Applies cleanly to the post-#1737 layout (extract.py + extractors/engine.py). 13 new tests; full suite 3135 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
279 lines
9.8 KiB
Python
279 lines
9.8 KiB
Python
"""Java receiver-typed member-call resolution.
|
|
|
|
Java ``method_invocation`` nodes carry both the method name and its receiver,
|
|
but the generic extractor currently resolves only by the bare method name. A
|
|
typed receiver must select the method owned by its declared type; unresolved or
|
|
ambiguous receivers must stay unlinked rather than creating a false call edge.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from graphify.extract import extract
|
|
|
|
|
|
def _calls(tmp_path: Path, files: dict[str, str]):
|
|
paths = []
|
|
for name, body in files.items():
|
|
path = tmp_path / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(body, encoding="utf-8")
|
|
paths.append(path)
|
|
result = extract(paths, cache_root=tmp_path / "graphify-out")
|
|
calls = {
|
|
(edge["source"], edge["target"])
|
|
for edge in result["edges"]
|
|
if edge.get("relation") == "calls"
|
|
}
|
|
return calls, result
|
|
|
|
|
|
def _find(result: dict, label: str, id_contains: str) -> str:
|
|
return next(
|
|
node["id"]
|
|
for node in result["nodes"]
|
|
if node.get("label") == label and id_contains in node["id"]
|
|
)
|
|
|
|
|
|
_AMBIGUOUS_METHODS = {
|
|
"Services.java": (
|
|
"class PaymentGateway { static void ping() {} void charge() {} }\n"
|
|
"class AuditLog { static void ping() {} void charge() {} }\n"
|
|
),
|
|
}
|
|
|
|
|
|
def test_explicit_type_receiver_resolves_to_owned_method(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
**_AMBIGUOUS_METHODS,
|
|
"Checkout.java": (
|
|
"class Checkout { void run() { PaymentGateway.ping(); } }\n"
|
|
),
|
|
})
|
|
|
|
run = _find(result, ".run()", "checkout")
|
|
gateway_ping = _find(result, ".ping()", "paymentgateway")
|
|
audit_ping = _find(result, ".ping()", "auditlog")
|
|
assert (run, gateway_ping) in calls
|
|
assert (run, audit_ping) not in calls
|
|
|
|
|
|
def test_field_receiver_resolves_to_declared_type(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
**_AMBIGUOUS_METHODS,
|
|
"Checkout.java": (
|
|
"class Checkout {\n"
|
|
" void run() { gateway.charge(); }\n"
|
|
" PaymentGateway gateway;\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
run = _find(result, ".run()", "checkout")
|
|
gateway_charge = _find(result, ".charge()", "paymentgateway")
|
|
audit_charge = _find(result, ".charge()", "auditlog")
|
|
assert (run, gateway_charge) in calls
|
|
assert (run, audit_charge) not in calls
|
|
|
|
|
|
def test_this_field_receiver_resolves_to_declared_type(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
**_AMBIGUOUS_METHODS,
|
|
"Checkout.java": (
|
|
"class Checkout {\n"
|
|
" PaymentGateway gateway;\n"
|
|
" void run() { this.gateway.charge(); }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
run = _find(result, ".run()", "checkout")
|
|
gateway_charge = _find(result, ".charge()", "paymentgateway")
|
|
assert (run, gateway_charge) in calls
|
|
|
|
|
|
def test_this_field_uses_field_type_when_parameter_shadows_name(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
**_AMBIGUOUS_METHODS,
|
|
"Checkout.java": (
|
|
"class Checkout {\n"
|
|
" PaymentGateway service;\n"
|
|
" void run(AuditLog service) {\n"
|
|
" service.charge();\n"
|
|
" this.service.charge();\n"
|
|
" }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
run = _find(result, ".run()", "checkout")
|
|
gateway_charge = _find(result, ".charge()", "paymentgateway")
|
|
audit_charge = _find(result, ".charge()", "auditlog")
|
|
assert (run, gateway_charge) in calls
|
|
assert (run, audit_charge) in calls
|
|
|
|
|
|
def test_parameter_and_local_receivers_resolve_per_method(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
**_AMBIGUOUS_METHODS,
|
|
"Checkout.java": (
|
|
"class Checkout {\n"
|
|
" void fromParameter(PaymentGateway service) { service.charge(); }\n"
|
|
" void fromLocal() { AuditLog service = new AuditLog(); service.charge(); }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
from_parameter = _find(result, ".fromParameter()", "checkout")
|
|
from_local = _find(result, ".fromLocal()", "checkout")
|
|
gateway_charge = _find(result, ".charge()", "paymentgateway")
|
|
audit_charge = _find(result, ".charge()", "auditlog")
|
|
assert (from_parameter, gateway_charge) in calls
|
|
assert (from_parameter, audit_charge) not in calls
|
|
assert (from_local, audit_charge) in calls
|
|
assert (from_local, gateway_charge) not in calls
|
|
|
|
|
|
def test_nested_receiver_bindings_do_not_escape_their_scope(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
**_AMBIGUOUS_METHODS,
|
|
"Checkout.java": (
|
|
"class Checkout {\n"
|
|
" PaymentGateway service;\n"
|
|
" void blockLocal() {\n"
|
|
" service.charge();\n"
|
|
" { AuditLog service = null; service.charge(); }\n"
|
|
" }\n"
|
|
" void anonymousClass() {\n"
|
|
" new Object() { void nested() { AuditLog service = null; } };\n"
|
|
" service.charge();\n"
|
|
" }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
block_local = _find(result, ".blockLocal()", "checkout")
|
|
anonymous_class = _find(result, ".anonymousClass()", "checkout")
|
|
gateway_charge = _find(result, ".charge()", "paymentgateway")
|
|
audit_charge = _find(result, ".charge()", "auditlog")
|
|
assert not any(source == block_local and "charge" in target
|
|
for source, target in calls)
|
|
assert (anonymous_class, gateway_charge) in calls
|
|
assert (anonymous_class, audit_charge) not in calls
|
|
|
|
|
|
def test_lambda_shadowing_does_not_reuse_enclosing_receiver_type(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
**_AMBIGUOUS_METHODS,
|
|
"Checkout.java": (
|
|
"class Checkout {\n"
|
|
" PaymentGateway service;\n"
|
|
" void captured() {\n"
|
|
" Runnable task = () -> service.charge();\n"
|
|
" }\n"
|
|
" void shadowed() {\n"
|
|
" java.util.function.Consumer<AuditLog> task =\n"
|
|
" service -> service.charge();\n"
|
|
" }\n"
|
|
" void parenthesized() {\n"
|
|
" java.util.function.Consumer<AuditLog> task =\n"
|
|
" (service) -> service.charge();\n"
|
|
" }\n"
|
|
" void typed() {\n"
|
|
" java.util.function.Consumer<AuditLog> task =\n"
|
|
" (AuditLog service) -> service.charge();\n"
|
|
" }\n"
|
|
" void sameType() {\n"
|
|
" java.util.function.Consumer<PaymentGateway> task =\n"
|
|
" (PaymentGateway service) -> service.charge();\n"
|
|
" }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
captured = _find(result, ".captured()", "checkout")
|
|
same_type = _find(result, ".sameType()", "checkout")
|
|
shadowed_callers = {
|
|
_find(result, f".{name}()", "checkout")
|
|
for name in ("shadowed", "parenthesized", "typed")
|
|
}
|
|
gateway_charge = _find(result, ".charge()", "paymentgateway")
|
|
assert (captured, gateway_charge) in calls
|
|
assert (same_type, gateway_charge) in calls
|
|
assert not any(source in shadowed_callers and "charge" in target
|
|
for source, target in calls)
|
|
|
|
|
|
def test_overloaded_callers_keep_body_scoped_receiver_types(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
**_AMBIGUOUS_METHODS,
|
|
"Checkout.java": (
|
|
"class Checkout {\n"
|
|
" void run(int value) { PaymentGateway service = null; service.charge(); }\n"
|
|
" void run(String value) { AuditLog service = null; service.charge(); }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
run = _find(result, ".run()", "checkout")
|
|
gateway_charge = _find(result, ".charge()", "paymentgateway")
|
|
audit_charge = _find(result, ".charge()", "auditlog")
|
|
assert (run, gateway_charge) in calls
|
|
assert (run, audit_charge) in calls
|
|
|
|
|
|
def test_ambiguous_receiver_type_emits_no_edge(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
"a/Gateway.java": "package a; public class Gateway { public void send() {} }\n",
|
|
"b/Gateway.java": "package b; public class Gateway { public void send() {} }\n",
|
|
"Caller.java": (
|
|
"class Caller { void run(Gateway gateway) { gateway.send(); } }\n"
|
|
),
|
|
})
|
|
|
|
run = _find(result, ".run()", "caller")
|
|
send_targets = {
|
|
target
|
|
for source, target in calls
|
|
if source == run and "send" in target
|
|
}
|
|
assert send_targets == set()
|
|
|
|
|
|
def test_inherited_field_and_chained_receiver_are_deferred(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
"Services.java": (
|
|
"class Gateway { void charge() {} Gateway create() { return this; } }\n"
|
|
"class Base { Gateway gateway; }\n"
|
|
"class Checkout extends Base {\n"
|
|
" Gateway factory;\n"
|
|
" void inherited() { this.gateway.charge(); }\n"
|
|
" void chained() { factory.create().charge(); }\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
inherited = _find(result, ".inherited()", "checkout")
|
|
chained = _find(result, ".chained()", "checkout")
|
|
assert not any(source in {inherited, chained} and "charge" in target
|
|
for source, target in calls)
|
|
|
|
|
|
def test_unqualified_call_still_resolves(tmp_path: Path):
|
|
calls, result = _calls(tmp_path, {
|
|
"Checkout.java": (
|
|
"class Checkout {\n"
|
|
" void run() { helper(); this.other(); }\n"
|
|
" void helper() {}\n"
|
|
" void other() {}\n"
|
|
"}\n"
|
|
),
|
|
})
|
|
|
|
run = _find(result, ".run()", "checkout")
|
|
helper = _find(result, ".helper()", "checkout")
|
|
other = _find(result, ".other()", "checkout")
|
|
assert (run, helper) in calls
|
|
assert (run, other) in calls
|