Coverage for src/pullapprove/trust/__init__.py: 100%
24 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-28 14:54 -0500
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-28 14:54 -0500
1"""Static, conservative classification of diff hunks by *trust*.
3A hunk is "trusted" when its change is mechanically trivial — lockfile churn,
4whitespace, comments, imports, type annotations — so it can be filtered out
5during review. `trust_diff` tags every hunk with a `Trust` label (e.g.
6`Trust.LOCKFILE`) or `None`. `Trust` is the closed vocabulary of labels; see
7`labels.py`.
9Every rule is **default-deny**: it recognizes one specific trivial shape and
10returns nothing for everything else, so an unrecognized change is shown, never
11hidden — a wrong label would hide a real change. The rules recognize their shape
12through a few mechanisms: filename (`_lockfile`), all-blank lines (`_empty_file`,
13`_whitespace`), a same-content newline reflow (`_line_length`), a same-statement
14import set (`_imports`), a comment-only scan (`_comments`), a token-identical
15respacing (`_spacing`), and a token-level delta where every changed token is
16trivial (`_style`, `_type_annotations`) — those last three share `delta.py`'s
17paired-line gates. Classification is a separate pass from parsing — it never
18runs on the `match_diff` hot path.
20Each rule lives in its own module beside the syntax tables it needs and is a
21pure `(file, hunk) -> Trust | None`; this package wires them into `_RULES`. Add a
22rule by writing one and appending it here. One rule classifies whole FILES
23instead of hunks: `trust_file_label` (renames.py) labels a hunkless pure rename
24from its header lines — hunkless files never reach the per-hunk rules at all.
26Ported from the `review` project's static classifier (full parity except
27`move:code`, which needs cross-file hunk pairing we don't model).
28"""
30from __future__ import annotations
32from collections.abc import Callable, Iterator
34from ..diff import DiffFile, DiffHunk, parse_diff
35from .annotations import _type_annotations
36from .comments import _comments
37from .formatting import _empty_file, _line_length, _spacing, _style, _whitespace
38from .imports import _imports
39from .labels import TRUST_FAMILIES, Trust
40from .lockfiles import _lockfile
41from .renames import trust_file_label
43__all__ = [
44 "TRUST_FAMILIES",
45 "Trust",
46 "trust_diff",
47 "trust_file_label",
48 "trust_label",
49]
51# Order matters: cheapest / most-specific first, first match wins. Default-deny
52# keeps the rules from fighting — each declines anything outside its shape — but
53# ordering still resolves the few honest overlaps: `_whitespace` precedes
54# `_line_length`/`_style` (a blank-only change is not a wrap or a quote swap), and
55# `_style`/`_spacing` precede `_comments` (a line whose code differs only in a
56# quote and whose comment also changed reads as style; a comment-only change
57# token-matches under both, declines, and falls through here). `_style` and
58# `_spacing` are disjoint — style needs token texts to differ, spacing needs them
59# equal — so their relative order is free. Revisit when adding a rule whose shape
60# can co-occur with another's.
61_RULES: tuple[Callable[[DiffFile, DiffHunk], Trust | None], ...] = (
62 _lockfile,
63 _empty_file,
64 _whitespace,
65 _line_length,
66 _style,
67 _spacing,
68 _comments,
69 _type_annotations,
70 _imports,
71)
74def trust_label(file: DiffFile, hunk: DiffHunk) -> Trust | None:
75 """Return the conservative trust label for a hunk, or None when nothing matches."""
76 for rule in _RULES:
77 label = rule(file, hunk)
78 if label:
79 return label
80 return None
83def trust_diff(diff: Iterator[str] | str) -> list[DiffFile]:
84 """Parse a diff and tag every hunk with its trust label.
86 Holds the whole parsed `DiffFile` tree — right for a hunk listing (the
87 CLI's `trust` command). When only the per-hunk labels are needed, compose
88 `iter_file_hunks` with `trust_label` instead: it classifies each hunk the
89 moment the diff finishes streaming it, holding one hunk at a time. Every
90 rule classifies a hunk from its own lines and the file path alone (never
91 sibling hunks), so the per-hunk result is identical either way.
92 """
93 files = parse_diff(diff)
94 for file in files:
95 for hunk in file.hunks:
96 hunk.trust = trust_label(file, hunk)
97 return files