ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_clang_tidy.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Source-located clang-tidy global exclusion inventory."""
4
5from __future__ import annotations
6
7import re
8
9import yaml
10from suppression_catalog import ownership
11from suppression_model import Finding, Suppression
12
13CLANG_TIDY_REASON_RE = re.compile(r"^\s*#\s+-(?P<rule>[A-Za-z0-9.*_-]+),?(?:\s+.*)?$")
14CLANG_TIDY_RULE_RE = re.compile(r"[A-Za-z0-9.*_-]+")
15
16
17def _concerns(rule: str, reason: str) -> tuple[str, ...]:
18 """Return machine-observable review concerns for one global exclusion."""
19 concerns: list[str] = []
20 if rule == "*":
21 concerns.append("broad-rule")
22 if not reason:
23 concerns.append("blank-reason")
24 return tuple(concerns)
25
26
27def _reasons(text: str) -> dict[str, str]:
28 """Associate documented disable headings with their following comments."""
29 result: dict[str, str] = {}
30 active: list[str] = []
31 notes: list[str] = []
32 for raw in text.splitlines():
33 if raw.startswith("Checks:"):
34 break
35 heading = CLANG_TIDY_REASON_RE.fullmatch(raw)
36 if heading is not None:
37 if active and notes:
38 reason = " ".join(notes).strip()
39 result.update(dict.fromkeys(active, reason))
40 active = []
41 notes = []
42 active.append(heading.group("rule"))
43 continue
44 if not active:
45 continue
46 stripped = raw.strip()
47 if not stripped.startswith("#"):
48 continue
49 note = stripped[1:].strip()
50 if note:
51 notes.append(note)
52 if active:
53 reason = " ".join(notes).strip()
54 result.update(dict.fromkeys(active, reason))
55 return result
56
57
58def scan_clang_tidy_config(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
59 """Inventory every negative clang-tidy Checks glob from active YAML."""
60 if path != ".clang-tidy":
61 return [], []
62 try:
63 parsed = yaml.safe_load(text)
64 except yaml.YAMLError as exc:
65 return [], [Finding("malformed-clang-tidy-config", str(exc), path)]
66 if not isinstance(parsed, dict) or not isinstance(parsed.get("Checks"), str):
67 message = "top-level Checks must be a YAML string"
68 return [], [Finding("malformed-clang-tidy-config", message, path)]
69 entries = [entry.strip() for entry in parsed["Checks"].split(",") if entry.strip()]
70 negative = [entry[1:].strip() for entry in entries if entry.startswith("-")]
71 findings: list[Finding] = []
72 if any(not rule or CLANG_TIDY_RULE_RE.fullmatch(rule) is None for rule in negative):
73 findings.append(
74 Finding("malformed-clang-tidy-config", "invalid negative Checks glob", path)
75 )
76 if len(negative) != len(set(negative)):
77 findings.append(
78 Finding("malformed-clang-tidy-config", "duplicate negative Checks glob", path)
79 )
80 reasons = _reasons(text)
81 checks_offset = text.find("Checks:")
82 records: list[Suppression] = []
83 for rule in negative:
84 offset = text.find("-" + rule, checks_offset)
85 if offset < 0:
86 findings.append(
87 Finding(
88 "malformed-clang-tidy-config",
89 f"cannot source-locate negative Checks glob {rule}",
90 path,
91 )
92 )
93 continue
94 line = text.count("\n", 0, offset) + 1
95 line_start = text.rfind("\n", 0, offset)
96 column = offset - line_start
97 normalized = "*" if rule == "*" else rule
98 reason = reasons.get(rule, "")
99 records.append(
100 Suppression(
101 path,
102 line,
103 column,
104 "clang-tidy",
105 "clang-tidy",
106 normalized,
107 "Checks exclude",
108 "repository",
109 reason,
110 "central-config",
111 ownership(path),
112 _concerns(normalized, reason),
113 )
114 )
115 return records, findings