mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
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:
@@ -239,7 +239,15 @@ def _type_receiver_targets(same_name: tuple[SymbolId, ...], type_name: str, arit
|
|||||||
|
|
||||||
|
|
||||||
def _variable_receiver_targets(same_name: tuple[SymbolId, ...], arity: int | None) -> CallTargetSet:
|
def _variable_receiver_targets(same_name: tuple[SymbolId, ...], arity: int | None) -> CallTargetSet:
|
||||||
|
# A variable receiver's type is unknown, but when exactly ONE method with the
|
||||||
|
# called name exists repository-wide, no second repository-defined target can
|
||||||
|
# exist, so the call binds complete (unique-declaration member-call binding).
|
||||||
|
# Two or more same-name declarations are a collision: they stay a finite
|
||||||
|
# candidate set, never a bare-name merge. A same-name module-level function is
|
||||||
|
# not a member-call target, so it neither binds nor collides here.
|
||||||
methods = tuple(symbol for symbol in same_name if symbol.enclosing_type is not None)
|
methods = tuple(symbol for symbol in same_name if symbol.enclosing_type is not None)
|
||||||
|
if len(methods) == 1:
|
||||||
|
return CallTargetSet(targets=methods, completeness=Completeness(is_complete=True))
|
||||||
narrowed = _arity_narrow(methods, arity)
|
narrowed = _arity_narrow(methods, arity)
|
||||||
if not narrowed:
|
if not narrowed:
|
||||||
return CallTargetSet(targets=(), completeness=Completeness(is_complete=False, reasons=("resolution:no_candidate",)))
|
return CallTargetSet(targets=(), completeness=Completeness(is_complete=False, reasons=("resolution:no_candidate",)))
|
||||||
|
|||||||
@@ -135,6 +135,47 @@ class TestResolveCall:
|
|||||||
assert len(candidates.targets) == 2
|
assert len(candidates.targets) == 2
|
||||||
assert candidates.completeness.reasons
|
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:
|
def test_no_candidate_is_unresolved_with_reason(self) -> None:
|
||||||
index = index_of(JAVA_ONE_CLASS)
|
index = index_of(JAVA_ONE_CLASS)
|
||||||
call = _call(name="missing", line=3, receiver="helper", arity=1)
|
call = _call(name="missing", line=3, receiver="helper", arity=1)
|
||||||
@@ -233,6 +274,26 @@ class TestBindCalls:
|
|||||||
assert bindings.exact_callers(target) == ()
|
assert bindings.exact_callers(target) == ()
|
||||||
assert bindings.counts["incomplete_calls"] == 1
|
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:
|
def _ir_stub(calls: tuple[CallSite, ...], name: str = "caller") -> FunctionIR:
|
||||||
return FunctionIR(name=name, params=(), start_line=1, end_line=9, binds=(), calls=calls)
|
return FunctionIR(name=name, params=(), start_line=1, end_line=9, binds=(), calls=calls)
|
||||||
|
|||||||
@@ -540,6 +540,10 @@ JAVA_AUDITOR = """class Auditor {
|
|||||||
public void check() {
|
public void check() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class Gate {
|
||||||
|
public void check() {
|
||||||
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user