feat(access-control-prover): 11.1 entry-recognition strategy registry

This commit is contained in:
Marius Ciepluch
2026-10-29 20:19:31 +01:00
parent 82e4a1ad97
commit 86709daf2e
5 changed files with 248 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
"""Entry-point mapping package: legacy recognisers plus the strategy registry."""
from .legacy import * # noqa: F401,F403
from .legacy import _php_methods as _php_methods # re-exported internal helpers (enforcement adapter)
from .legacy import _php_routes as _php_routes
@@ -8,7 +8,8 @@ from dataclasses import asdict, dataclass, replace
from pathlib import Path
from typing import Any, Literal, cast
from .preprocess import FileTarget
from ..preprocess import FileTarget
from .registry import registry_entries
MappingKind = Literal["semgrep", "codeql", "differential", "sharp_edge"]
AccessLevel = Literal[
@@ -107,6 +108,7 @@ def analyze_entry_points(
for target in targets:
text = source_texts[target.path]
records.extend(_language_entry_points(target, text))
records.extend(registry_entries(target, text))
if target.language == "php":
framework_entries = _php_framework_entries(target, text, source_texts)
names = {e.function_name for e in framework_entries}
@@ -138,7 +140,7 @@ def php_hook_callbacks(root: Path, targets: list[FileTarget], register_calls: Se
plugin system's event bus -- and a second framework is a row in a TOML, not a change to this function.
"""
if register_calls is None:
from .semantic.facts import load_facts
from ..semantic.facts import load_facts
register_calls = tuple(sorted({call for fact in load_facts().for_language("php").dispatches for call in fact.register}))
if not register_calls:
@@ -222,7 +224,7 @@ class _PhpMethod:
def _php_methods(text: str) -> list[_PhpMethod]:
"""CST class and method boundaries, including the method's own attributes and docblock."""
from .semantic.extra import grammar_for
from ..semantic.extra import grammar_for
grammar = grammar_for("php")
if grammar is None:
@@ -417,7 +419,7 @@ def _tornado_entries(target: FileTarget, text: str) -> list[EntryPointRecord]:
def _remix_exports(target: FileTarget, text: str) -> list[EntryPointRecord]:
if "app/routes/" not in "/" + target.path:
return []
from .semantic.extra import grammar_for
from ..semantic.extra import grammar_for
grammar = grammar_for("tsx" if Path(target.path).suffix == ".tsx" else target.language)
if grammar is None:
@@ -490,7 +492,7 @@ def entry_framework(entry: object) -> str | None:
def _java_methods(target: FileTarget, text: str) -> list[tuple[object, str, str]]:
"""Use the CST to keep class declarations, parameters and method bodies separate."""
from .semantic.extra import grammar_for
from ..semantic.extra import grammar_for
grammar = grammar_for("java")
if grammar is None:
+52
View File
@@ -0,0 +1,52 @@
"""Entry-recognition strategy registry (task 11.1, Reqs 1.1, 1.5, 1.7).
Target knowledge enters entry recognition through registered strategies; the
registry stamps every returned record with the strategy identifier unless the
strategy recorded its own provenance. The registry itself names no target.
"""
from dataclasses import replace
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
if TYPE_CHECKING:
from .legacy import EntryPointRecord
__all__ = ["EntryRecogniser", "register", "registry_entries", "registered_strategies"]
@runtime_checkable
class EntryRecogniser(Protocol):
"""A target-specific entry-recognition strategy registered from data or a strategy module."""
identifier: str
languages: tuple[str, ...]
def recognize(self, target: Any, text: str) -> list["EntryPointRecord"]:
"""Return the entry points this strategy recognises in one file."""
...
_STRATEGIES: dict[str, EntryRecogniser] = {}
def register(recogniser: EntryRecogniser) -> None:
"""Register a strategy; re-registering an identifier replaces the earlier strategy."""
_STRATEGIES[recogniser.identifier] = recogniser
def registered_strategies(language: str) -> tuple[EntryRecogniser, ...]:
"""Return the strategies that recognise entries for one language, ordered by identifier."""
return tuple(
strategy for strategy in sorted(_STRATEGIES.values(), key=lambda strategy: strategy.identifier) if language in strategy.languages
)
def registry_entries(target: Any, text: str) -> list["EntryPointRecord"]:
"""Run every matching strategy and stamp its identifier onto each returned record."""
records: list[EntryPointRecord] = []
for strategy in registered_strategies(getattr(target, "language", "")):
for record in strategy.recognize(target, text):
if record.provenance == "":
record = replace(record, provenance=f"strategy:{strategy.identifier}")
records.append(record)
return records