"""Detect gate_policy exemption entries broken by deleted or renamed paths."""
from __future__ import annotations
from typing import TYPE_CHECKING
from scripts.helpers.ci_gate.models import ChangeSet, GateError
if TYPE_CHECKING:
from scripts.helpers.ci_gate.diff import DiffEntry
from scripts.helpers.ci_gate.models import CiGatePolicy
def iter_rename_pairs(entries: tuple[DiffEntry, ...]) -> tuple[tuple[str, str], ...]:
"""Return ``(old_path, new_path)`` for each rename entry in a git diff."""
pairs: list[tuple[str, str]] = []
for entry in entries:
if not entry.status.startswith("R"):
continue
if entry.old_path is None or entry.new_path is None:
continue
pairs.append((entry.old_path, entry.new_path))
return tuple(pairs)
def gate_exemption_drift(
policy: CiGatePolicy,
changes: ChangeSet,
rename_pairs: tuple[tuple[str, str], ...],
) -> tuple[GateError, ...]:
"""Return blocking errors when exemptions reference deleted or renamed paths."""
errors: list[GateError] = []
deleted_sources = set(changes.del_source)
deleted_tests = set(changes.del_test)
rename_by_old = dict(rename_pairs)
for entry in policy.source_exemptions:
if entry.file in rename_by_old:
new_path = rename_by_old[entry.file]
errors.append(
GateError(
category="exemption_drift",
path=entry.file,
symbol=entry.symbol,
detail=(
f"exemption {entry.symbol_key} references renamed source; update to {new_path}::{entry.symbol}"
),
)
)
elif entry.file in deleted_sources:
errors.append(
GateError(
category="exemption_drift",
path=entry.file,
symbol=entry.symbol,
detail=f"exemption {entry.symbol_key} references deleted source file",
)
)
for entry in policy.test_exemptions:
test_file = entry.test_id.split("::", 1)[0]
if test_file in rename_by_old:
new_file = rename_by_old[test_file]
new_test_id = f"{new_file}{entry.test_id[len(test_file) :]}"
errors.append(
GateError(
category="exemption_drift",
path=test_file,
detail=(f"exemption {entry.test_id!r} references renamed test file; update to {new_test_id!r}"),
)
)
elif test_file in deleted_tests:
errors.append(
GateError(
category="exemption_drift",
path=test_file,
detail=f"exemption {entry.test_id!r} references deleted test file",
)
)
return tuple(errors)