mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(access-control-prover): 8.1 capture call receiver and arity evidence in the semantic IR
This commit is contained in:
@@ -110,7 +110,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
|
|||||||
- _Depends: 7.1, 7.2, 7.3_
|
- _Depends: 7.1, 7.2, 7.3_
|
||||||
|
|
||||||
- [ ] 8. Stable symbols and conservative call resolution
|
- [ ] 8. Stable symbols and conservative call resolution
|
||||||
- [ ] 8.1 Capture call receiver and arity evidence in the semantic IR
|
- [x] 8.1 Capture call receiver and arity evidence in the semantic IR
|
||||||
- Preserve receiver expression and argument count for supported syntax while keeping existing constructors valid.
|
- Preserve receiver expression and argument count for supported syntax while keeping existing constructors valid.
|
||||||
- Done when language fixtures retain receiver and arity evidence without regressing existing IR consumers.
|
- Done when language fixtures retain receiver and arity evidence without regressing existing IR consumers.
|
||||||
- _Requirements: 3.2, 3.3_
|
- _Requirements: 3.2, 3.3_
|
||||||
|
|||||||
@@ -206,17 +206,52 @@ def _call(node: Any, kinds: dict[str, frozenset[str]]) -> CallSite:
|
|||||||
# PHP wraps positional and named argument values in an `argument` node.
|
# PHP wraps positional and named argument values in an `argument` node.
|
||||||
arg_nodes = [arg.named_children[-1] if arg.type == "argument" and arg.named_children else arg for arg in arg_nodes]
|
arg_nodes = [arg.named_children[-1] if arg.type == "argument" and arg.named_children else arg for arg in arg_nodes]
|
||||||
extra = len(arg_nodes) >= 2 and arg_nodes[1].type in _SEQUENCE
|
extra = len(arg_nodes) >= 2 and arg_nodes[1].type in _SEQUENCE
|
||||||
|
callee = _callee_name(node, args_node)
|
||||||
return CallSite(
|
return CallSite(
|
||||||
order=int(node.end_byte),
|
order=int(node.end_byte),
|
||||||
name=_callee_name(node, args_node),
|
name=callee,
|
||||||
line=_line(node),
|
line=_line(node),
|
||||||
arg_texts=tuple(_text(arg) for arg in arg_nodes),
|
arg_texts=tuple(_text(arg) for arg in arg_nodes),
|
||||||
arg_is_constant=tuple(_is_constant(arg, kinds) for arg in arg_nodes),
|
arg_is_constant=tuple(_is_constant(arg, kinds) for arg in arg_nodes),
|
||||||
arg_names=tuple(_identifiers(arg) for arg in arg_nodes),
|
arg_names=tuple(_identifiers(arg) for arg in arg_nodes),
|
||||||
extra_arg_is_sequence=extra,
|
extra_arg_is_sequence=extra,
|
||||||
|
receiver=_receiver_text(node, args_node, callee),
|
||||||
|
arity=len(arg_nodes),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_RECEIVER_SEPARATORS = ("->", "::", "?.", ".")
|
||||||
|
|
||||||
|
|
||||||
|
def _receiver_text(node: Any, args_node: Any, callee: str) -> str | None:
|
||||||
|
parts = [child for child in getattr(node, "named_children", ()) or () if child is not args_node]
|
||||||
|
final_identifier = callee
|
||||||
|
for separator in _RECEIVER_SEPARATORS:
|
||||||
|
final_identifier = final_identifier.rsplit(separator, 1)[-1]
|
||||||
|
final_identifier = final_identifier.strip()
|
||||||
|
for child in parts:
|
||||||
|
receiver = _receiver_before_separator(_text(child), final_identifier)
|
||||||
|
if receiver is not None:
|
||||||
|
return receiver
|
||||||
|
for index, child in enumerate(parts):
|
||||||
|
if _text(child).strip() == final_identifier:
|
||||||
|
before = [text for text in (_text(part).strip() for part in parts[:index]) if text]
|
||||||
|
if before:
|
||||||
|
return ".".join(before)
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _receiver_before_separator(text: str, final_identifier: str) -> str | None:
|
||||||
|
for separator in _RECEIVER_SEPARATORS:
|
||||||
|
position = text.rfind(separator)
|
||||||
|
if position <= 0:
|
||||||
|
continue
|
||||||
|
if text[position + len(separator) :].strip() == final_identifier:
|
||||||
|
return text[:position].strip() or None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _callee_name(node: Any, args_node: Any) -> str:
|
def _callee_name(node: Any, args_node: Any) -> str:
|
||||||
if node.type in _KIND["php"]["call"]:
|
if node.type in _KIND["php"]["call"]:
|
||||||
callee = node.child_by_field_name("name") or node.child_by_field_name("function")
|
callee = node.child_by_field_name("name") or node.child_by_field_name("function")
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class CallSite:
|
|||||||
arg_names: tuple[tuple[str, ...], ...]
|
arg_names: tuple[tuple[str, ...], ...]
|
||||||
extra_arg_is_sequence: bool
|
extra_arg_is_sequence: bool
|
||||||
order: int = 0
|
order: int = 0
|
||||||
|
receiver: str | None = None
|
||||||
|
arity: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -202,9 +204,17 @@ def _call_site(node: ast.Call) -> CallSite:
|
|||||||
arg_is_constant=tuple(arg_const),
|
arg_is_constant=tuple(arg_const),
|
||||||
arg_names=tuple(arg_names),
|
arg_names=tuple(arg_names),
|
||||||
extra_arg_is_sequence=extra_sequence,
|
extra_arg_is_sequence=extra_sequence,
|
||||||
|
receiver=_receiver(node.func),
|
||||||
|
arity=len(node.args) + len(node.keywords),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _receiver(func: ast.expr) -> str | None:
|
||||||
|
if isinstance(func, ast.Attribute):
|
||||||
|
return _unparse(func.value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _end_position(node: ast.AST) -> int:
|
def _end_position(node: ast.AST) -> int:
|
||||||
return int(getattr(node, "end_lineno", 0) or 0) * 1_000_000 + int(getattr(node, "end_col_offset", 0) or 0)
|
return int(getattr(node, "end_lineno", 0) or 0) * 1_000_000 + int(getattr(node, "end_col_offset", 0) or 0)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""Task 8.1 -- call receiver and arity evidence in the semantic IR (Reqs 3.2, 3.3)."""
|
||||||
|
|
||||||
|
from openultrasast.semantic.ir import CallSite, parse_file
|
||||||
|
|
||||||
|
|
||||||
|
def _calls(path: str, text: str, language: str) -> list[CallSite]:
|
||||||
|
parsed = parse_file(path, text, language)
|
||||||
|
assert parsed.parse_ok, parsed.reason
|
||||||
|
return [call for function in parsed.functions for call in function.calls]
|
||||||
|
|
||||||
|
|
||||||
|
PHP_SOURCE = """<?php
|
||||||
|
final class Controller {
|
||||||
|
public function run(): void {
|
||||||
|
$this->helper('a', 'b');
|
||||||
|
self::staticHelper('c');
|
||||||
|
plain('d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
JAVA_SOURCE = """public final class Job {
|
||||||
|
void execute() {
|
||||||
|
getListener().onEvent(item, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
TYPESCRIPT_SOURCE = """export function handle(client: Client) {
|
||||||
|
client.send(payload);
|
||||||
|
log("done");
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
PYTHON_SOURCE = """def handle(request):
|
||||||
|
store.save(request.body)
|
||||||
|
audit(request, flag=True)
|
||||||
|
log("done")
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_php_member_call_retains_receiver_and_arity() -> None:
|
||||||
|
calls = {call.name: call for call in _calls("controller.php", PHP_SOURCE, "php")}
|
||||||
|
helper = calls["helper"]
|
||||||
|
assert helper.receiver == "$this"
|
||||||
|
assert helper.arity == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_php_scoped_call_names_the_class_scope_as_receiver() -> None:
|
||||||
|
calls = {call.name: call for call in _calls("controller.php", PHP_SOURCE, "php")}
|
||||||
|
assert calls["staticHelper"].receiver == "self"
|
||||||
|
assert calls["staticHelper"].arity == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_php_bare_function_call_has_no_receiver() -> None:
|
||||||
|
calls = {call.name: call for call in _calls("controller.php", PHP_SOURCE, "php")}
|
||||||
|
assert calls["plain"].receiver is None
|
||||||
|
assert calls["plain"].arity == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_java_method_invocation_retains_receiver_expression_and_arity() -> None:
|
||||||
|
calls = {call.name: call for call in _calls("Job.java", JAVA_SOURCE, "java")}
|
||||||
|
event = calls["onEvent"]
|
||||||
|
assert event.receiver == "getListener()"
|
||||||
|
assert event.arity == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_typescript_member_call_retains_receiver_and_arity() -> None:
|
||||||
|
calls = {call.name: call for call in _calls("handle.ts", TYPESCRIPT_SOURCE, "typescript")}
|
||||||
|
send = calls["client.send"]
|
||||||
|
assert send.receiver == "client"
|
||||||
|
assert send.arity == 1
|
||||||
|
assert calls["log"].receiver is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_attribute_call_retains_receiver_and_arity() -> None:
|
||||||
|
calls = {call.name: call for call in _calls("handle.py", PYTHON_SOURCE, "python")}
|
||||||
|
save = calls["store.save"]
|
||||||
|
assert save.receiver == "store"
|
||||||
|
assert save.arity == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_keyword_argument_counts_toward_arity() -> None:
|
||||||
|
calls = {call.name: call for call in _calls("handle.py", PYTHON_SOURCE, "python")}
|
||||||
|
assert calls["audit"].receiver is None
|
||||||
|
assert calls["audit"].arity == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_bare_call_has_no_receiver() -> None:
|
||||||
|
calls = {call.name: call for call in _calls("handle.py", PYTHON_SOURCE, "python")}
|
||||||
|
assert calls["log"].receiver is None
|
||||||
|
assert calls["log"].arity == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_positional_construction_stays_valid_with_default_evidence() -> None:
|
||||||
|
call = CallSite("perform", 7, ("x",), (False,), (("x",),), False, 3)
|
||||||
|
assert call.receiver is None
|
||||||
|
assert call.arity is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_receiver_capture_survives_whitespaced_member_syntax() -> None:
|
||||||
|
source = "export function go(c: Client) {\n c . flush ( );\n}\n"
|
||||||
|
calls = {call.name: call for call in _calls("go.ts", source, "typescript")}
|
||||||
|
flush = calls["c . flush"]
|
||||||
|
assert flush.receiver == "c"
|
||||||
|
assert flush.arity == 0
|
||||||
Reference in New Issue
Block a user