feat(access-control-prover): 8.4 inheritance and base-declaration attachment through repo-defined descendants

This commit is contained in:
Marius Ciepluch
2026-10-29 16:10:44 +01:00
parent 8ef2629e0b
commit 2466670ba3
4 changed files with 208 additions and 2 deletions
+127
View File
@@ -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)