Skip to content

Commit 74b0cd1

Browse files
fix(extract): preserve Java receiver scope boundaries
1 parent e7f6650 commit 74b0cd1

2 files changed

Lines changed: 61 additions & 5 deletions

File tree

graphify/extractors/engine.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -432,9 +432,9 @@ def _java_method_receiver_types(
432432
) -> dict[str, str]:
433433
"""Build the receiver type table visible to one Java method.
434434
435-
Current-class fields are the base scope. Parameters and explicit local
436-
declarations shadow them. Conflicting local declarations in disjoint nested
437-
scopes are omitted because raw call facts do not retain lexical scope.
435+
Current-class fields are the base scope, and parameters shadow them for the
436+
full method. Conflicting local declarations are omitted because raw call
437+
facts do not retain lexical scope.
438438
"""
439439
method_types: dict[str, str] = {}
440440
ambiguous: set[str] = set()
@@ -467,6 +467,7 @@ def bind(name: str, type_name: str | None) -> None:
467467
node = stack.pop()
468468
if node.type in (
469469
"class_declaration",
470+
"class_body",
470471
"interface_declaration",
471472
"record_declaration",
472473
"enum_declaration",
@@ -479,13 +480,18 @@ def bind(name: str, type_name: str | None) -> None:
479480
node.child_by_field_name("type"), source
480481
)
481482
for name in _java_declarator_names(node, source):
482-
bind(name, type_name)
483+
if field_types.get(name) not in (None, type_name):
484+
method_types.pop(name, None)
485+
ambiguous.add(name)
486+
else:
487+
bind(name, type_name)
483488
stack.extend(node.children)
484489

485490
table = dict(field_types)
486491
table.update(method_types)
487492
for name in ambiguous:
488493
table.pop(name, None)
494+
table.update({f"this.{name}": type_name for name, type_name in field_types.items()})
489495
return table
490496

491497

@@ -3854,7 +3860,8 @@ def walk_calls(
38543860
owner = receiver.child_by_field_name("object")
38553861
field = receiver.child_by_field_name("field")
38563862
if owner is not None and owner.type == "this" and field is not None:
3857-
member_receiver = _read_text(field, source)
3863+
member_receiver = f"this.{_read_text(field, source)}"
3864+
is_this_field_call = True
38583865
elif config.ts_module == "tree_sitter_ruby":
38593866
# Ruby's `call` node carries `receiver` and `method` as direct
38603867
# fields (no intermediate accessor node), so the generic accessor

tests/test_java_member_calls.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,27 @@ def test_this_field_receiver_resolves_to_declared_type(tmp_path: Path):
9393
assert (run, gateway_charge) in calls
9494

9595

96+
def test_this_field_uses_field_type_when_parameter_shadows_name(tmp_path: Path):
97+
calls, result = _calls(tmp_path, {
98+
**_AMBIGUOUS_METHODS,
99+
"Checkout.java": (
100+
"class Checkout {\n"
101+
" PaymentGateway service;\n"
102+
" void run(AuditLog service) {\n"
103+
" service.charge();\n"
104+
" this.service.charge();\n"
105+
" }\n"
106+
"}\n"
107+
),
108+
})
109+
110+
run = _find(result, ".run()", "checkout")
111+
gateway_charge = _find(result, ".charge()", "paymentgateway")
112+
audit_charge = _find(result, ".charge()", "auditlog")
113+
assert (run, gateway_charge) in calls
114+
assert (run, audit_charge) in calls
115+
116+
96117
def test_parameter_and_local_receivers_resolve_per_method(tmp_path: Path):
97118
calls, result = _calls(tmp_path, {
98119
**_AMBIGUOUS_METHODS,
@@ -114,6 +135,34 @@ def test_parameter_and_local_receivers_resolve_per_method(tmp_path: Path):
114135
assert (from_local, gateway_charge) not in calls
115136

116137

138+
def test_nested_receiver_bindings_do_not_escape_their_scope(tmp_path: Path):
139+
calls, result = _calls(tmp_path, {
140+
**_AMBIGUOUS_METHODS,
141+
"Checkout.java": (
142+
"class Checkout {\n"
143+
" PaymentGateway service;\n"
144+
" void blockLocal() {\n"
145+
" service.charge();\n"
146+
" { AuditLog service = null; service.charge(); }\n"
147+
" }\n"
148+
" void anonymousClass() {\n"
149+
" new Object() { void nested() { AuditLog service = null; } };\n"
150+
" service.charge();\n"
151+
" }\n"
152+
"}\n"
153+
),
154+
})
155+
156+
block_local = _find(result, ".blockLocal()", "checkout")
157+
anonymous_class = _find(result, ".anonymousClass()", "checkout")
158+
gateway_charge = _find(result, ".charge()", "paymentgateway")
159+
audit_charge = _find(result, ".charge()", "auditlog")
160+
assert not any(source == block_local and "charge" in target
161+
for source, target in calls)
162+
assert (anonymous_class, gateway_charge) in calls
163+
assert (anonymous_class, audit_charge) not in calls
164+
165+
117166
def test_overloaded_callers_keep_body_scoped_receiver_types(tmp_path: Path):
118167
calls, result = _calls(tmp_path, {
119168
**_AMBIGUOUS_METHODS,

0 commit comments

Comments
 (0)