Files
rune/scripts/lint_ratchet.py

81 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""Silent-handler ratchet (#126): fail if S110/BLE001 counts exceed the baseline.
S110 = try-except-pass (the truly silent handlers), BLE001 = blind
`except Exception`. The baseline (lint-baseline.json) is committed and can
only shrink: new silent handlers fail CI, fixes lower the ledger. BLE001's
built-in exemptions (re-raise, log with `.exception()`/`exc_info=`) are the
compliant-handler spec; intentional silence carries `# noqa: BLE001 — <reason>`.
Same philosophy as the pyright ratchet (scripts/type_check_ratchet.py, #148).
"""
import json
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BASELINE_FILE = Path(os.environ.get("LINT_BASELINE", ROOT / "lint-baseline.json"))
SCOPE = ["src/", "shared/", "scripts/"]
RULES = ("BLE001", "S110")
def _ruff_bin() -> str:
"""Prefer the venv sibling of this interpreter, then PATH."""
sibling = Path(sys.executable).parent / "ruff"
if sibling.is_file():
return str(sibling)
return "ruff"
def current_counts() -> dict[str, int]:
result = subprocess.run(
[_ruff_bin(), "check", "--select", ",".join(RULES), "--output-format", "json", *SCOPE],
capture_output=True,
text=True,
cwd=str(ROOT),
timeout=300,
)
if result.returncode not in (0, 1): # 1 = violations found (expected)
sys.stderr.write(result.stderr)
raise RuntimeError(f"ruff failed with exit {result.returncode}")
counts = {rule: 0 for rule in RULES}
for v in json.loads(result.stdout): # ruff emits one JSON array
code = v.get("code")
code = code.get("name") if isinstance(code, dict) else code
if code in counts:
counts[code] += 1
return counts
def main() -> int:
baseline = json.loads(BASELINE_FILE.read_text())
missing = [rule for rule in RULES if rule not in baseline]
if missing:
print(f"::error::lint-baseline.json is missing rules {missing} — ratchet cannot run")
return 1
counts = current_counts()
print(
"silent-handler ratchet: " + ", ".join(f"{rule} {counts[rule]} (baseline {baseline[rule]})" for rule in RULES)
)
failed = False
for rule in RULES:
if counts[rule] > baseline[rule]:
print(
f"::error::Silent-handler count increased for {rule}: "
f"{baseline[rule]} -> {counts[rule]} (max {baseline[rule]}). "
"Fix the handler (log with exc_info or re-raise) or add "
"`# noqa: BLE001 — <reason>` only when silence is intentional."
)
failed = True
if failed:
return 1
print("Silent-handler ratchet: PASS")
return 0
if __name__ == "__main__":
sys.exit(main())