ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_catalog.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Declarative syntax catalog for repository suppression directives."""
4
5from __future__ import annotations
6
7import re
8from pathlib import Path
9
10from lint_coverage_rules import EXEMPT_PREFIXES, EXT_CLASS, PATH_CLASS
11
12VENDOR_PREFIXES = tuple(
13 prefix for prefix, reason in EXEMPT_PREFIXES if "vendored" in reason or "SOUP" in reason
14)
15GENERATED_PREFIXES = tuple(
16 prefix for prefix, reason in EXEMPT_PREFIXES if "generated" in reason or "emitted" in reason
17)
18GENERATED_CLASSES = frozenset({"generated-source"})
19BINARY_SUFFIXES = frozenset(suffix for suffix, cls in EXT_CLASS.items() if cls == "binary")
20REQUIRED_FAMILIES = frozenset(
21 {
22 "ansible",
23 "baseline-ratchet",
24 "encoding-exemption",
25 "clang-tidy",
26 "compiler",
27 "checker-nonfatal-control",
28 "checker-scope-control",
29 "ci-parity-exemption",
30 "coverage",
31 "coverage-mask",
32 "cppcheck",
33 "ctest",
34 "formatter",
35 "generated-artifact",
36 "documentation-control",
37 "lint-control",
38 "mcdc-deactivation",
39 "project-policy",
40 "python",
41 "shellcheck",
42 "shell-status",
43 "ansible-lint-config",
44 "security-analysis-control",
45 "other-language-control",
46 "test-control",
47 "workflow",
48 }
49)
50THREADX_FIRST_PARTY_FILES = frozenset(
51 {
52 "port/threadx/inc/ra8_threadx.h",
53 "port/threadx/inc/tx_user.h",
54 "port/threadx/src/cortex_m85/tx_systick_ready.c",
55 "port/threadx/src/cortex_m85/tx_systick_retune.c",
56 }
57)
58
59KNOWN_PROJECT_MARKERS = frozenset(
60 {
61 "AI-OK",
62 "ATTR-OK",
63 "C23HDR-OK",
64 "CITES-OK",
65 "FILE-SIZE-OK",
66 "LEGACY-OK",
67 "MAGIC-OK",
68 "PATHREF-OK",
69 "TZ-DISCARD-OK",
70 "WAVE-OK",
71 }
72)
73
74KNOWN_CPPCHECK_RULES = frozenset(
75 {
76 "comparePointers",
77 "constParameterCallback",
78 "constParameterPointer",
79 "constVariablePointer",
80 "duplicateExpression",
81 "knownConditionTrueFalse",
82 "missingInclude",
83 "redundantAssignment",
84 "unassignedVariable",
85 "unknownMacro",
86 "unreadVariable",
87 "unusedFunction",
88 "unusedScopedObject",
89 "unusedStructMember",
90 "unusedVariable",
91 "variableScope",
92 *{
93 f"misra-c2012-{rule}"
94 for rule in (
95 "7.4",
96 "8.4",
97 "8.9",
98 "9.2",
99 "9.5",
100 "11.1",
101 "11.2",
102 "11.3",
103 "11.5",
104 "11.6",
105 "11.8",
106 "12.1",
107 "15.5",
108 "17.3",
109 "17.7",
110 "18.4",
111 "18.8",
112 "21.3",
113 "21.6",
114 "21.16",
115 "22.10",
116 )
117 },
118 }
119)
120
121C_FAMILY_SUFFIXES = frozenset(
122 {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inc", ".m", ".mm"}
123)
124PYTHON_SUFFIXES = frozenset({".py", ".pyi"})
125HASH_COMMENT_SUFFIXES = frozenset(
126 {".bash", ".cmake", ".ini", ".just", ".mk", ".sh", ".toml", ".yaml", ".yml", ".zsh"}
127)
128HASH_COMMENT_NAMES = frozenset(
129 {"CMakeLists.txt", "Dockerfile", "GNUmakefile", "Justfile", "Makefile", "justfile"}
130)
131BUILD_CONTROL_SUFFIXES = frozenset(
132 {".bash", ".cmake", ".just", ".mk", ".sh", ".yaml", ".yml", ".zsh"}
133)
134BUILD_CONTROL_NAMES = HASH_COMMENT_NAMES
135
136NOLINT_RE = re.compile(
137 r"^NOLINT(?P<scope>NEXTLINE|BEGIN|END)?(?:\‍((?P<rules>[^)]*)\‍))?"
138 r"(?P<tail>.*)$"
139)
140NOLINT_RAW_RE = re.compile(
141 r"NOLINT(?P<scope>NEXTLINE|BEGIN|END)?"
142 r"(?:\‍((?P<rules>[^)\r\n]*)\‍)|\‍[(?P<bracket_rules>[^]\r\n]*)\‍])?"
143 r"(?![A-Za-z])"
144)
145NOQA_RE = re.compile(
146 r"^noqa(?:\s*:\s*(?P<rules>[A-Za-z0-9_.-]+(?:\s*,\s*[A-Za-z0-9_.-]+)*))?"
147 r"(?P<tail>\s*(?:(?:--|#|-\s).*)?|\s*\‍(.*\‍))$",
148 re.IGNORECASE,
149)
150TYPE_IGNORE_RE = re.compile(
151 r"^type\s*:\s*ignore(?:\‍[(?P<rules>[^]]*)\‍])?"
152 r"(?P<tail>\s*(?:--.*|#.*)?)$",
153 re.IGNORECASE,
154)
155PYTHON_COVERAGE_RE = re.compile(
156 r"^pragma\s*:\s*no\s+cover(?P<tail>\s*(?:--.*)?)$",
157 re.IGNORECASE,
158)
159PYLINT_RE = re.compile(
160 r"^pylint\s*:\s*(?P<control>disable|enable|skip-file)"
161 r"(?:\s*=\s*(?P<rules>[A-Za-z0-9_.-]+(?:\s*,\s*[A-Za-z0-9_.-]+)*))?"
162 r"(?P<tail>\s*(?:--.*)?)$",
163 re.IGNORECASE,
164)
165MYPY_RE = re.compile(
166 r"^mypy\s*:\s*(?P<control>ignore-errors|disable-error-code|enable-error-code)"
167 r"(?:\s*=\s*[\"']?\‍[?(?P<rules>[A-Za-z0-9_.-]+(?:\s*,\s*[A-Za-z0-9_.-]+)*)"
168 r"\‍]?[\"']?)?(?P<tail>\s*(?:--.*)?)$",
169 re.IGNORECASE,
170)
171PYRIGHT_RE = re.compile(
172 r"^pyright\s*:\s*(?:(?P<ignore>ignore)(?:\‍[(?P<rules>[A-Za-z0-9_.-]+"
173 r"(?:\s*,\s*[A-Za-z0-9_.-]+)*)\‍])?|(?P<setting>report[A-Za-z0-9_]+)"
174 r"\s*=\s*(?P<value>true|false|none|information|warning|error))"
175 r"(?P<tail>\s*(?:--.*)?)$",
176 re.IGNORECASE,
177)
178BANDIT_RE = re.compile(
179 r"^(?:(?P<nosec>nosec)(?:\s+(?P<nosec_rules>[A-Za-z0-9_.-]+"
180 r"(?:\s*,\s*[A-Za-z0-9_.-]+)*))?|bandit\s*:\s*skip\s*=\s*"
181 r"(?P<bandit_rules>[A-Za-z0-9_.-]+(?:\s*,\s*[A-Za-z0-9_.-]+)*))"
182 r"(?P<tail>\s*(?:--.*)?)$",
183 re.IGNORECASE,
184)
185PYTHON_FORMATTER_RE = re.compile(
186 r"^(?:(?P<ruff>ruff\s*:\s*noqa)(?:\s*:\s*(?P<ruff_rules>[A-Za-z0-9_.-]+"
187 r"(?:\s*,\s*[A-Za-z0-9_.-]+)*))?|(?P<fmt>fmt)\s*:\s*(?P<fmt_control>off|on|skip)|"
188 r"(?P<isort>isort)\s*:\s*(?P<isort_control>off|on|skip|skip_file))"
189 r"(?P<tail>\s*(?:--.*)?)$",
190 re.IGNORECASE,
191)
192SHELLCHECK_RE = re.compile(
193 r"^shellcheck\s+(?P<control>disable|enable|source|source-path|shell|external-sources)"
194 r"\s*=\s*(?P<value>\S+(?:\s*,\s*\S+)*)"
195 r"(?P<tail>\s*(?:#.*)?)$",
196 re.IGNORECASE,
197)
198SHELL_STATUS_MASK_RE = re.compile(r"\|\|\s*(?:true\b|:(?![A-Za-z0-9_]))")
199SHELLCHECK_GLOBAL_EXCLUDE_RE = re.compile(
200 r"(?:^|\s)(?:-e|--exclude)(?:=|\s+)(?P<rules>SC\d+(?:\s*,\s*SC\d+)*)"
201)
202COVERAGE_RE = re.compile(
203 r"^(?P<marker>(?:GCOVR|LCOV)_EXCL(?:_BR)?_(?:LINE|START|STOP))"
204 r"(?P<tail>\s*(?:(?:--|:).*)?)$"
205)
206CPPCHECK_RE = re.compile(
207 r"^cppcheck-suppress(?P<scope>-(?:begin|end|file))?\s+"
208 r"\‍[?(?P<rule>[A-Za-z0-9_.-]+)\‍]?(?P<tail>\s*(?:(?:--|;).*)?)$"
209)
210ALLOC_ALLOW_RE = re.compile(
211 r"\balloc-allow\s*:\s*(?P<reason>\S(?:.*\S)?)\s*$",
212 re.IGNORECASE,
213)
214PROJECT_MARKER_RE = re.compile(
215 r"^(?P<marker>[A-Z][A-Z0-9_-]*-OK)"
216 r"(?:(?:\s*:\s*|\s+--\s*)(?P<reason>.*))?$"
217)
218UNKNOWN_DIRECTIVE_RE = re.compile(
219 r"^(?:NOLINT[A-Z]*(?:\‍(|\s*(?:--|$))|noqa(?:\s*:|\s*--|\s*$)|"
220 r"type\s*:\s*ignore\b|pragma\s*:\s*no\s+cover\b|"
221 r"pylint\s*:|mypy\s*:|pyright\s*:|nosec\b|bandit\s*:|"
222 r"ruff\s*:\s*noqa|fmt\s*:|isort\s*:|shellcheck\s+\S+|"
223 r"(?:GCOVR|LCOV)_EXCL[A-Z_]*(?:\s*--|\s*$)|cppcheck-suppress\S*|"
224 r"clang-format\s+(?:off|on)\b|yamllint\s+(?:disable|enable)|"
225 r"hadolint\s+(?:global\s+)?ignore\s*=|cmake-(?:lint|format)\s*:|"
226 r"shfmt\s*:|prettier-ignore\b|markdownlint-[A-Za-z-]+|"
227 r"(?:@|\\)(?:cond|endcond)\b|IWYU\s+pragma\s*:|"
228 r"[A-Z][A-Z0-9_-]*-OK(?:\s*:|\s+--|\s*$))"
229)
230WARNING_FLAG_RE = re.compile(r"(?<![A-Za-z0-9_])(?P<flag>-Wno-[A-Za-z0-9_.=+-]+)(?![A-Za-z0-9_])")
231BLANKET_WARNING_RE = re.compile(r"(?<![A-Za-z0-9_])(?P<flag>-w)(?![A-Za-z0-9_])")
232SUPPORTED_HINT_RE = re.compile(
233 r"(?:NOLINT|noqa|type\s*:\s*ignore|pragma\s*:\s*no\s+cover|"
234 r"pylint\s*:|mypy\s*:|pyright\s*:|nosec\b|bandit\s*:|"
235 r"ruff\s*:\s*noqa|fmt\s*:|isort\s*:|shellcheck|(?:GCOVR|LCOV)_EXCL|"
236 r"cppcheck-suppress|clang-format|(?:@|\\)(?:cond|endcond)\b|"
237 r"IWYU\s+pragma\s*:|[A-Z][A-Z0-9_-]*-OK|-Wno-|"
238 r"(?<![A-Za-z0-9_])-w(?![A-Za-z0-9_]))",
239 re.IGNORECASE,
240)
241
242# Every formerly unsupported governance family now has a typed parser. Keep
243# this compatibility tuple empty so old callers cannot silently revive a raw
244# substring census as an authority.
245UNSUPPORTED_CATEGORIES: tuple[tuple[str, re.Pattern[str]], ...] = ()
246
247
248def ownership(path: str) -> str:
249 """Classify ownership from the repository's canonical path evidence."""
250 if PATH_CLASS.get(path) in GENERATED_CLASSES:
251 return "generated"
252 if path in THREADX_FIRST_PARTY_FILES:
253 return "first-party"
254 if path.startswith(VENDOR_PREFIXES):
255 return "vendor"
256 if path.startswith("port/threadx/") and Path(path).suffix.lower() in C_FAMILY_SUFFIXES:
257 return "vendor"
258 if path.startswith(GENERATED_PREFIXES):
259 return "generated"
260 return "first-party"
261
262
263def language(path: str, first_line: str = "") -> str:
264 """Classify comment syntax from path and an optional shebang."""
265 item = Path(path)
266 if item.suffix.lower() in C_FAMILY_SUFFIXES:
267 return "c-family"
268 if item.suffix.lower() in PYTHON_SUFFIXES:
269 return "python"
270 if (
271 item.name in HASH_COMMENT_NAMES
272 or item.name.startswith("Dockerfile.")
273 or item.suffix.lower() in HASH_COMMENT_SUFFIXES
274 ):
275 return "hash"
276 if first_line.startswith("#!") and any(word in first_line for word in ("sh", "bash", "zsh")):
277 return "hash"
278 return "text"
279
280
281def is_build_control(path: str, first_line: str = "") -> bool:
282 """Return whether compiler warning flags in this file can be active."""
283 item = Path(path)
284 if item.name in BUILD_CONTROL_NAMES or item.suffix.lower() in BUILD_CONTROL_SUFFIXES:
285 return True
286 return first_line.startswith("#!") and any(word in first_line for word in ("sh", "bash", "zsh"))
287
288
289def is_shell_control(path: str, first_line: str = "") -> bool:
290 """Return whether hash comments follow shell word-boundary rules."""
291 item = Path(path)
292 suffix = item.suffix.lower()
293 if suffix in {".bash", ".sh", ".zsh"}:
294 return True
295 if suffix == ".just" or item.name == "justfile":
296 return True
297 return first_line.startswith("#!") and any(word in first_line for word in ("sh", "bash", "zsh"))