feat(access-control-prover): 8.1 capture call receiver and arity evidence in the semantic IR

This commit is contained in:
Marius Ciepluch
2026-10-29 15:33:18 +01:00
parent 03807f4ddd
commit 9eee4d7f35
4 changed files with 153 additions and 2 deletions
+36 -1
View File
@@ -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.
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
callee = _callee_name(node, args_node)
return CallSite(
order=int(node.end_byte),
name=_callee_name(node, args_node),
name=callee,
line=_line(node),
arg_texts=tuple(_text(arg) 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),
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:
if node.type in _KIND["php"]["call"]:
callee = node.child_by_field_name("name") or node.child_by_field_name("function")
+10
View File
@@ -27,6 +27,8 @@ class CallSite:
arg_names: tuple[tuple[str, ...], ...]
extra_arg_is_sequence: bool
order: int = 0
receiver: str | None = None
arity: int | None = None
@dataclass(frozen=True)
@@ -202,9 +204,17 @@ def _call_site(node: ast.Call) -> CallSite:
arg_is_constant=tuple(arg_const),
arg_names=tuple(arg_names),
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:
return int(getattr(node, "end_lineno", 0) or 0) * 1_000_000 + int(getattr(node, "end_col_offset", 0) or 0)