litellm/scripts/ruff_strict_gate.py
mateo-berri 861ab17898 ci(lint): gate every rule on its merge-base count and cap Anys at a fixed total
Builds on the merge-base gate design from litellm_mat_140_base_count_gates and
brings it up to date with main. Every rule in the four lint gates now has zero
headroom: a branch may not grow any rule past its merge-base count. reportAny
and reportExplicitAny instead get a fixed codebase-wide cap in ANY_CAPS, so a
branch can absorb Any contagion while the total stays under the cap, and the
cap only moves when someone lowers it on main
2026-10-07 20:59:33 -07:00

168 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Delta-vs-base gate for the strict ruff rules in ruff-strict.toml.
Each rule is counted across the whole tree at HEAD and at the merge-base with
the branch this change merges into, and the gate fails only when a rule grew
past the merge-base count, so a change is blamed for the violations it adds,
never for drift that already sits in the base. There is no committed budget: the merge-base count is the
ceiling, so it moves only when the base branch does.
The merge-base counts come from scripts/lint_base_counts.py: the disk cache,
then the CI artifact published for that commit, then a ruff pass over a
detached worktree at the merge-base under the current ruff configs.
``--emit-counts-dir`` writes HEAD's counts as the file that artifact is built
from.
"""
import argparse
import json
import re
import shutil
import subprocess
import sys
import tempfile
from collections import Counter
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Final, NamedTuple
from lint_base_counts import (
Checker,
base_counts_cached,
emit_counts,
evaluate,
head_sha,
resolve_base_point,
sha256_of,
)
REPO_ROOT: Final = Path(__file__).resolve().parent.parent
STRICT_CONFIG: Final = REPO_ROOT / "ruff-strict.toml"
BASE_CONFIG: Final = REPO_ROOT / "ruff.toml"
TARGET: Final = "litellm"
_HUNK: Final = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
class Violation(NamedTuple):
file: str
line: int
code: str
def _run(cmd: Sequence[str], cwd: Path = REPO_ROOT) -> str:
proc: Final = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
if proc.returncode not in (0, 1):
sys.stderr.write(proc.stderr)
raise SystemExit(f"{cmd[0]} exited {proc.returncode}")
return proc.stdout
def ruff_version() -> str:
return _run(["ruff", "--version"]).strip()
def checker_identity() -> Checker:
return Checker("ruff-strict", (sha256_of(STRICT_CONFIG), sha256_of(BASE_CONFIG), ruff_version()))
def _ruff_json(cwd: Path, config: Path) -> list:
raw = _run(
["ruff", "check", TARGET, "--config", str(config), "--output-format", "json"],
cwd=cwd,
)
return json.loads(raw or "[]")
def head_violations() -> list[Violation]:
out = []
for item in _ruff_json(REPO_ROOT, STRICT_CONFIG):
name = Path(item["filename"])
rel = (
(name if name.is_absolute() else REPO_ROOT / name)
.resolve()
.relative_to(REPO_ROOT)
.as_posix()
)
out.append(Violation(rel, item["location"]["row"], item["code"]))
return out
def count_by_rule(violations: Sequence[Violation]) -> dict[str, int]:
return dict(Counter(v.code for v in violations))
def base_counts(ref: str) -> dict[str, int]:
parent: Final = Path(tempfile.mkdtemp(prefix="ruff_base_"))
worktree: Final = parent / "wt"
try:
_run(["git", "worktree", "add", "--detach", str(worktree), ref])
shutil.copy(BASE_CONFIG, worktree / BASE_CONFIG.name)
shutil.copy(STRICT_CONFIG, worktree / STRICT_CONFIG.name)
items: Final = _ruff_json(worktree, worktree / STRICT_CONFIG.name)
return dict(Counter(item["code"] for item in items))
finally:
_run(["git", "worktree", "remove", "--force", str(worktree)])
shutil.rmtree(parent, ignore_errors=True)
def parse_changed_lines(diff_text: str) -> dict[str, set[int]]:
changed: dict[str, set[int]] = {}
path = None
for line in diff_text.splitlines():
if line.startswith("+++ b/"):
path = line[6:]
elif path and (match := _HUNK.match(line)):
start = int(match.group(1))
count = int(match.group(2)) if match.group(2) is not None else 1
changed.setdefault(path, set()).update(range(start, start + count))
return changed
def introduced(violations: Sequence[Violation], changed: Mapping[str, set[int]]) -> list[Violation]:
return [v for v in violations if v.line in changed.get(v.file, set())]
def cmd_check(base: str) -> None:
head: Final = head_violations()
base_point: Final = resolve_base_point(base)
breaches: Final = evaluate(count_by_rule(head), base_counts_cached(checker_identity(), base_point, base_counts))
if not breaches:
print(f"OK: no strict rule grew past its merge-base count (base {base})")
return
diff: Final = _run(["git", "diff", base_point, "--unified=0", "--no-color", "--", TARGET])
new: Final = introduced(head, parse_changed_lines(diff))
print(f"FAIL: strict-rule totals grew past their merge-base count (base {base}):")
for breach in breaches:
print(f" {breach.rule}: total {breach.total} over ceiling {breach.ceiling} (this change added {breach.added})")
for violation in sorted(v for v in new if v.code == breach.rule):
print(f" {violation.file}:{violation.line}")
print(
"Reduce the new violations or remove an equal number elsewhere; the ceiling is the merge-base count."
)
raise SystemExit(1)
def main() -> None:
parser: Final = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", help="Comparison ref (default: origin's current default branch)")
parser.add_argument(
"--emit-counts-dir",
type=Path,
help="Write HEAD's per-rule counts to this directory as a base-counts artifact instead of gating",
)
args: Final = parser.parse_args()
from default_branch import resolve_base_ref
from gate_slot_lock import held_slot
if args.emit_counts_dir is not None:
with held_slot():
emit_counts(checker_identity(), count_by_rule(head_violations()), args.emit_counts_dir, head_sha())
return
base_ref: Final = resolve_base_ref(args.base, REPO_ROOT)
with held_slot():
cmd_check(base_ref)
if __name__ == "__main__":
main()