Coverage for src/pullapprove/trust/__init__.py: 100%

23 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-07-17 11:30 -0500

1"""Static, conservative classification of diff hunks by *trust*. 

2 

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`. 

8 

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. 

19 

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. 

23 

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""" 

27 

28from __future__ import annotations 

29 

30from collections.abc import Callable, Iterator 

31 

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 

39 

40__all__ = ["TRUST_FAMILIES", "Trust", "trust_diff", "trust_label"] 

41 

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) 

63 

64 

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 

72 

73 

74def trust_diff(diff: Iterator[str] | str) -> list[DiffFile]: 

75 """Parse a diff and tag every hunk with its trust label. 

76 

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