feat(detection-core): unique-declaration member-call binding (8.2 final gated round step 1-2)

x.method() with a variable receiver binds complete iff exactly one same-name
method declaration exists repository-wide; collisions stay a finite candidate
set (receiver_type_unknown) and module-level functions never bind. Authorized
by maintainer strategy-review 2026-10-31; gated by dev-pool FA non-regression.
This commit is contained in:
Marius Ciepluch
2026-10-31 07:10:36 +01:00
parent 1481bb33d7
commit dc2832564a
3 changed files with 73 additions and 0 deletions
+61
View File
@@ -135,6 +135,47 @@ class TestResolveCall:
assert len(candidates.targets) == 2
assert candidates.completeness.reasons
def test_unique_declaration_member_call_binds_complete(self) -> None:
index = index_of(
(
"src/a/Store.java",
"""public class Store {
public void persist(String item) { }
}
""",
"java",
)
)
call = _call(name="persist", line=1, receiver="store", arity=1)
target_set = resolve_call(call, module="src/a/Use.java", index=index)
assert target_set.targets == (SymbolId("src/a/Store.java", "Store", "persist", ("item",)),)
assert target_set.completeness.reasons == ()
def test_member_call_collision_across_modules_stays_incomplete(self) -> None:
index = index_of(
("src/a/One.java", "public class Store { public void persist(String item) { } }", "java"),
("src/b/Two.java", "public class Vault { public void persist(String item) { } }", "java"),
)
call = _call(name="persist", line=1, receiver="obj", arity=1)
target_set = resolve_call(call, module="src/a/One.java", index=index)
assert len(target_set.targets) == 2
assert target_set.completeness.reasons == ("resolution:receiver_type_unknown",)
def test_member_call_never_binds_module_level_function(self) -> None:
index = index_of(
(
"src/svc.py",
"""def persist(item):
pass
""",
"python",
)
)
call = _call(name="persist", line=3, receiver="obj", arity=1)
target_set = resolve_call(call, module="src/svc.py", index=index)
assert target_set.targets == ()
assert target_set.completeness.reasons
def test_no_candidate_is_unresolved_with_reason(self) -> None:
index = index_of(JAVA_ONE_CLASS)
call = _call(name="missing", line=3, receiver="helper", arity=1)
@@ -233,6 +274,26 @@ class TestBindCalls:
assert bindings.exact_callers(target) == ()
assert bindings.counts["incomplete_calls"] == 1
def test_unique_declaration_member_call_counts_complete(self) -> None:
index = index_of(
(
"src/a/Store.java",
"""public class Store {
public void persist(String item) { }
}
""",
"java",
)
)
bindings = bind_calls(
functions_by_module={"src/a/Use.java": (_ir_stub(calls=(_call(name="persist", line=2, receiver="store", arity=1),)),)},
index=index,
)
target = SymbolId("src/a/Store.java", "Store", "persist", ("item",))
assert bindings.exact_callers(target) == (CallerBinding(module="src/a/Use.java", function="caller", line=2, exact=True),)
assert bindings.counts["complete_calls"] == 1
assert bindings.counts["incomplete_calls"] == 0
def _ir_stub(calls: tuple[CallSite, ...], name: str = "caller") -> FunctionIR:
return FunctionIR(name=name, params=(), start_line=1, end_line=9, binds=(), calls=calls)
+4
View File
@@ -540,6 +540,10 @@ JAVA_AUDITOR = """class Auditor {
public void check() {
}
}
class Gate {
public void check() {
}
}
"""