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

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

25 

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

29 

30from __future__ import annotations 

31 

32from collections.abc import Callable, Iterator 

33 

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 

42 

43__all__ = [ 

44 "TRUST_FAMILIES", 

45 "Trust", 

46 "trust_diff", 

47 "trust_file_label", 

48 "trust_label", 

49] 

50 

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) 

72 

73 

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 

81 

82 

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

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

85 

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