mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(access-control-prover): 8.4 inheritance and base-declaration attachment through repo-defined descendants
This commit is contained in:
@@ -129,7 +129,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
|
|||||||
- _Requirements: 3.3, 3.4, 11.1, 11.6_
|
- _Requirements: 3.3, 3.4, 11.1, 11.6_
|
||||||
- _Boundary: Call Resolution_
|
- _Boundary: Call Resolution_
|
||||||
- _Depends: 8.2_
|
- _Depends: 8.2_
|
||||||
- [ ] 8.4 Resolve inheritance and base-declaration attachment
|
- [x] 8.4 Resolve inheritance and base-declaration attachment
|
||||||
- Attach base operations and entries only through repository-defined descendant relations.
|
- Attach base operations and entries only through repository-defined descendant relations.
|
||||||
- Preserve ambiguous inheritance as incomplete rather than projecting to unrelated routes.
|
- Preserve ambiguous inheritance as incomplete rather than projecting to unrelated routes.
|
||||||
- Done when subclass and unrelated-class fixtures produce distinct attachments and judgments.
|
- Done when subclass and unrelated-class fixtures produce distinct attachments and judgments.
|
||||||
|
|||||||
@@ -154,6 +154,36 @@ def declaration_scopes(path: str, text: str, language: str) -> tuple[Declaration
|
|||||||
return tuple(scopes)
|
return tuple(scopes)
|
||||||
|
|
||||||
|
|
||||||
|
def class_bases(path: str, text: str, language: str) -> tuple[tuple[str, tuple[str, ...]], ...]:
|
||||||
|
"""Each class in the file with its declared base names; method-less classes included (task 8.4)."""
|
||||||
|
from .extra import grammar_for
|
||||||
|
|
||||||
|
kinds = _KIND.get(language)
|
||||||
|
grammar = grammar_for(_grammar_language(language, path))
|
||||||
|
if grammar is None or kinds is None:
|
||||||
|
return ()
|
||||||
|
try:
|
||||||
|
from tree_sitter import Parser
|
||||||
|
|
||||||
|
tree = Parser(grammar).parse(text.encode())
|
||||||
|
except Exception:
|
||||||
|
return ()
|
||||||
|
if bool(getattr(tree.root_node, "has_error", False)):
|
||||||
|
return ()
|
||||||
|
declarations: list[tuple[str, tuple[str, ...]]] = []
|
||||||
|
_collect_class_bases(tree.root_node, declarations)
|
||||||
|
return tuple(declarations)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_class_bases(node: object, declarations: list[tuple[str, tuple[str, ...]]]) -> None:
|
||||||
|
if node.type in _CLASS_KINDS: # type: ignore[attr-defined]
|
||||||
|
name_node = node.child_by_field_name("name") # type: ignore[attr-defined]
|
||||||
|
if name_node is not None:
|
||||||
|
declarations.append((_text(name_node), _class_bases(node))) # type: ignore[arg-type]
|
||||||
|
for child in getattr(node, "named_children", ()): # type: ignore[attr-defined]
|
||||||
|
_collect_class_bases(child, declarations)
|
||||||
|
|
||||||
|
|
||||||
def _collect_scopes(
|
def _collect_scopes(
|
||||||
node: Any,
|
node: Any,
|
||||||
kinds: dict[str, frozenset[str]],
|
kinds: dict[str, frozenset[str]],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from ..cst import DeclarationScope, declaration_scopes
|
from ..cst import DeclarationScope, class_bases, declaration_scopes
|
||||||
from ..ir import CallSite
|
from ..ir import CallSite
|
||||||
from .abstract_domain import Completeness
|
from .abstract_domain import Completeness
|
||||||
|
|
||||||
@@ -53,6 +53,8 @@ class SymbolIndex:
|
|||||||
scopes = declaration_scopes(path, text, language)
|
scopes = declaration_scopes(path, text, language)
|
||||||
declarations.update(_symbol_id(path, scope) for scope in scopes)
|
declarations.update(_symbol_id(path, scope) for scope in scopes)
|
||||||
base_types.update(_inheritance_edges(path, scopes))
|
base_types.update(_inheritance_edges(path, scopes))
|
||||||
|
for type_name, bases in class_bases(path, text, language):
|
||||||
|
base_types[(path, type_name)] = bases
|
||||||
by_name: dict[str, list[SymbolId]] = {}
|
by_name: dict[str, list[SymbolId]] = {}
|
||||||
for declaration in sorted(declarations):
|
for declaration in sorted(declarations):
|
||||||
by_name.setdefault(declaration.name, []).append(declaration)
|
by_name.setdefault(declaration.name, []).append(declaration)
|
||||||
@@ -63,6 +65,53 @@ class SymbolIndex:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ancestors_of(index: SymbolIndex, module: str, type_name: str) -> tuple[str, ...]:
|
||||||
|
"""Repository-defined base types of one type, transitive, nearest first, cycle-safe."""
|
||||||
|
bases: list[str] = []
|
||||||
|
queue: list[str] = [type_name]
|
||||||
|
while queue:
|
||||||
|
current = queue.pop(0)
|
||||||
|
for base in index.base_types.get((module, current), ()):
|
||||||
|
if base in bases or base == type_name:
|
||||||
|
continue
|
||||||
|
bases.append(base)
|
||||||
|
queue.append(base)
|
||||||
|
return tuple(bases)
|
||||||
|
|
||||||
|
|
||||||
|
def descendants_of(index: SymbolIndex, base_name: str) -> tuple[tuple[str, str], ...]:
|
||||||
|
"""(module, type) pairs whose ancestry includes base_name; unrelated types never appear."""
|
||||||
|
return tuple(
|
||||||
|
sorted((module, type_name) for module, type_name in index.base_types if base_name in ancestors_of(index, module, type_name))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def inherited_method(
|
||||||
|
index: SymbolIndex,
|
||||||
|
base_type: str,
|
||||||
|
method_name: str,
|
||||||
|
*,
|
||||||
|
arity: int | None,
|
||||||
|
) -> CallTargetSet:
|
||||||
|
"""Resolve a method as declared on base_type; attachment flows only along descendant edges.
|
||||||
|
|
||||||
|
A unique declaration resolves complete. Declarations of the same base name in
|
||||||
|
several modules stay a candidate set with an ambiguity reason (Req 3.5). No
|
||||||
|
same-name method on an unrelated class ever enters the target set.
|
||||||
|
"""
|
||||||
|
same_name = index.by_name.get(method_name, ())
|
||||||
|
declared = [symbol for symbol in same_name if symbol.enclosing_type == base_type]
|
||||||
|
narrowed = _arity_narrow(tuple(declared), arity) if declared else ()
|
||||||
|
if not narrowed:
|
||||||
|
return CallTargetSet(targets=(), completeness=Completeness(is_complete=False, reasons=("resolution:no_candidate",)))
|
||||||
|
if len(narrowed) == 1:
|
||||||
|
return CallTargetSet(targets=(narrowed[0],), completeness=Completeness(is_complete=True))
|
||||||
|
return CallTargetSet(
|
||||||
|
targets=tuple(narrowed),
|
||||||
|
completeness=Completeness(is_complete=False, reasons=("resolution:inheritance_ambiguous_across_modules",)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _symbol_id(module: str, scope: DeclarationScope) -> SymbolId:
|
def _symbol_id(module: str, scope: DeclarationScope) -> SymbolId:
|
||||||
"""Parameter names are the signature when present; a declaration without them has none."""
|
"""Parameter names are the signature when present; a declaration without them has none."""
|
||||||
signature = tuple(scope.params) if scope.params else None
|
signature = tuple(scope.params) if scope.params else None
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""Task 8.4 -- inheritance and base-declaration attachment (Req 3.5).
|
||||||
|
|
||||||
|
Base-type operations and entries attach only through repository-defined
|
||||||
|
descendant relations; ambiguous inheritance stays incomplete; unrelated
|
||||||
|
same-name classes never receive attachments (the mage-ai shape).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from openultrasast.semantic.obligations.abstract_domain import Completeness
|
||||||
|
from openultrasast.semantic.obligations.resolution import (
|
||||||
|
SymbolIndex,
|
||||||
|
ancestors_of,
|
||||||
|
descendants_of,
|
||||||
|
inherited_method,
|
||||||
|
)
|
||||||
|
|
||||||
|
JAVA_BASE = (
|
||||||
|
"src/Base.java",
|
||||||
|
"public class Base { public void run(String command) { } }",
|
||||||
|
"java",
|
||||||
|
)
|
||||||
|
JAVA_SUB = (
|
||||||
|
"src/Sub.java",
|
||||||
|
"public class Sub extends Base { public void handle() { } }",
|
||||||
|
"java",
|
||||||
|
)
|
||||||
|
JAVA_UNRELATED = (
|
||||||
|
"src/Other.java",
|
||||||
|
"public class Unrelated { public void run(String command) { } }",
|
||||||
|
"java",
|
||||||
|
)
|
||||||
|
JAVA_CHAIN = (
|
||||||
|
"src/Chain.java",
|
||||||
|
"public class A extends B { } class B extends C { } class C { public void audit(String request) { } }",
|
||||||
|
"java",
|
||||||
|
)
|
||||||
|
JAVA_CYCLE = (
|
||||||
|
"src/Cycle.java",
|
||||||
|
"public class P extends Q { } class Q extends P { }",
|
||||||
|
"java",
|
||||||
|
)
|
||||||
|
PHP_BASE_A = (
|
||||||
|
"a/Base.php",
|
||||||
|
"<?php class Base { public function run($command) { } }",
|
||||||
|
"php",
|
||||||
|
)
|
||||||
|
PHP_BASE_B = (
|
||||||
|
"b/Base.php",
|
||||||
|
"<?php class Base { public function run($command) { } }",
|
||||||
|
"php",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _index(*files: tuple[str, str, str]) -> SymbolIndex:
|
||||||
|
return SymbolIndex.build(files)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ancestors_walk_the_transitive_chain_nearest_first() -> None:
|
||||||
|
index = _index(JAVA_CHAIN)
|
||||||
|
|
||||||
|
assert ancestors_of(index, "src/Chain.java", "A") == ("B", "C")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ancestors_survive_inheritance_cycles_without_hanging() -> None:
|
||||||
|
index = _index(JAVA_CYCLE)
|
||||||
|
|
||||||
|
assert ancestors_of(index, "src/Cycle.java", "P") == ("Q",)
|
||||||
|
|
||||||
|
|
||||||
|
def test_descendants_cover_only_repo_defined_descendants() -> None:
|
||||||
|
index = _index(JAVA_BASE, JAVA_SUB, JAVA_UNRELATED)
|
||||||
|
|
||||||
|
assert descendants_of(index, "Base") == (("src/Sub.java", "Sub"),)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inherited_method_resolves_complete_on_a_unique_base() -> None:
|
||||||
|
index = _index(JAVA_BASE, JAVA_SUB, JAVA_UNRELATED)
|
||||||
|
|
||||||
|
target_set = inherited_method(index, "Base", "run", arity=1)
|
||||||
|
|
||||||
|
assert [symbol.enclosing_type for symbol in target_set.targets] == ["Base"]
|
||||||
|
assert target_set.completeness == Completeness(is_complete=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inherited_method_is_absent_when_no_base_declares_it() -> None:
|
||||||
|
index = _index(JAVA_BASE, JAVA_SUB, JAVA_UNRELATED)
|
||||||
|
|
||||||
|
target_set = inherited_method(index, "Base", "handle", arity=None)
|
||||||
|
|
||||||
|
assert target_set.targets == ()
|
||||||
|
assert target_set.completeness == Completeness(is_complete=False, reasons=("resolution:no_candidate",))
|
||||||
|
|
||||||
|
|
||||||
|
def test_inherited_method_keeps_ambiguous_bases_as_incomplete_candidates() -> None:
|
||||||
|
index = _index(PHP_BASE_A, PHP_BASE_B)
|
||||||
|
|
||||||
|
target_set = inherited_method(index, "Base", "run", arity=None)
|
||||||
|
|
||||||
|
assert len(target_set.targets) == 2
|
||||||
|
assert {symbol.module for symbol in target_set.targets} == {"a/Base.php", "b/Base.php"}
|
||||||
|
assert target_set.completeness == Completeness(is_complete=False, reasons=("resolution:inheritance_ambiguous_across_modules",))
|
||||||
|
|
||||||
|
|
||||||
|
def test_inherited_method_narrows_overloads_by_arity() -> None:
|
||||||
|
overloaded_base = (
|
||||||
|
"src/Ops.java",
|
||||||
|
"public class Ops { public void go() { } public void go(String payload) { } }",
|
||||||
|
"java",
|
||||||
|
)
|
||||||
|
index = _index(overloaded_base)
|
||||||
|
|
||||||
|
target_set = inherited_method(index, "Ops", "go", arity=1)
|
||||||
|
|
||||||
|
assert [symbol.signature for symbol in target_set.targets] == [("payload",)]
|
||||||
|
assert target_set.completeness == Completeness(is_complete=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unrelated_same_name_classes_never_receive_base_attachments() -> None:
|
||||||
|
index = _index(JAVA_BASE, JAVA_SUB, JAVA_UNRELATED)
|
||||||
|
|
||||||
|
target_set = inherited_method(index, "Base", "run", arity=1)
|
||||||
|
unrelated = index.by_name["run"]
|
||||||
|
|
||||||
|
assert all(symbol.enclosing_type == "Base" for symbol in target_set.targets)
|
||||||
|
assert ("src/Other.java", "Unrelated") not in descendants_of(index, "Base")
|
||||||
|
assert any(symbol.enclosing_type == "Unrelated" and symbol.module == "src/Other.java" for symbol in unrelated)
|
||||||
Reference in New Issue
Block a user