Coverage for src/pullapprove/trust/__init__.py: 100%
23 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-01 18:26 -0500
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-01 18:26 -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.
24Ported from the `review` project's static classifier (full parity except
25`move:code`, which needs cross-file hunk pairing we don't model).
26"""
28from __future__ import annotations
30from collections.abc import Callable, Iterator
32from ..diff import DiffFile, DiffHunk, parse_diff
33from .annotations import _type_annotations
34from .comments import _comments
35from .formatting import _empty_file, _line_length, _spacing, _style, _whitespace
36from .imports import _imports
37from .labels import TRUST_FAMILIES, Trust
38from .lockfiles import _lockfile
40__all__ = ["TRUST_FAMILIES", "Trust", "trust_diff", "trust_label"]
42# Order matters: cheapest / most-specific first, first match wins. Default-deny
43# keeps the rules from fighting — each declines anything outside its shape — but
44# ordering still resolves the few honest overlaps: `_whitespace` precedes
45# `_line_length`/`_style` (a blank-only change is not a wrap or a quote swap), and
46# `_style`/`_spacing` precede `_comments` (a line whose code differs only in a
47# quote and whose comment also changed reads as style; a comment-only change
48# token-matches under both, declines, and falls through here). `_style` and
49# `_spacing` are disjoint — style needs token texts to differ, spacing needs them
50# equal — so their relative order is free. Revisit when adding a rule whose shape
51# can co-occur with another's.
52_RULES: tuple[Callable[[DiffFile, DiffHunk], Trust | None], ...] = (
53 _lockfile,
54 _empty_file,
55 _whitespace,
56 _line_length,
57 _style,
58 _spacing,
59 _comments,
60 _type_annotations,
61 _imports,
62)
65def trust_label(file: DiffFile, hunk: DiffHunk) -> Trust | None:
66 """Return the conservative trust label for a hunk, or None when nothing matches."""
67 for rule in _RULES:
68 label = rule(file, hunk)
69 if label:
70 return label
71 return None
74def trust_diff(diff: Iterator[str] | str) -> list[DiffFile]:
75 """Parse a diff and tag every hunk with its trust label.
77 Holds the whole parsed `DiffFile` tree — right for a hunk listing (the
78 CLI's `trust` command). When only the per-hunk labels are needed, compose
79 `iter_file_hunks` with `trust_label` instead: it classifies each hunk the
80 moment the diff finishes streaming it, holding one hunk at a time. Every
81 rule classifies a hunk from its own lines and the file path alone (never
82 sibling hunks), so the per-hunk result is identical either way.
83 """
84 files = parse_diff(diff)
85 for file in files:
86 for hunk in file.hunks:
87 hunk.trust = trust_label(file, hunk)
88 return files