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
« 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."""
7from __future__ import annotations
9from ..diff import DiffFile, DiffHunk
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"
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}
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}
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 ""
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())]
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"