| 1 | #!/usr/bin/env python3 |
| 2 | """Ratchet on `#[allow(dead_code)]` so the wall can shrink but never regrow (#4785). |
| 3 | |
| 4 | Why this exists rather than a one-time sweep: |
| 5 | |
| 6 | issue filed 2026-07-?? 464 attributes / 143 files |
| 7 | audit 2026-07-26 426 / 111 |
| 8 | audit 2026-07-28 481 / 155 |
| 9 | |
| 10 | The sweep was working and the count still went *up*, because two large landings |
| 11 | added state whose accessors only their own tests read. A sweep is a snapshot; a |
| 12 | budget is a direction. This gate makes the number a one-way door. |
| 13 | |
| 14 | It deliberately does NOT judge whether any individual attribute is justified — |
| 15 | plenty are. It only refuses to let the total rise, which is the property the |
| 16 | issue actually needs and the only one that can be checked mechanically. |
| 17 | |
| 18 | Note the blind spot this compensates for: CI's clippy runs without |
| 19 | `--all-targets`, so it never lints `cfg(test)` or integration-test code. A prior |
| 20 | strip-and-check measured 197 attributes alive *only* because a test references |
| 21 | them — exactly the ones a test-blind lint can never adjudicate. |
| 22 | |
| 23 | Usage: |
| 24 | python3 scripts/check-dead-code-budget.py # enforce |
| 25 | python3 scripts/check-dead-code-budget.py --update # rewrite the budget file |
| 26 | """ |
| 27 | |
| 28 | from __future__ import annotations |
| 29 | |
| 30 | import argparse |
| 31 | import json |
| 32 | import re |
| 33 | import sys |
| 34 | from pathlib import Path |
| 35 | |
| 36 | REPO_ROOT = Path(__file__).resolve().parent.parent |
| 37 | CRATES_DIR = REPO_ROOT / "crates" |
| 38 | BUDGET_PATH = REPO_ROOT / "scripts" / "dead-code-budget.json" |
| 39 | |
| 40 | # Matches `#[allow(dead_code)]`, `#![allow(dead_code)]`, and combined forms like |
| 41 | # `#[allow(dead_code, clippy::large_enum_variant)]`. |
| 42 | ALLOW_PATTERN = re.compile(r"allow\(\s*dead_code\b") |
| 43 | # `#[expect(dead_code)]` suppresses exactly the same lint. Counting only the |
| 44 | # `allow` spelling made the gate blind: rewriting an `allow` as an `expect` |
| 45 | # lowered the headline number by the full count while removing no dead code |
| 46 | # at all (#6241). `137fb70a9` did this 92 times and the budget recorded a |
| 47 | # 115-point "improvement" for a real reduction of 21. |
| 48 | EXPECT_PATTERN = re.compile(r"expect\(\s*dead_code\b") |
| 49 | |
| 50 | |
| 51 | def measure() -> tuple[int, int, dict[str, dict[str, int]]]: |
| 52 | """Return (allow total, expect total, per-crate {allow, expect} counts). |
| 53 | |
| 54 | Both spellings suppress the same lint, so the gate ratchets on their sum. |
| 55 | They are still reported separately because `expect` is the better |
| 56 | attribute — it errors when the lint stops firing, so it cannot rot |
| 57 | silently — and a sweep converting `allow` into `expect` is real progress |
| 58 | even though it leaves the total unchanged. |
| 59 | """ |
| 60 | per_crate: dict[str, dict[str, int]] = {} |
| 61 | allow_total = 0 |
| 62 | expect_total = 0 |
| 63 | for path in sorted(CRATES_DIR.rglob("*.rs")): |
| 64 | try: |
| 65 | text = path.read_text(encoding="utf-8") |
| 66 | except (OSError, UnicodeDecodeError): |
| 67 | continue |
| 68 | allows = len(ALLOW_PATTERN.findall(text)) |
| 69 | expects = len(EXPECT_PATTERN.findall(text)) |
| 70 | if not allows and not expects: |
| 71 | continue |
| 72 | crate = path.relative_to(CRATES_DIR).parts[0] |
| 73 | entry = per_crate.setdefault(crate, {"allow": 0, "expect": 0}) |
| 74 | entry["allow"] += allows |
| 75 | entry["expect"] += expects |
| 76 | allow_total += allows |
| 77 | expect_total += expects |
| 78 | return allow_total, expect_total, per_crate |
| 79 | |
| 80 | |
| 81 | def load_budget() -> dict: |
| 82 | if not BUDGET_PATH.exists(): |
| 83 | sys.exit(f"missing budget file: {BUDGET_PATH.relative_to(REPO_ROOT)}") |
| 84 | return json.loads(BUDGET_PATH.read_text(encoding="utf-8")) |
| 85 | |
| 86 | |
| 87 | def write_budget( |
| 88 | allow_total: int, expect_total: int, per_crate: dict[str, dict[str, int]] |
| 89 | ) -> None: |
| 90 | payload = { |
| 91 | "_comment": ( |
| 92 | "Ceiling for dead-code suppressions across crates/, counting both " |
| 93 | "`#[allow(dead_code)]` and `#[expect(dead_code)]`. `total` is the " |
| 94 | "sum and is the ratcheted figure. It may go down freely; raising " |
| 95 | "it needs a reviewer to say why in the PR. Regenerate with: " |
| 96 | "python3 scripts/check-dead-code-budget.py --update" |
| 97 | ), |
| 98 | "_issue": "https://github.com/codewhale-hq/CodeWhale/issues/4785", |
| 99 | "_expect_blind_spot": ( |
| 100 | "Until #6241 this gate counted only the `allow` spelling, so " |
| 101 | "rewriting an allow as an expect lowered the number without " |
| 102 | "removing any dead code. The ceiling was re-based to the true " |
| 103 | "combined count when that was fixed; it is not a regression." |
| 104 | ), |
| 105 | "total": allow_total + expect_total, |
| 106 | "allow_total": allow_total, |
| 107 | "expect_total": expect_total, |
| 108 | "per_crate": dict(sorted(per_crate.items())), |
| 109 | } |
| 110 | BUDGET_PATH.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") |
| 111 | |
| 112 | |
| 113 | def main() -> int: |
| 114 | parser = argparse.ArgumentParser(description=__doc__) |
| 115 | parser.add_argument( |
| 116 | "--update", |
| 117 | action="store_true", |
| 118 | help="rewrite the budget file from the working tree", |
| 119 | ) |
| 120 | args = parser.parse_args() |
| 121 | |
| 122 | allow_total, expect_total, per_crate = measure() |
| 123 | total = allow_total + expect_total |
| 124 | |
| 125 | if args.update: |
| 126 | write_budget(allow_total, expect_total, per_crate) |
| 127 | rel = BUDGET_PATH.relative_to(REPO_ROOT) |
| 128 | print( |
| 129 | f"[dead-code-budget] wrote {rel}: total={total} " |
| 130 | f"(allow={allow_total}, expect={expect_total})" |
| 131 | ) |
| 132 | return 0 |
| 133 | |
| 134 | budget = load_budget() |
| 135 | ceiling = int(budget["total"]) |
| 136 | |
| 137 | if total > ceiling: |
| 138 | print( |
| 139 | f"[dead-code-budget] FAIL: {total} dead-code suppressions " |
| 140 | f"(allow={allow_total}, expect={expect_total}), " |
| 141 | f"budget is {ceiling} (+{total - ceiling}).", |
| 142 | file=sys.stderr, |
| 143 | ) |
| 144 | print("", file=sys.stderr) |
| 145 | print("Per crate now vs. budget:", file=sys.stderr) |
| 146 | recorded = budget.get("per_crate", {}) |
| 147 | for crate in sorted(set(per_crate) | set(recorded)): |
| 148 | entry = per_crate.get(crate, {"allow": 0, "expect": 0}) |
| 149 | now = entry["allow"] + entry["expect"] |
| 150 | was_entry = recorded.get(crate, 0) |
| 151 | # Tolerate the pre-#6241 shape, where each crate mapped to a bare |
| 152 | # `allow` count rather than an {allow, expect} pair. |
| 153 | if isinstance(was_entry, dict): |
| 154 | was = int(was_entry.get("allow", 0)) + int(was_entry.get("expect", 0)) |
| 155 | else: |
| 156 | was = int(was_entry) |
| 157 | marker = " <-- grew" if now > was else "" |
| 158 | print( |
| 159 | f" {crate:<16} {now:>4} (allow {entry['allow']}, " |
| 160 | f"expect {entry['expect']}; budget {was}){marker}", |
| 161 | file=sys.stderr, |
| 162 | ) |
| 163 | print("", file=sys.stderr) |
| 164 | print( |
| 165 | "Either delete the dead item, or narrow the attribute to the one item\n" |
| 166 | "that needs it instead of a whole module. If the growth is genuinely\n" |
| 167 | "justified, run `python3 scripts/check-dead-code-budget.py --update`\n" |
| 168 | "and say why in the PR description — the point of this gate is that\n" |
| 169 | "raising the number is a visible decision, not an accident.", |
| 170 | file=sys.stderr, |
| 171 | ) |
| 172 | return 1 |
| 173 | |
| 174 | if total < ceiling: |
| 175 | print( |
| 176 | f"[dead-code-budget] {total} suppressions " |
| 177 | f"(allow={allow_total}, expect={expect_total}), budget {ceiling} " |
| 178 | f"({ceiling - total} under). Lower the budget to lock in the win:\n" |
| 179 | f" python3 scripts/check-dead-code-budget.py --update" |
| 180 | ) |
| 181 | return 0 |
| 182 | |
| 183 | print( |
| 184 | f"[dead-code-budget] PASS: {total} suppressions " |
| 185 | f"(allow={allow_total}, expect={expect_total}), exactly at budget." |
| 186 | ) |
| 187 | return 0 |
| 188 | |
| 189 | |
| 190 | if __name__ == "__main__": |
| 191 | sys.exit(main()) |
| 192 |