mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 09:48:18 +00:00
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.
309 lines
11 KiB
Python
309 lines
11 KiB
Python
"""Call resolution tests: exact, finite-candidate and unresolved targets (task 8.3).
|
|
|
|
Req 3.3: multiple feasible targets stay a finite candidate set marked incomplete,
|
|
never merged under a bare name.
|
|
Req 3.4: no bounded candidate set -> unresolved, unusable for complete paths.
|
|
Req 11.1/11.6: grades distinguish definite/possible/unknown; precision loss never
|
|
flips a complete result to a different complete result.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from openultrasast.semantic.ir import CallSite, FunctionIR
|
|
from openultrasast.semantic.obligations.abstract_domain import Completeness
|
|
from openultrasast.semantic.obligations.resolution import (
|
|
CallBindings,
|
|
CallerBinding,
|
|
CallTargetSet,
|
|
SymbolId,
|
|
SymbolIndex,
|
|
bind_calls,
|
|
resolve_call,
|
|
)
|
|
|
|
|
|
def _complete() -> Completeness:
|
|
return Completeness(is_complete=True)
|
|
|
|
|
|
def _call(name: str, line: int, receiver: str | None = None, arity: int | None = None) -> CallSite:
|
|
return CallSite(
|
|
name=name,
|
|
line=line,
|
|
arg_texts=(),
|
|
arg_is_constant=(),
|
|
arg_names=(),
|
|
extra_arg_is_sequence=False,
|
|
receiver=receiver,
|
|
arity=arity,
|
|
)
|
|
|
|
|
|
JAVA_ONE_CLASS = (
|
|
"src/a/Handler.java",
|
|
"""package pkg;
|
|
public class Handler {
|
|
public void run(String command) { }
|
|
public void handle() { run("x"); this.run("y"); }
|
|
}
|
|
""",
|
|
"java",
|
|
)
|
|
|
|
JAVA_TWO_CLASSES_SAME_METHOD = (
|
|
"src/a/Two.java",
|
|
"""package pkg;
|
|
public class Auditor { public void check(String item) { } }
|
|
public class Gate { public void check(String item) { } }
|
|
""",
|
|
"java",
|
|
)
|
|
|
|
PYTHON_MODULE_SCOPE = (
|
|
"src/svc.py",
|
|
"""def audit(request):
|
|
pass
|
|
|
|
def other(request):
|
|
audit(request)
|
|
""",
|
|
"python",
|
|
)
|
|
|
|
|
|
def index_of(*files: tuple[str, str, str]) -> SymbolIndex:
|
|
return SymbolIndex.build(files)
|
|
|
|
|
|
class TestResolveCall:
|
|
def test_exact_self_receiver_in_one_class_is_complete(self) -> None:
|
|
index = index_of(JAVA_ONE_CLASS)
|
|
call = _call(name="run", line=5, receiver="this", arity=1)
|
|
target_set = resolve_call(call, module="src/a/Handler.java", index=index)
|
|
assert target_set.targets == (SymbolId("src/a/Handler.java", "Handler", "run", ("command",)),)
|
|
assert target_set.completeness.reasons == ()
|
|
|
|
def test_two_classes_same_method_stay_candidates_never_bare(self) -> None:
|
|
index = index_of(JAVA_TWO_CLASSES_SAME_METHOD)
|
|
call = _call(name="check", line=2, receiver="auditor", arity=1)
|
|
target_set = resolve_call(call, module="src/a/Two.java", index=index)
|
|
assert set(target_set.targets) == {
|
|
SymbolId("src/a/Two.java", "Auditor", "check", ("item",)),
|
|
SymbolId("src/a/Two.java", "Gate", "check", ("item",)),
|
|
}
|
|
assert target_set.completeness.reasons # incomplete, not merged
|
|
|
|
def test_overload_arity_narrows_but_variable_receiver_stays_incomplete(self) -> None:
|
|
index = index_of(
|
|
(
|
|
"src/a/Ops.java",
|
|
"""public class Ops {
|
|
public void run() { }
|
|
public void run(String command) { }
|
|
}
|
|
""",
|
|
"java",
|
|
)
|
|
)
|
|
narrowed = resolve_call(
|
|
_call(name="run", line=1, receiver="ops", arity=1),
|
|
module="src/a/Ops.java",
|
|
index=index,
|
|
)
|
|
assert narrowed.targets == (SymbolId("src/a/Ops.java", "Ops", "run", ("command",)),)
|
|
assert narrowed.completeness.reasons # partial type information stays incomplete
|
|
|
|
def test_unknown_arity_keeps_overloads_as_candidates(self) -> None:
|
|
index = index_of(
|
|
(
|
|
"src/a/Ops.java",
|
|
"""public class Ops {
|
|
public void run() { }
|
|
public void run(String command) { }
|
|
}
|
|
""",
|
|
"java",
|
|
)
|
|
)
|
|
candidates = resolve_call(
|
|
_call(name="run", line=1, receiver="ops", arity=None),
|
|
module="src/a/Ops.java",
|
|
index=index,
|
|
)
|
|
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)
|
|
target_set = resolve_call(call, module="src/a/Handler.java", index=index)
|
|
assert target_set.targets == ()
|
|
assert target_set.completeness.reasons
|
|
|
|
def test_module_scope_unique_bare_call_is_complete(self) -> None:
|
|
index = index_of(PYTHON_MODULE_SCOPE)
|
|
call = _call(name="audit", line=5, receiver=None, arity=1)
|
|
target_set = resolve_call(call, module="src/svc.py", index=index)
|
|
assert target_set.targets == (SymbolId("src/svc.py", None, "audit", ("request",)),)
|
|
assert target_set.completeness.reasons == ()
|
|
|
|
def test_module_scope_shared_name_is_incomplete(self) -> None:
|
|
index = index_of(
|
|
(
|
|
"src/twin.py",
|
|
"""def audit(request):
|
|
pass
|
|
|
|
def audit(request, extra):
|
|
pass
|
|
""",
|
|
"python",
|
|
)
|
|
)
|
|
call = _call(name="audit", line=4, receiver=None, arity=None)
|
|
target_set = resolve_call(call, module="src/twin.py", index=index)
|
|
assert len(target_set.targets) == 2
|
|
assert target_set.completeness.reasons
|
|
|
|
def test_precision_loss_never_flips_complete_to_complete(self) -> None:
|
|
single = index_of(
|
|
(
|
|
"src/a/One.java",
|
|
"""public class Gate {
|
|
public void check(String item) { }
|
|
public void audit() { check("x"); }
|
|
}
|
|
""",
|
|
"java",
|
|
)
|
|
)
|
|
doubled = index_of(
|
|
(
|
|
"src/a/One.java",
|
|
"""public class Gate {
|
|
public void check(String item) { }
|
|
public void audit() { check("x"); }
|
|
}
|
|
public class Other {
|
|
public void check(String item) { }
|
|
}
|
|
""",
|
|
"java",
|
|
)
|
|
)
|
|
call = _call(name="check", line=3, receiver="this", arity=1)
|
|
before = resolve_call(call, module="src/a/One.java", index=single)
|
|
after = resolve_call(call, module="src/a/One.java", index=doubled)
|
|
assert before.completeness.reasons == ()
|
|
assert after.completeness.reasons # grew ambiguous, never silently complete
|
|
|
|
|
|
class TestBindCalls:
|
|
def test_exact_binding_feeds_reverse_caller_index_and_counts(self) -> None:
|
|
index = index_of(JAVA_ONE_CLASS)
|
|
bindings = bind_calls(
|
|
functions_by_module={
|
|
"src/a/Handler.java": (
|
|
_ir_stub(
|
|
calls=(
|
|
_call(name="run", line=5, receiver="this", arity=1),
|
|
_call(name="run", line=5, receiver="this", arity=1),
|
|
),
|
|
name="handle",
|
|
),
|
|
)
|
|
},
|
|
index=index,
|
|
)
|
|
assert isinstance(bindings, CallBindings)
|
|
target = SymbolId("src/a/Handler.java", "Handler", "run", ("command",))
|
|
assert bindings.exact_callers(target) == (CallerBinding(module="src/a/Handler.java", function="handle", line=5, exact=True),)
|
|
assert bindings.counts["complete_calls"] == 2
|
|
assert bindings.counts["unresolved_calls"] == 0
|
|
|
|
def test_candidate_only_binding_is_not_an_exact_caller(self) -> None:
|
|
index = index_of(JAVA_TWO_CLASSES_SAME_METHOD)
|
|
bindings = bind_calls(
|
|
functions_by_module={"src/a/Two.java": (_ir_stub(calls=(_call(name="check", line=3, receiver="gate", arity=1),)),)},
|
|
index=index,
|
|
)
|
|
target = SymbolId("src/a/Two.java", "Gate", "check", ("item",))
|
|
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)
|
|
|
|
|
|
@pytest.mark.parametrize("fixture", [JAVA_ONE_CLASS, PYTHON_MODULE_SCOPE])
|
|
def test_call_target_set_is_canonical_and_frozen(fixture: tuple[str, str, str]) -> None:
|
|
index = index_of(fixture)
|
|
assert index.declarations == tuple(sorted(index.declarations))
|
|
target_set = CallTargetSet(targets=index.declarations, completeness=_complete())
|
|
with pytest.raises(Exception): # noqa: B017 - frozen contract
|
|
target_set.targets = () # type: ignore[misc]
|