mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(access-control-prover): match reviewed entry facts
This commit is contained in:
@@ -373,7 +373,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
|
||||
- Done when stored observation and match bytes resolve canonically and byte or reference substitution is refused deterministically.
|
||||
- _Requirements: 13.5_
|
||||
- _Boundary: Fact-Match Artifact Writer_
|
||||
- [ ] 14.7 Implement the authority-bearing structural matcher
|
||||
- [x] 14.7 Implement the authority-bearing structural matcher
|
||||
- Build typed observations from repository structure for the requirements declared by the selected reviewed fact; do not infer missing structure from a regex or name hit.
|
||||
- Use task 14.6 to persist every observation and completed `FactMatchEvidence`, then pass the immutable match reference with family contract, policies, lineages, custody and read-only resolver to detection-core.
|
||||
- Bind matcher behavior and fact lineage to current custody and the evaluation population so same-population label influence is incomplete or refused.
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Authority-bearing application of reviewed Express structural facts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
|
||||
from openultrasast.core import canonical
|
||||
from openultrasast.core.factapply import apply_fact
|
||||
from openultrasast.core.factmatch import (
|
||||
FactMatchDecision,
|
||||
FactMatchEvidence,
|
||||
StructuralObservation,
|
||||
derive_fact_match_digest,
|
||||
validate_fact_match,
|
||||
)
|
||||
from openultrasast.core.factpack import FactPack, SemanticFact, StructuralRequirement, StructuralRequirementKind
|
||||
from openultrasast.core.semantics import SemanticRelation, SourceLocation
|
||||
from openultrasast.mapping.entry_fact_structure import (
|
||||
CandidateStructure,
|
||||
EntryStructureError,
|
||||
ResolvedHandler,
|
||||
analyze_candidate,
|
||||
candidates,
|
||||
read_source_structure,
|
||||
)
|
||||
from openultrasast.mapping.fact_authority import EntryFactAuthorityContext
|
||||
from openultrasast.mapping.fact_match_store import FactMatchArtifactWriter
|
||||
|
||||
|
||||
class EntryFactMatcherError(ValueError):
|
||||
"""Reviewed structural evidence could not be safely produced."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _JoinedResolver:
|
||||
primary: canonical.ContentResolver
|
||||
fallback: canonical.ContentResolver
|
||||
|
||||
def resolve(self, reference: canonical.ContentRef) -> canonical.ResolvedContent:
|
||||
try:
|
||||
return self.primary.resolve(reference)
|
||||
except canonical.ArtifactResolutionError:
|
||||
return self.fallback.resolve(reference)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EntryFactMatchResult:
|
||||
"""Persisted match, lexical evidence, and exact core application result."""
|
||||
|
||||
pack: FactPack
|
||||
match: FactMatchEvidence
|
||||
match_reference: canonical.ContentRef
|
||||
decision: FactMatchDecision
|
||||
relations: tuple[SemanticRelation, ...]
|
||||
resolver: canonical.ContentResolver
|
||||
resolved_handlers: tuple[ResolvedHandler, ...]
|
||||
structural_reasons: tuple[str, ...]
|
||||
|
||||
|
||||
def _selected(pack: FactPack, fact_id: str) -> SemanticFact:
|
||||
facts = tuple(fact for fact in pack.facts if fact.fact_id == fact_id)
|
||||
if len(facts) != 1:
|
||||
raise EntryFactMatcherError("reviewed fact is missing or ambiguous")
|
||||
return facts[0]
|
||||
|
||||
|
||||
def _pack(authority: EntryFactAuthorityContext, language: str, fact_id: str) -> tuple[FactPack, SemanticFact]:
|
||||
if not isinstance(authority, EntryFactAuthorityContext):
|
||||
raise TypeError("authority must be EntryFactAuthorityContext")
|
||||
packs = tuple(pack for pack in authority.bundle.packs if pack.language == language)
|
||||
if len(packs) != 1:
|
||||
raise EntryFactMatcherError("reviewed language pack is missing or ambiguous")
|
||||
return packs[0], _selected(packs[0], fact_id)
|
||||
|
||||
|
||||
def _qualified_target(fact: SemanticFact) -> str:
|
||||
targets = tuple(
|
||||
item.expected
|
||||
for item in fact.structural_requirements
|
||||
if item.kind is StructuralRequirementKind.QUALIFIED_NAME and isinstance(item.expected, str)
|
||||
)
|
||||
if len(targets) != 1:
|
||||
raise EntryFactMatcherError("reviewed fact lacks one textual qualified target")
|
||||
return targets[0]
|
||||
|
||||
|
||||
def _observed(requirement: StructuralRequirement, structure: CandidateStructure) -> tuple[canonical.JsonValue, ...]:
|
||||
if requirement.kind is StructuralRequirementKind.QUALIFIED_NAME:
|
||||
return (structure.candidate.target,)
|
||||
if requirement.kind is StructuralRequirementKind.AST_SHAPE:
|
||||
return (structure.candidate.shape,)
|
||||
if requirement.kind is StructuralRequirementKind.RECEIVER_TYPE:
|
||||
return structure.receiver_types
|
||||
if requirement.kind is StructuralRequirementKind.ARGUMENT_SHAPE and requirement.field == "arguments":
|
||||
return structure.argument_shapes
|
||||
if requirement.kind is StructuralRequirementKind.ARGUMENT_SHAPE and requirement.field == "binding":
|
||||
return structure.binding_shapes
|
||||
return ()
|
||||
|
||||
|
||||
def _location(path: str, structure: CandidateStructure) -> SourceLocation:
|
||||
node = structure.candidate.node
|
||||
symbol = structure.candidate.target
|
||||
if len(structure.resolved_handlers) == 1:
|
||||
symbol = f"{symbol}->{structure.resolved_handlers[0].identity}"
|
||||
return SourceLocation(path, node.start_point.row + 1, node.start_point.column, symbol)
|
||||
|
||||
|
||||
def _persist_observations(
|
||||
fact: SemanticFact,
|
||||
structure: CandidateStructure,
|
||||
location: SourceLocation,
|
||||
writer: FactMatchArtifactWriter,
|
||||
) -> tuple[StructuralObservation, ...]:
|
||||
values: list[StructuralObservation] = []
|
||||
pending = canonical.ContentRef("pending", "0" * 64, "application/json", 0, "0" * 64, "0" * 64)
|
||||
for requirement in fact.structural_requirements:
|
||||
for observed in _observed(requirement, structure):
|
||||
provisional = StructuralObservation(requirement.requirement_id, location, observed, pending)
|
||||
values.append(replace(provisional, evidence=writer.persist_observation(provisional)))
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def _match(
|
||||
pack: FactPack,
|
||||
fact: SemanticFact,
|
||||
location: SourceLocation,
|
||||
observations: tuple[StructuralObservation, ...],
|
||||
authority: EntryFactAuthorityContext,
|
||||
) -> FactMatchEvidence:
|
||||
bundle = authority.bundle
|
||||
provisional = FactMatchEvidence(
|
||||
pack.normalized_content_digest,
|
||||
fact.fact_id,
|
||||
location,
|
||||
observations,
|
||||
bundle.matcher_revision,
|
||||
bundle.matcher_behavior_digest,
|
||||
bundle.matcher_lineage_manifest_digest,
|
||||
"0" * 64,
|
||||
)
|
||||
return replace(provisional, match_digest=derive_fact_match_digest(provisional))
|
||||
|
||||
|
||||
def _apply(
|
||||
pack: FactPack,
|
||||
fact: SemanticFact,
|
||||
match: FactMatchEvidence,
|
||||
structure: CandidateStructure,
|
||||
authority: EntryFactAuthorityContext,
|
||||
writer: FactMatchArtifactWriter,
|
||||
evaluation_population_digest: str,
|
||||
) -> EntryFactMatchResult:
|
||||
reference = writer.persist_match(match)
|
||||
resolver = _JoinedResolver(writer.resolver, authority.resolver)
|
||||
bundle = authority.bundle
|
||||
core_tail = (
|
||||
evaluation_population_digest,
|
||||
bundle.lineages,
|
||||
authority.custody,
|
||||
bundle.custody_policy,
|
||||
bundle.review_policy,
|
||||
resolver,
|
||||
)
|
||||
decision = validate_fact_match(pack, fact.fact_id, match, *core_tail)
|
||||
relations = apply_fact(pack, fact.fact_id, match, reference, bundle.contract, *core_tail)
|
||||
return EntryFactMatchResult(
|
||||
pack,
|
||||
match,
|
||||
reference,
|
||||
decision,
|
||||
relations,
|
||||
resolver,
|
||||
structure.resolved_handlers,
|
||||
structure.reasons,
|
||||
)
|
||||
|
||||
|
||||
def match_entry_fact(
|
||||
*,
|
||||
repository_root: Path,
|
||||
relative_path: str,
|
||||
language: str,
|
||||
fact_id: str,
|
||||
authority: EntryFactAuthorityContext,
|
||||
writer: FactMatchArtifactWriter,
|
||||
evaluation_population_digest: str,
|
||||
) -> tuple[EntryFactMatchResult, ...]:
|
||||
"""Resolve lexical structure, persist it, and apply one reviewed fact."""
|
||||
try:
|
||||
source = read_source_structure(repository_root, relative_path, language)
|
||||
except EntryStructureError as exc:
|
||||
raise EntryFactMatcherError(str(exc)) from exc
|
||||
pack, fact = _pack(authority, language, fact_id)
|
||||
results: list[EntryFactMatchResult] = []
|
||||
for candidate in candidates(source, _qualified_target(fact)):
|
||||
structure = analyze_candidate(source, candidate)
|
||||
location = _location(source.path, structure)
|
||||
observations = _persist_observations(fact, structure, location, writer)
|
||||
match = _match(pack, fact, location, observations, authority)
|
||||
results.append(_apply(pack, fact, match, structure, authority, writer, evaluation_population_digest))
|
||||
return tuple(results)
|
||||
|
||||
|
||||
__all__ = ["EntryFactMatchResult", "EntryFactMatcherError", "ResolvedHandler", "match_entry_fact"]
|
||||
@@ -0,0 +1,391 @@
|
||||
"""Lexical JavaScript/TypeScript structure for reviewed Express facts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from openultrasast.core.semantics import SourceLocation
|
||||
from openultrasast.semantic.extra import grammar_for
|
||||
|
||||
_LANGUAGES = frozenset({"javascript", "typescript"})
|
||||
_SCOPES = frozenset({"program", "statement_block", "function_declaration", "function_expression", "arrow_function", "method_definition"})
|
||||
_FUNCTION_VALUES = frozenset({"function_expression", "arrow_function"})
|
||||
_HOISTED = frozenset({"express_import", "function", "parameter"})
|
||||
_STRINGS = frozenset({"string", "template_string"})
|
||||
|
||||
|
||||
class EntryStructureError(ValueError):
|
||||
"""Repository structure was unavailable or unsafe to interpret."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResolvedHandler:
|
||||
identity: str
|
||||
symbol: str
|
||||
location: SourceLocation
|
||||
declaration_kind: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Declaration:
|
||||
name: str
|
||||
node: Any
|
||||
scope: Any
|
||||
kind: str
|
||||
value: Any | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Resolution:
|
||||
declarations: tuple[_Declaration, ...]
|
||||
reasons: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StructuralCandidate:
|
||||
node: Any
|
||||
target: str
|
||||
shape: str
|
||||
receiver: str | None
|
||||
arguments: Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandidateStructure:
|
||||
candidate: StructuralCandidate
|
||||
receiver_types: tuple[str, ...]
|
||||
argument_shapes: tuple[str, ...]
|
||||
binding_shapes: tuple[str, ...]
|
||||
resolved_handlers: tuple[ResolvedHandler, ...]
|
||||
reasons: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SourceStructure:
|
||||
path: str
|
||||
data: bytes
|
||||
root: Any
|
||||
declarations: tuple[_Declaration, ...]
|
||||
writes: tuple[Any, ...]
|
||||
|
||||
|
||||
def _walk(node: Any) -> Iterator[Any]:
|
||||
yield node
|
||||
for child in node.named_children:
|
||||
yield from _walk(child)
|
||||
|
||||
|
||||
def _text(node: Any, data: bytes) -> str:
|
||||
return data[node.start_byte : node.end_byte].decode("utf-8")
|
||||
|
||||
|
||||
def _string_value(node: Any, data: bytes) -> str | None:
|
||||
if node is None or node.type != "string":
|
||||
return None
|
||||
raw = _text(node, data)
|
||||
if len(raw) < 2 or raw[0] not in {'"', "'"} or raw[-1] != raw[0]:
|
||||
return None
|
||||
return raw[1:-1]
|
||||
|
||||
|
||||
def _member_target(node: Any, data: bytes) -> tuple[str, str] | None:
|
||||
if node is None or node.type != "member_expression":
|
||||
return None
|
||||
owner = node.child_by_field_name("object")
|
||||
member = node.child_by_field_name("property")
|
||||
if owner is None or member is None or owner.type != "identifier":
|
||||
return None
|
||||
return _text(owner, data), _text(member, data)
|
||||
|
||||
|
||||
def _call_target(call: Any, data: bytes) -> str | None:
|
||||
function = call.child_by_field_name("function")
|
||||
if function is None:
|
||||
return None
|
||||
if function.type == "identifier":
|
||||
return _text(function, data)
|
||||
member = _member_target(function, data)
|
||||
return ".".join(member) if member is not None else None
|
||||
|
||||
|
||||
def _scope(node: Any) -> Any:
|
||||
parent = node.parent
|
||||
while parent is not None:
|
||||
if parent.type in _SCOPES:
|
||||
return parent
|
||||
parent = parent.parent
|
||||
raise EntryStructureError("repository source declaration has no lexical scope")
|
||||
|
||||
|
||||
def _import_declarations(root: Any, data: bytes) -> list[_Declaration]:
|
||||
values: list[_Declaration] = []
|
||||
for node in root.named_children:
|
||||
if node.type != "import_statement" or _string_value(node.child_by_field_name("source"), data) != "express":
|
||||
continue
|
||||
clause = next((item for item in node.named_children if item.type == "import_clause"), None)
|
||||
if clause is None:
|
||||
continue
|
||||
for name in clause.named_children:
|
||||
if name.type == "identifier":
|
||||
values.append(_Declaration(_text(name, data), name, root, "express_import", None))
|
||||
return values
|
||||
|
||||
|
||||
def _is_express_require(value: Any, data: bytes) -> bool:
|
||||
if value is None or value.type != "call_expression" or _call_target(value, data) != "require":
|
||||
return False
|
||||
arguments = value.child_by_field_name("arguments")
|
||||
return bool(arguments and len(arguments.named_children) == 1 and _string_value(arguments.named_children[0], data) == "express")
|
||||
|
||||
|
||||
def _variable_declaration(node: Any, data: bytes) -> _Declaration | None:
|
||||
name = node.child_by_field_name("name")
|
||||
if name is None or name.type != "identifier":
|
||||
return None
|
||||
value = node.child_by_field_name("value")
|
||||
kind = "express_require" if _is_express_require(value, data) else "variable"
|
||||
if getattr(value, "type", None) in _FUNCTION_VALUES:
|
||||
kind = "handler_variable"
|
||||
return _Declaration(_text(name, data), name, _scope(node), kind, value)
|
||||
|
||||
|
||||
def _parameter_declarations(node: Any, data: bytes) -> list[_Declaration]:
|
||||
parameters = node.child_by_field_name("parameters")
|
||||
if parameters is None:
|
||||
return []
|
||||
values: list[_Declaration] = []
|
||||
for item in parameters.named_children:
|
||||
name = item if item.type == "identifier" else item.child_by_field_name("pattern")
|
||||
if name is not None and name.type == "identifier":
|
||||
values.append(_Declaration(_text(name, data), name, node, "parameter", None))
|
||||
return values
|
||||
|
||||
|
||||
def _declarations(root: Any, data: bytes) -> tuple[_Declaration, ...]:
|
||||
values = _import_declarations(root, data)
|
||||
for node in _walk(root):
|
||||
if node.type == "variable_declarator":
|
||||
declaration = _variable_declaration(node, data)
|
||||
if declaration is not None:
|
||||
values.append(declaration)
|
||||
if node.type == "function_declaration":
|
||||
name = node.child_by_field_name("name")
|
||||
if name is not None:
|
||||
values.append(_Declaration(_text(name, data), name, _scope(node), "function", node))
|
||||
if node.type in {"function_declaration", "function_expression", "arrow_function", "method_definition"}:
|
||||
values.extend(_parameter_declarations(node, data))
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def _writes(root: Any) -> tuple[Any, ...]:
|
||||
kinds = {"assignment_expression", "augmented_assignment_expression", "update_expression"}
|
||||
return tuple(node for node in _walk(root) if node.type in kinds)
|
||||
|
||||
|
||||
def read_source_structure(repository_root: Path, relative_path: str, language: str) -> SourceStructure:
|
||||
if language not in _LANGUAGES:
|
||||
raise EntryStructureError("repository source language is unsupported")
|
||||
root_path = Path(repository_root).resolve()
|
||||
source_path = (root_path / relative_path).resolve()
|
||||
try:
|
||||
normalized = source_path.relative_to(root_path).as_posix()
|
||||
data = source_path.read_bytes()
|
||||
except (OSError, ValueError) as exc:
|
||||
raise EntryStructureError("repository source is unreadable or outside its root") from exc
|
||||
if not data:
|
||||
raise EntryStructureError("repository source is empty")
|
||||
grammar = grammar_for(language)
|
||||
if grammar is None:
|
||||
raise EntryStructureError("repository source parser is unavailable")
|
||||
try:
|
||||
from tree_sitter import Parser
|
||||
|
||||
tree = Parser(grammar).parse(data)
|
||||
except Exception as exc:
|
||||
raise EntryStructureError("repository source parse failed") from exc
|
||||
if tree.root_node.has_error:
|
||||
raise EntryStructureError("repository source parse failed")
|
||||
return SourceStructure(normalized, data, tree.root_node, _declarations(tree.root_node, data), _writes(tree.root_node))
|
||||
|
||||
|
||||
def candidates(source: SourceStructure, target: str) -> tuple[StructuralCandidate, ...]:
|
||||
values: list[StructuralCandidate] = []
|
||||
for node in _walk(source.root):
|
||||
if node.type != "call_expression" or _call_target(node, source.data) != target:
|
||||
continue
|
||||
function = node.child_by_field_name("function")
|
||||
member = _member_target(function, source.data)
|
||||
values.append(
|
||||
StructuralCandidate(
|
||||
node,
|
||||
target,
|
||||
"member_call" if member is not None else "factory_call",
|
||||
member[0] if member is not None else None,
|
||||
node.child_by_field_name("arguments"),
|
||||
)
|
||||
)
|
||||
return tuple(values)
|
||||
|
||||
|
||||
def _visible_scopes(node: Any) -> tuple[Any, ...]:
|
||||
scopes: list[Any] = []
|
||||
current = node.parent
|
||||
while current is not None:
|
||||
if current.type in _SCOPES:
|
||||
scopes.append(current)
|
||||
current = current.parent
|
||||
return tuple(scopes)
|
||||
|
||||
|
||||
def _raw_resolution(source: SourceStructure, use: Any, name: str) -> tuple[_Declaration, ...]:
|
||||
for scope in _visible_scopes(use):
|
||||
matches = tuple(item for item in source.declarations if item.name == name and item.scope == scope)
|
||||
if matches:
|
||||
live = tuple(item for item in matches if item.kind in _HOISTED or item.node.start_byte < use.start_byte)
|
||||
return live or matches
|
||||
return ()
|
||||
|
||||
|
||||
def _write_name(write: Any, data: bytes) -> str | None:
|
||||
target = write.child_by_field_name("left") or write.child_by_field_name("argument")
|
||||
return _text(target, data) if target is not None and target.type == "identifier" else None
|
||||
|
||||
|
||||
def _is_reassigned(source: SourceStructure, use: Any, declaration: _Declaration) -> bool:
|
||||
for write in source.writes:
|
||||
if write.start_byte >= use.start_byte or _write_name(write, source.data) != declaration.name:
|
||||
continue
|
||||
resolved = _raw_resolution(source, write, declaration.name)
|
||||
if len(resolved) == 1 and resolved[0] == declaration:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _resolve(source: SourceStructure, use: Any, name: str, role: str) -> _Resolution:
|
||||
nearest = _raw_resolution(source, use, name)
|
||||
reasons: list[str] = []
|
||||
if not nearest:
|
||||
reasons.append(f"{role}_binding_missing:{name}")
|
||||
if len(nearest) > 1:
|
||||
reasons.append(f"{role}_binding_ambiguous:{name}")
|
||||
if len(nearest) == 1 and nearest[0].kind not in _HOISTED and nearest[0].node.start_byte > use.start_byte:
|
||||
reasons.append(f"{role}_binding_not_live:{name}")
|
||||
if len(nearest) == 1 and _is_reassigned(source, use, nearest[0]):
|
||||
reasons.append(f"{role}_binding_reassigned:{name}")
|
||||
return _Resolution(nearest, tuple(sorted(set(reasons))))
|
||||
|
||||
|
||||
def _express_binding(source: SourceStructure, use: Any, name: str) -> tuple[bool, tuple[str, ...]]:
|
||||
resolved = _resolve(source, use, name, "express")
|
||||
reasons = list(resolved.reasons)
|
||||
authoritative = len(resolved.declarations) == 1 and resolved.declarations[0].kind in {"express_import", "express_require"}
|
||||
if resolved.declarations and not authoritative:
|
||||
reasons.append(f"express_binding_not_authoritative:{name}")
|
||||
return authoritative and not reasons, tuple(sorted(set(reasons)))
|
||||
|
||||
|
||||
def _receiver_kind(source: SourceStructure, declaration: _Declaration) -> tuple[str | None, tuple[str, ...]]:
|
||||
value = declaration.value
|
||||
if value is None or value.type != "call_expression":
|
||||
return None, (f"receiver_factory_not_authoritative:{declaration.name}",)
|
||||
arguments = value.child_by_field_name("arguments")
|
||||
if arguments is None or arguments.named_children:
|
||||
return None, (f"receiver_factory_not_authoritative:{declaration.name}",)
|
||||
target = _call_target(value, source.data)
|
||||
if target is None:
|
||||
return None, (f"receiver_factory_not_authoritative:{declaration.name}",)
|
||||
express_name = target.removesuffix(".Router") if target.endswith(".Router") else target
|
||||
authoritative, reasons = _express_binding(source, value, express_name)
|
||||
if not authoritative:
|
||||
return None, reasons
|
||||
return ("express_router" if target.endswith(".Router") else "express_application"), ()
|
||||
|
||||
|
||||
def _receiver(source: SourceStructure, candidate: StructuralCandidate) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
||||
if candidate.receiver is None:
|
||||
return (), ()
|
||||
resolved = _resolve(source, candidate.node, candidate.receiver, "receiver")
|
||||
if any("reassigned" in item or "not_live" in item for item in resolved.reasons):
|
||||
return (), resolved.reasons
|
||||
kinds: list[str] = []
|
||||
reasons = list(resolved.reasons)
|
||||
for declaration in resolved.declarations:
|
||||
kind, extra = _receiver_kind(source, declaration)
|
||||
if kind is not None:
|
||||
kinds.append(kind)
|
||||
reasons.extend(extra)
|
||||
normalized = tuple(sorted(set(reasons)))
|
||||
return (tuple(kinds) if len(kinds) == len(resolved.declarations) else ()), normalized
|
||||
|
||||
|
||||
def _handler(source: SourceStructure, node: Any) -> tuple[tuple[ResolvedHandler, ...], tuple[str, ...]]:
|
||||
if node.type in _FUNCTION_VALUES:
|
||||
location = SourceLocation(source.path, node.start_point.row + 1, node.start_point.column, "<inline>")
|
||||
identity = f"<inline>@{source.path}:{location.line}:{location.column}"
|
||||
return (ResolvedHandler(identity, "<inline>", location, node.type),), ()
|
||||
if node.type != "identifier":
|
||||
return (), ("handler_shape_unsupported",)
|
||||
name = _text(node, source.data)
|
||||
resolved = _resolve(source, node, name, "handler")
|
||||
if any("reassigned" in item or "not_live" in item for item in resolved.reasons):
|
||||
return (), resolved.reasons
|
||||
handlers: list[ResolvedHandler] = []
|
||||
reasons = list(resolved.reasons)
|
||||
for declaration in resolved.declarations:
|
||||
if declaration.kind not in {"function", "handler_variable"}:
|
||||
reasons.append(f"handler_binding_not_repository_defined:{name}")
|
||||
continue
|
||||
location = SourceLocation(
|
||||
source.path,
|
||||
declaration.node.start_point.row + 1,
|
||||
declaration.node.start_point.column,
|
||||
name,
|
||||
)
|
||||
identity = f"{name}@{source.path}:{location.line}:{location.column}"
|
||||
handlers.append(ResolvedHandler(identity, name, location, declaration.kind))
|
||||
normalized = tuple(sorted(set(reasons)))
|
||||
return (tuple(handlers) if len(handlers) == len(resolved.declarations) else ()), normalized
|
||||
|
||||
|
||||
def _arguments(
|
||||
source: SourceStructure, candidate: StructuralCandidate
|
||||
) -> tuple[tuple[str, ...], tuple[ResolvedHandler, ...], tuple[str, ...]]:
|
||||
arguments = candidate.arguments
|
||||
if arguments is None or len(arguments.named_children) < 2:
|
||||
return (), (), ("route_arguments_incomplete",)
|
||||
if arguments.named_children[0].type not in _STRINGS:
|
||||
return (), (), ("route_path_not_literal",)
|
||||
handlers, reasons = _handler(source, arguments.named_children[-1])
|
||||
return tuple("route_path_and_handler" for _ in handlers), handlers, reasons
|
||||
|
||||
|
||||
def _binding(source: SourceStructure, candidate: StructuralCandidate) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
||||
parent = candidate.node.parent
|
||||
if parent is None or parent.type != "variable_declarator" or parent.child_by_field_name("value") != candidate.node:
|
||||
return (), ("application_binding_missing",)
|
||||
if candidate.arguments is None or candidate.arguments.named_children:
|
||||
return (), ("application_factory_arguments_unsupported",)
|
||||
authoritative, reasons = _express_binding(source, candidate.node, candidate.target)
|
||||
return (("application_instance_binding",) if authoritative else ()), reasons
|
||||
|
||||
|
||||
def analyze_candidate(source: SourceStructure, candidate: StructuralCandidate) -> CandidateStructure:
|
||||
receiver_types, receiver_reasons = _receiver(source, candidate)
|
||||
argument_shapes, handlers, argument_reasons = _arguments(source, candidate) if candidate.receiver else ((), (), ())
|
||||
binding_shapes, binding_reasons = _binding(source, candidate) if candidate.receiver is None else ((), ())
|
||||
reasons = tuple(sorted(set((*receiver_reasons, *argument_reasons, *binding_reasons))))
|
||||
return CandidateStructure(candidate, receiver_types, argument_shapes, binding_shapes, handlers, reasons)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CandidateStructure",
|
||||
"EntryStructureError",
|
||||
"ResolvedHandler",
|
||||
"SourceStructure",
|
||||
"analyze_candidate",
|
||||
"candidates",
|
||||
"read_source_structure",
|
||||
]
|
||||
@@ -0,0 +1,686 @@
|
||||
"""Authority-bearing structural matching for reviewed Express entry facts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from openultrasast.core import canonical, provenance
|
||||
from openultrasast.core.factapply import FactApplicationError, apply_fact
|
||||
from openultrasast.core.factpack import FactPackValidationError
|
||||
from openultrasast.core.semantics import RelationKind, RelationStatus
|
||||
from openultrasast.mapping.entry_fact_matcher import match_entry_fact
|
||||
from openultrasast.mapping.entry_packs import load_installed_entry_fact_authority
|
||||
from openultrasast.mapping.fact_authority import EntryFactAuthorityContext
|
||||
from openultrasast.mapping.fact_match_store import FactMatchArtifactWriter
|
||||
|
||||
|
||||
class _UnusedCustody:
|
||||
def current_log(self, population_digest: str) -> provenance.PopulationCustodyLog:
|
||||
raise AssertionError(f"authority load read custody: {population_digest}")
|
||||
|
||||
|
||||
class _LogsCustody:
|
||||
def __init__(
|
||||
self,
|
||||
logs: tuple[provenance.PopulationCustodyLog, ...],
|
||||
policy: provenance.PopulationCustodyPolicy,
|
||||
) -> None:
|
||||
self._logs = {log.population_digest: log for log in logs}
|
||||
self._policy = policy
|
||||
|
||||
def current_log(self, population_digest: str) -> provenance.PopulationCustodyLog:
|
||||
try:
|
||||
return self._logs[population_digest]
|
||||
except KeyError as exc:
|
||||
raise provenance.PopulationCustodyError("population custody log not found") from exc
|
||||
|
||||
def append(self, request: provenance.PopulationUseRequest) -> provenance.PopulationUseReceipt:
|
||||
prior = self.current_log(request.population_digest)
|
||||
custodian = self._policy.reviewed_custodian_digests[0]
|
||||
receipt = provenance.record_population_use(request, prior, custodian, self._policy)
|
||||
self._logs[request.population_digest] = provenance.append_population_use(prior, receipt, self._policy)
|
||||
return receipt
|
||||
|
||||
|
||||
def _authority() -> tuple[EntryFactAuthorityContext, str, _LogsCustody]:
|
||||
loaded = load_installed_entry_fact_authority(_UnusedCustody())
|
||||
bundle = loaded.bundle
|
||||
custodian = bundle.custody_policy.reviewed_custodian_digests[0]
|
||||
logs: dict[str, provenance.PopulationCustodyLog] = {}
|
||||
for lineage in bundle.lineages:
|
||||
for use in lineage.uses:
|
||||
prior = logs.setdefault(
|
||||
use.source_population_digest,
|
||||
provenance.empty_population_custody_log(use.source_population_digest, custodian),
|
||||
)
|
||||
request = provenance.PopulationUseRequest(
|
||||
use.source_population_digest,
|
||||
lineage.arm_id,
|
||||
use.purpose,
|
||||
use.labels_accessed,
|
||||
use.influenced_artifact_digest,
|
||||
prior.log_digest,
|
||||
)
|
||||
receipt = provenance.record_population_use(request, prior, custodian, bundle.custody_policy)
|
||||
assert receipt.receipt_digest == use.population_use_receipt_digest
|
||||
logs[use.source_population_digest] = provenance.append_population_use(prior, receipt, bundle.custody_policy)
|
||||
evaluation = canonical.content_digest("untouched-entry-matcher-evaluation")
|
||||
logs[evaluation] = provenance.empty_population_custody_log(evaluation, custodian)
|
||||
custody = _LogsCustody(tuple(logs.values()), bundle.custody_policy)
|
||||
return replace(loaded, custody=custody), evaluation, custody
|
||||
|
||||
|
||||
def _writer(tmp_path: Path, context: EntryFactAuthorityContext) -> FactMatchArtifactWriter:
|
||||
return FactMatchArtifactWriter(
|
||||
tmp_path / "matches",
|
||||
context.bundle.review_policy.trusted_store_identities[0],
|
||||
canonical.content_digest("retain-entry-fact-match-tests"),
|
||||
)
|
||||
|
||||
|
||||
def _source(tmp_path: Path, name: str, content: str) -> tuple[Path, str]:
|
||||
root = tmp_path / "repository"
|
||||
path = root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
assert path.stat().st_size > 0 # Prove the matcher instrument receives readable input.
|
||||
return root, name
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_complete_route_structure_persists_and_applies_declared_relation(
|
||||
tmp_path: Path,
|
||||
language: str,
|
||||
suffix: str,
|
||||
) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/routes.{suffix}",
|
||||
'import express from "express";\n'
|
||||
"const app = express();\n"
|
||||
'app.get("/admin", adminHandler);\n'
|
||||
"function adminHandler(req, res) { res.sendStatus(204); }\n",
|
||||
)
|
||||
writer = _writer(tmp_path, context)
|
||||
|
||||
results = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=writer,
|
||||
evaluation_population_digest=evaluation,
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
result = results[0]
|
||||
assert result.decision.status is RelationStatus.COMPLETE
|
||||
assert tuple(observation.requirement_id for observation in result.match.observations) == (
|
||||
"qualified_target",
|
||||
"call_shape",
|
||||
"receiver",
|
||||
"arguments",
|
||||
)
|
||||
assert tuple((relation.kind, relation.role, relation.status) for relation in result.relations) == (
|
||||
(RelationKind.REGISTRATION, "entry_registration", RelationStatus.COMPLETE),
|
||||
)
|
||||
assert writer.resolver.resolve(result.match_reference).content
|
||||
assert all(writer.resolver.resolve(item.evidence).content for item in result.match.observations)
|
||||
|
||||
|
||||
def test_factory_binding_applies_only_from_verified_express_import(tmp_path: Path) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
"src/app.js",
|
||||
'import express from "express";\nconst app = express();\n',
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language="javascript",
|
||||
fact_id="entry:express:express-app",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.COMPLETE
|
||||
assert tuple(item.observed for item in result.match.observations) == (
|
||||
"express",
|
||||
"factory_call",
|
||||
"application_instance_binding",
|
||||
)
|
||||
|
||||
untrusted_root, untrusted_relative = _source(
|
||||
tmp_path,
|
||||
"untrusted/local.js",
|
||||
"function express() {}\nconst app = express();\n",
|
||||
)
|
||||
untrusted = match_entry_fact(
|
||||
repository_root=untrusted_root,
|
||||
relative_path=untrusted_relative,
|
||||
language="javascript",
|
||||
fact_id="entry:express:express-app",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path / "untrusted", context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
assert untrusted.decision.status is RelationStatus.INCOMPLETE
|
||||
assert "missing_observation:binding" in untrusted.decision.reasons
|
||||
assert "express_binding_not_authoritative:express" in untrusted.structural_reasons
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"source,missing_reason,structural_reason",
|
||||
(
|
||||
(
|
||||
'app.get("/admin", adminHandler);\nfunction adminHandler(req, res) {}\n',
|
||||
"missing_observation:receiver",
|
||||
"receiver_binding_missing:app",
|
||||
),
|
||||
(
|
||||
'import express from "express";\nconst app = express();\napp.get("/admin");\n',
|
||||
"missing_observation:arguments",
|
||||
"route_arguments_incomplete",
|
||||
),
|
||||
(
|
||||
'import express from "express";\nconst app = express();\napp.get("/admin", adminHandler);\n',
|
||||
"missing_observation:arguments",
|
||||
"handler_binding_missing:adminHandler",
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_name_only_and_partial_structure_cannot_complete(
|
||||
tmp_path: Path,
|
||||
source: str,
|
||||
missing_reason: str,
|
||||
structural_reason: str,
|
||||
) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(tmp_path, "src/routes.js", source)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language="javascript",
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.INCOMPLETE
|
||||
assert missing_reason in result.decision.reasons
|
||||
assert structural_reason in result.structural_reasons
|
||||
assert all(relation.status is not RelationStatus.COMPLETE for relation in result.relations)
|
||||
|
||||
|
||||
def test_ambiguous_receiver_bindings_are_unresolved(tmp_path: Path) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
"src/routes.js",
|
||||
'import express from "express";\n'
|
||||
"const app = express();\n"
|
||||
"const app = express.Router();\n"
|
||||
'app.get("/admin", adminHandler);\n'
|
||||
"function adminHandler(req, res) {}\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language="javascript",
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.UNRESOLVED
|
||||
assert "duplicate_observation:receiver" in result.decision.reasons
|
||||
assert "conflicting_observation:receiver" in result.decision.reasons
|
||||
assert "receiver_binding_ambiguous:app" in result.structural_reasons
|
||||
|
||||
|
||||
def test_substituted_match_reference_is_refused_by_core(tmp_path: Path) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
"src/routes.js",
|
||||
'import express from "express";\nconst app = express();\napp.get("/admin", adminHandler);\nfunction adminHandler(req, res) {}\n',
|
||||
)
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language="javascript",
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
substituted = replace(result.match_reference, storage_receipt_digest="f" * 64)
|
||||
with pytest.raises(FactApplicationError, match="match reference cannot be validated"):
|
||||
apply_fact(
|
||||
result.pack,
|
||||
result.match.fact_id,
|
||||
result.match,
|
||||
substituted,
|
||||
context.bundle.contract,
|
||||
evaluation,
|
||||
context.bundle.lineages,
|
||||
context.custody,
|
||||
context.bundle.custody_policy,
|
||||
context.bundle.review_policy,
|
||||
result.resolver,
|
||||
)
|
||||
|
||||
|
||||
def test_current_same_population_label_influence_cannot_complete(tmp_path: Path) -> None:
|
||||
context, _, custody = _authority()
|
||||
matcher_lineage = next(
|
||||
item for item in context.bundle.lineages if item.manifest_digest == context.bundle.matcher_lineage_manifest_digest
|
||||
)
|
||||
population = matcher_lineage.uses[0].source_population_digest
|
||||
current = custody.current_log(population)
|
||||
custody.append(
|
||||
provenance.PopulationUseRequest(
|
||||
population,
|
||||
matcher_lineage.arm_id,
|
||||
provenance.LineagePurpose.MATCHING_BEHAVIOR,
|
||||
True,
|
||||
context.bundle.matcher_behavior_digest,
|
||||
current.log_digest,
|
||||
)
|
||||
)
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
"src/routes.js",
|
||||
'import express from "express";\nconst app = express();\napp.get("/admin", adminHandler);\nfunction adminHandler(req, res) {}\n',
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language="javascript",
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=population,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.UNRESOLVED
|
||||
assert f"protected_population_labels:{population}" in result.decision.reasons
|
||||
assert all(relation.status is not RelationStatus.COMPLETE for relation in result.relations)
|
||||
|
||||
|
||||
def test_current_same_population_fact_label_influence_is_refused(tmp_path: Path) -> None:
|
||||
context, _, custody = _authority()
|
||||
pack = next(item for item in context.bundle.packs if item.language == "javascript")
|
||||
fact_lineage = next(item for item in context.bundle.lineages if item.subject_digest == pack.normalized_content_digest)
|
||||
population = fact_lineage.uses[0].source_population_digest
|
||||
current = custody.current_log(population)
|
||||
custody.append(
|
||||
provenance.PopulationUseRequest(
|
||||
population,
|
||||
fact_lineage.arm_id,
|
||||
provenance.LineagePurpose.SEMANTIC_FACT,
|
||||
True,
|
||||
pack.normalized_content_digest,
|
||||
current.log_digest,
|
||||
)
|
||||
)
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
"src/routes.js",
|
||||
'import express from "express";\nconst app = express();\napp.get("/admin", adminHandler);\nfunction adminHandler(req, res) {}\n',
|
||||
)
|
||||
|
||||
with pytest.raises(FactPackValidationError, match="semantic fact lineage is contaminated.*protected_population_labels"):
|
||||
match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language="javascript",
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=population,
|
||||
)
|
||||
|
||||
|
||||
def test_matcher_refuses_empty_unreadable_or_outside_source(tmp_path: Path) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root = tmp_path / "repository"
|
||||
root.mkdir()
|
||||
(root / "empty.js").touch()
|
||||
|
||||
for relative in ("empty.js", "missing.js", "../outside.js"):
|
||||
with pytest.raises(ValueError, match="repository source"):
|
||||
match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language="javascript",
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_function_local_require_does_not_authorize_top_level_local_express(
|
||||
tmp_path: Path,
|
||||
language: str,
|
||||
suffix: str,
|
||||
) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/local.{suffix}",
|
||||
"function unrelated() { const express = require('express'); return express; }\n"
|
||||
"function express() {}\n"
|
||||
"const app = express();\n"
|
||||
"function handler(req, res) {}\n"
|
||||
"app.get('/x', handler);\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.INCOMPLETE
|
||||
assert "express_binding_not_authoritative:express" in result.structural_reasons
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_receiver_reassignment_cannot_complete(tmp_path: Path, language: str, suffix: str) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/reassigned.{suffix}",
|
||||
"import express from 'express';\n"
|
||||
"let app = express();\n"
|
||||
"app = express.Router();\n"
|
||||
"function handler(req, res) {}\n"
|
||||
"app.get('/x', handler);\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.INCOMPLETE
|
||||
assert "receiver_binding_reassigned:app" in result.structural_reasons
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_handler_reassignment_cannot_complete(tmp_path: Path, language: str, suffix: str) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/handler-reassigned.{suffix}",
|
||||
"import express from 'express';\n"
|
||||
"const app = express();\n"
|
||||
"let handler = (req, res) => {};\n"
|
||||
"handler = otherHandler;\n"
|
||||
"app.get('/x', handler);\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.INCOMPLETE
|
||||
assert "handler_binding_reassigned:handler" in result.structural_reasons
|
||||
assert result.resolved_handlers == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_duplicate_handler_declarations_are_unresolved(tmp_path: Path, language: str, suffix: str) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/duplicate-handler.{suffix}",
|
||||
"import express from 'express';\n"
|
||||
"const app = express();\n"
|
||||
"function handler(req, res) {}\n"
|
||||
"function handler(req, res) {}\n"
|
||||
"app.get('/x', handler);\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.UNRESOLVED
|
||||
assert "handler_binding_ambiguous:handler" in result.structural_reasons
|
||||
assert len(result.resolved_handlers) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_valid_nested_registration_resolves_exact_handler_declaration(
|
||||
tmp_path: Path,
|
||||
language: str,
|
||||
suffix: str,
|
||||
) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/nested.{suffix}",
|
||||
"import express from 'express';\n"
|
||||
"function register() {\n"
|
||||
" const app = express();\n"
|
||||
" const nestedHandler = (req, res) => {};\n"
|
||||
" app.get('/x', nestedHandler);\n"
|
||||
"}\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.COMPLETE
|
||||
assert result.structural_reasons == ()
|
||||
assert len(result.resolved_handlers) == 1
|
||||
assert result.resolved_handlers[0].symbol == "nestedHandler"
|
||||
assert result.resolved_handlers[0].identity == f"nestedHandler@src/nested.{suffix}:4:8"
|
||||
assert result.resolved_handlers[0].location.path == f"src/nested.{suffix}"
|
||||
assert result.resolved_handlers[0].location.line == 4
|
||||
assert result.match.product_location.symbol == f"app.get->nestedHandler@src/nested.{suffix}:4:8"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_mixed_receiver_declarations_suppress_authoritative_observation(
|
||||
tmp_path: Path,
|
||||
language: str,
|
||||
suffix: str,
|
||||
) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/mixed-receiver.{suffix}",
|
||||
"import express from 'express';\nvar app = express();\nvar app = fake;\nfunction handler(req, res) {}\napp.get('/x', handler);\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.INCOMPLETE
|
||||
assert "receiver_binding_ambiguous:app" in result.structural_reasons
|
||||
assert "receiver_factory_not_authoritative:app" in result.structural_reasons
|
||||
assert not any(item.requirement_id == "receiver" for item in result.match.observations)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_mixed_handler_declarations_suppress_argument_observation(
|
||||
tmp_path: Path,
|
||||
language: str,
|
||||
suffix: str,
|
||||
) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/mixed-handler.{suffix}",
|
||||
"import express from 'express';\n"
|
||||
"const app = express();\n"
|
||||
"function handler(req, res) {}\n"
|
||||
"var handler = externalHandler;\n"
|
||||
"app.get('/x', handler);\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.INCOMPLETE
|
||||
assert "handler_binding_ambiguous:handler" in result.structural_reasons
|
||||
assert "handler_binding_not_repository_defined:handler" in result.structural_reasons
|
||||
assert result.resolved_handlers == ()
|
||||
assert not any(item.requirement_id == "arguments" for item in result.match.observations)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_inner_lexical_bindings_cleanly_shadow_outer_bindings(tmp_path: Path, language: str, suffix: str) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/shadowed.{suffix}",
|
||||
"import express from 'express';\n"
|
||||
"const app = express();\n"
|
||||
"function handler(req, res) {}\n"
|
||||
"function register() {\n"
|
||||
" const express = require('express');\n"
|
||||
" const app = express();\n"
|
||||
" const handler = (req, res) => {};\n"
|
||||
" app.get('/x', handler);\n"
|
||||
"}\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.COMPLETE
|
||||
assert result.structural_reasons == ()
|
||||
assert result.resolved_handlers[0].identity == f"handler@src/shadowed.{suffix}:7:8"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_post_registration_reassignment_does_not_contaminate_match(
|
||||
tmp_path: Path,
|
||||
language: str,
|
||||
suffix: str,
|
||||
) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/post-write.{suffix}",
|
||||
"import express from 'express';\n"
|
||||
"let app = express();\n"
|
||||
"let handler = (req, res) => {};\n"
|
||||
"app.get('/x', handler);\n"
|
||||
"app = express.Router();\n"
|
||||
"handler = externalHandler;\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.COMPLETE
|
||||
assert result.structural_reasons == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,suffix", (("javascript", "js"), ("typescript", "ts")))
|
||||
def test_post_registration_declaration_initializers_do_not_contaminate_match(
|
||||
tmp_path: Path,
|
||||
language: str,
|
||||
suffix: str,
|
||||
) -> None:
|
||||
context, evaluation, _ = _authority()
|
||||
root, relative = _source(
|
||||
tmp_path,
|
||||
f"src/post-initializer.{suffix}",
|
||||
"import express from 'express';\n"
|
||||
"var app = express();\n"
|
||||
"function handler(req, res) {}\n"
|
||||
"app.get('/x', handler);\n"
|
||||
"var app = fake;\n"
|
||||
"var handler = externalHandler;\n",
|
||||
)
|
||||
|
||||
result = match_entry_fact(
|
||||
repository_root=root,
|
||||
relative_path=relative,
|
||||
language=language,
|
||||
fact_id="entry:express:app-route:get",
|
||||
authority=context,
|
||||
writer=_writer(tmp_path, context),
|
||||
evaluation_population_digest=evaluation,
|
||||
)[0]
|
||||
|
||||
assert result.decision.status is RelationStatus.COMPLETE
|
||||
assert result.structural_reasons == ()
|
||||
Reference in New Issue
Block a user