Second-round review on #132646 (F1-F10), each reproduced on 2531fb2 first: - Moves are matched one-to-one: unchanged units are reserved first, then same-file body, then cross-file body only when the origin's name is gone, then same name. A copy of unchanged debt is new debt; a function moved over a same-name stub keeps its own cap (F1, F3). - Every base hit is owned by exactly one head scope, so a file rename no longer counts each old hit twice (F2). - Ruff runs when either side has Python, so a delete-only .py diff with a TS edit no longer crashes on a missing ruff result (F4). - Deadlines must be finite: timeout=None, **{} and asyncio.timeout(None) are no deadline; awaited calls inside `async with asyncio.timeout(x)` are bounded (F5, F8). - HX005 walks only what runs at import: the import-time branch of a `__name__` guard (either operator), loops, match, try*, decorators, class bases; a lambda value is deferred (F9). - `health: allow` counts only in real comment tokens (Python tokenize, TS comment trivia), never in string literals (F7). - TS function lines are owned by the innermost function, so siblings on one line give it up once (F10). - The pre-commit hook runs `scripts/check --staged`: health compares the index tree, and when the working tree differs from the index the other checkers run on a checkout-index copy with a copied index, so commits are judged on the staged bytes and unstaged work is never touched (F6).
66 lines
1.9 KiB
Python
66 lines
1.9 KiB
Python
"""Data shapes shared by the measurers and the comparison."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
from dataclasses import dataclass, field
|
|
|
|
MODULE_SCOPE = "<module>"
|
|
|
|
|
|
@dataclass
|
|
class Unit:
|
|
"""One function (Python def, TS function/method/arrow) and its metrics."""
|
|
|
|
qualname: str
|
|
line: int
|
|
metrics: dict[str, int]
|
|
body_hash: str
|
|
parent: str | None = None # nearest enclosing function (nested defs are their own units)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Hit:
|
|
"""A pattern-rule fingerprint: never a line number, so edits above don't move it."""
|
|
|
|
rule: str
|
|
scope: str
|
|
text: str
|
|
|
|
|
|
@dataclass
|
|
class FileMeasure:
|
|
path: str
|
|
metrics: dict[str, int] = field(default_factory=dict)
|
|
units: dict[str, Unit] = field(default_factory=dict)
|
|
hits: Counter[Hit] = field(default_factory=Counter)
|
|
# Line numbers of each hit occurrence, for reporting only.
|
|
hit_lines: dict[Hit, list[int]] = field(default_factory=dict)
|
|
# 1-based line -> source text, for messages.
|
|
lines: list[str] = field(default_factory=list)
|
|
# 1-based line -> text of the real comment tokens on it (allow directives live only here,
|
|
# so a string literal that happens to say "health: allow" waives nothing).
|
|
comments: dict[int, str] = field(default_factory=dict)
|
|
|
|
def add_hit(self, rule: str, scope: str, line_no: int) -> None:
|
|
text = " ".join(self.source_line(line_no).split())
|
|
hit = Hit(rule, scope, text)
|
|
self.hits[hit] += 1
|
|
self.hit_lines.setdefault(hit, []).append(line_no)
|
|
|
|
def source_line(self, line_no: int) -> str:
|
|
if 1 <= line_no <= len(self.lines):
|
|
return self.lines[line_no - 1]
|
|
return ""
|
|
|
|
|
|
@dataclass
|
|
class Finding:
|
|
path: str
|
|
rule: str
|
|
scope: str
|
|
line: int
|
|
detail: str
|
|
blocking: bool = True
|
|
allowed_reason: str | None = None
|