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

21 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-07-28 14:54 -0500

1"""Small diff-model accessors and language-syntax tables shared across the 

2trust rules. The comment-syntax tables live here (not in comments.py) so the 

3tokenizer can derive from the same source without an import cycle — the 

4comments rule and the token-delta rules must never disagree about what starts 

5a comment.""" 

6 

7from __future__ import annotations 

8 

9from ..diff import DiffFile, DiffHunk 

10 

11# Languages grouped by their line-comment prefix. NOTE: CSS is deliberately NOT 

12# here — it has no `//` line comment (only `/* */`), and a `//` in a CSS value 

13# (e.g. a protocol-relative `url(//cdn/…)`) would otherwise read as a comment and 

14# hide a real value change. CSS lives in the block-comment table below only. 

15_SLASH_LANGS = "js jsx ts tsx mjs mts cjs cts rs go java kt kts scala swift c cc cpp cxx h hpp cs m mm zig v dart groovy gradle" 

16# NOTE: YAML/TOML are intentionally absent. A `#` there can be a comment, part of a 

17# value (`url: …/#frag`), OR literal text inside a `|`/`>` block scalar — too 

18# ambiguous to classify safely, so these data/config formats are declined for 

19# content rules (the whitespace guard in strip_inline_comment only covers the 

20# value-fragment case, not block scalars). 

21_HASH_LANGS = "py rb sh bash zsh fish pl pm r jl ex exs cr nim coffee mk cmake tf hcl" 

22_DASH_LANGS = "lua hs sql" 

23_SEMI_LANGS = "lisp clj cljs cljc edn scm rkt" 

24_PERCENT_LANGS = "erl hrl" 

25 

26# File extension -> the line-comment prefixes that language uses. 

27LINE_COMMENT_PREFIXES: dict[str, tuple[str, ...]] = { 

28 **dict.fromkeys(_SLASH_LANGS.split(), ("//",)), 

29 **dict.fromkeys(_HASH_LANGS.split(), ("#",)), 

30 **dict.fromkeys(_DASH_LANGS.split(), ("--",)), 

31 **dict.fromkeys(_SEMI_LANGS.split(), (";",)), 

32 **dict.fromkeys(_PERCENT_LANGS.split(), ("%",)), 

33} 

34 

35# File extension -> (open, close) block-comment delimiters. `css` is block-only. 

36BLOCK_COMMENT_DELIMITERS: dict[str, tuple[str, str]] = { 

37 **dict.fromkeys(_SLASH_LANGS.split(), ("/*", "*/")), 

38 "css": ("/*", "*/"), 

39 **dict.fromkeys("html xml svg".split(), ("<!--", "-->")), 

40} 

41 

42 

43def extension(file: DiffFile) -> str: 

44 """The file's extension, without the dot. Plain string ops — every rule calls 

45 this per hunk, and it's just the text after the last `.` in the basename.""" 

46 name = file.new_path.rsplit("/", 1)[-1] 

47 _, dot, ext = name.rpartition(".") 

48 return ext if dot and _ else "" 

49 

50 

51def leading_indent(text: str) -> str: 

52 """The leading-whitespace prefix of `text`. Indentation is semantic in Python 

53 (a dedent moves a statement to another block), so rules that compare 

54 whitespace-normalized lines must keep it, or an indent-only change would read 

55 as trivial and be hidden.""" 

56 return text[: len(text) - len(text.lstrip())] 

57 

58 

59def change_suffix(hunk: DiffHunk) -> str: 

60 """`added`, `removed`, or `modified` based on which sides changed.""" 

61 has_added = bool(hunk.added_lines) 

62 has_removed = bool(hunk.removed_lines) 

63 if has_added and has_removed: 

64 return "modified" 

65 return "added" if has_added else "removed"