ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
lint_coverage_rules.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Classification tables and provider descriptors for ``check_lint_coverage.py``.
5
6This module is DATA plus pure helpers: it says what kinds of file exist in this
7repository, which of them are code, who is supposed to lint and format each
8kind, and which paths are deliberately outside every checker's reach. The
9enforcement logic that consumes it lives in ``check_lint_coverage.py``.
10
11The split is deliberate. The tables below are the part a human edits when a new
12file type lands; keeping them away from the subprocess plumbing means that edit
13is reviewable on its own, and means neither file drifts toward being the
142000-line checker nobody reads.
15
16A NOTE ON WHAT "FORMATTER" MEANS HERE
17-------------------------------------
18Two different things enforce layout in this tree, and both count:
19
20 * rewriters -- clang-format, gofmt, ruff format, shfmt, cmake-format,
21 just --fmt. Given a file they emit the canonical form, and the format
22 gate runs them in --check mode.
23 * canonical-form checkers -- yamllint's style rules,
24 check_linker_scripts.py. Nothing rewrites a GNU ld script in this
25 ecosystem, so these enforce the layout rules by rejecting deviations
26 instead of by producing the fixed text.
27
28The distinction that matters for coverage is "does something reject this file
29for being laid out wrongly", not "can something rewrite it for me". A class
30served only by a canonical-form checker is covered; a class served by nothing
31is a gap, and gaps are enumerated in KNOWN_GAPS rather than quietly dropped.
32"""
33
34from __future__ import annotations
35
36from collections.abc import Callable
37from dataclasses import dataclass
38
39# ---------------------------------------------------------------------------
40# Roles
41# ---------------------------------------------------------------------------
42LINT = "lint"
43FORMAT = "format"
44
45# ---------------------------------------------------------------------------
46# File kinds
47#
48# CODE -- hand-authored instructions to a machine. Must be linted AND
49# formatted. This is the class the gate exists to protect.
50# DATA -- committed bytes that are read, not executed or compiled: fixtures,
51# baselines, assets, generated tables. No linter applies.
52# DOC -- prose for humans. The ascii/attribution/terminology gates already
53# sweep every tracked file including these; there is no prose
54# formatter in this tree and inventing one is not this gate's job.
55# CONF -- tool configuration consumed by a specific tool, which validates it
56# on use. A malformed .clang-format fails the format gate itself.
57# ---------------------------------------------------------------------------
58CODE = "code"
59DATA = "data"
60DOC = "doc"
61CONF = "conf"
62
63
64@dataclass(frozen=True)
65class ClassSpec:
66 """One file class: what kind it is and what coverage it therefore needs."""
67
68 name: str
69 kind: str
70 why: str
71
72 @property
73 def needs_checking(self) -> bool:
74 """True when files of this class must have both a linter and formatter."""
75 return self.kind == CODE
76
77
78def _spec(name: str, kind: str, why: str) -> ClassSpec:
79 return ClassSpec(name, kind, why)
80
81
82# Every class the repository contains. Adding a file type means adding a row
83# here AND (if it is CODE) wiring a provider below -- the gate fails on an
84# unclassified extension precisely so that decision cannot be skipped.
85CLASSES: dict[str, ClassSpec] = {
86 "c-family": _spec("c-family", CODE, "firmware, host tools and tests"),
87 "asm": _spec("asm", CODE, "hand-written startup and low-level entry code"),
88 "dockerfile": _spec("dockerfile", CODE, "the devcontainer that pins every CI tool version"),
89 "zsh": _spec("zsh", CODE, "zsh dialect; shellcheck refuses zsh, so not `shell`"),
90 "python": _spec("python", CODE, "the gate suite and host tooling"),
91 "golang": _spec("golang", CODE, "host CLI and conversion policy"),
92 "shell": _spec("shell", CODE, "gate drivers, HIL scripts, git hooks"),
93 "cmake": _spec("cmake", CODE, "decides what compiles with which flags"),
94 "make": _spec("make", CODE, "per-app and top-level build entry points"),
95 "just": _spec("just", CODE, "just task runner recipes"),
96 "linker-script": _spec("linker-script", CODE, "the memory map is code"),
97 "yaml": _spec("yaml", CODE, "workflows decide which gates run at all"),
98 "markdown": _spec("markdown", DOC, "prose; swept by ascii/terminology gates"),
99 "restructuredtext": _spec("restructuredtext", DOC, "prose, vendored doc trees"),
100 "html": _spec("html", DATA, "rendered fixtures and doxygen fragments"),
101 "css": _spec("css", DATA, "e-reader stylesheet assets shipped as content"),
102 "javascript": _spec("javascript", DATA, "doxygen theme assets, not authored logic"),
103 "json": _spec("json", CONF, "manifests and SBOM; consumers validate on load"),
104 "toml": _spec("toml", CONF, "pyproject and friends; ruff validates its own"),
105 "ini": _spec("ini", CONF, "tool configuration"),
106 "xml": _spec("xml", DATA, "EPUB/OPF package descriptors and IDE leftovers"),
107 "csv": _spec("csv", DATA, "measurement tables"),
108 "text": _spec("text", DATA, "baselines, pin lists, licence text"),
109 "binary": _spec("binary", DATA, "images, fonts, archives, PDFs, book fixtures"),
110 "tool-config": _spec("tool-config", CONF, "dotfile config for a named tool"),
111 "vcs-metadata": _spec("vcs-metadata", CONF, "gitignore/gitattributes and kin"),
112 "fixture": _spec("fixture", DATA, "test corpora and golden inputs"),
113 "validated-input": _spec(
114 "validated-input",
115 CONF,
116 "exact machine-readable inputs parsed by their pinned build or generator",
117 ),
118 "generated-source": _spec(
119 "generated-source",
120 DATA,
121 "exact reproducible source outputs whose generator owns the canonical bytes",
122 ),
123 "ansible-systemd-template": _spec(
124 "ansible-systemd-template",
125 CODE,
126 "privileged Jinja/systemd input checked against the fleet role contract",
127 ),
128}
129
130# ---------------------------------------------------------------------------
131# Exact path -> class, checked BEFORE basename and extension tables.
132#
133# These exact files deliberately do not create a blanket exemption for every
134# future .patch, `series`, .proto, or *pb-c.c file. The coprocessor patch is
135# checked by its pinned build; the SOUP patch series are replayed byte-exact by
136# check_third_party_patches.py; the schema is parsed by the pinned protobuf-c
137# generator; and the two codec files are that generator's reproducible outputs.
138# A second file of any of these types remains unclassified/C-family and makes
139# lint-coverage fail until it receives an equally specific validation story.
140# ---------------------------------------------------------------------------
141PATH_CLASS: dict[str, str] = {
142 "infra/ansible/roles/dev_box/templates/ra8-hil-runner.service.j2": "ansible-systemd-template",
143 "infra/ansible/roles/dev_box/templates/ra8-hil-privileged-policy.json.j2": "validated-input",
144 "scripts/hil/lib/ra8-hil-privileged.sha256": "validated-input",
145 "scripts/checks/patches/cppcheck-2.13/misra_9-c23-empty-initializer.patch": "validated-input",
146 "coprocessor/esp32c6/patches/0001-custom-rpc-sync-response-hook.patch": "validated-input",
147 "coprocessor/esp32c6/patches/series": "validated-input",
148 "docs/sbom/patches/levelx/0001-remove-nested-attribute-macros.patch": "validated-input",
149 "docs/sbom/patches/levelx/series": "validated-input",
150 "docs/sbom/patches/libwebp/0001-use-ra8-arena-allocator.patch": "validated-input",
151 "docs/sbom/patches/libwebp/series": "validated-input",
152 "docs/sbom/patches/miniz/0001-use-ra8-assertion-policy.patch": "validated-input",
153 "docs/sbom/patches/miniz/series": "validated-input",
154 "docs/sbom/patches/mbedtls/0001-track-generated-config-headers.patch": "validated-input",
155 "docs/sbom/patches/mbedtls/series": "validated-input",
156 "docs/sbom/patches/netxduo/0001-remove-nested-attribute-macros.patch": "validated-input",
157 "docs/sbom/patches/netxduo/series": "validated-input",
158 "docs/sbom/patches/protobuf-c/0001-use-ra8-runtime-policy.patch": "validated-input",
159 "docs/sbom/patches/protobuf-c/series": "validated-input",
160 "docs/sbom/patches/stb/0001-harden-font-parser-bounds.patch": "validated-input",
161 "docs/sbom/patches/stb/series": "validated-input",
162 "docs/sbom/patches/threadx/0001-remove-nested-attribute-macros.patch": "validated-input",
163 "docs/sbom/patches/threadx/series": "validated-input",
164 "docs/sbom/patches/usbx/0001-remove-nested-attribute-macros.patch": "validated-input",
165 "docs/sbom/patches/usbx/series": "validated-input",
166 "libs/ra8_c6link/proto/ra8_media_download.proto": "validated-input",
167 "libs/ra8_c6link/inc/ra8_media_download.pb-c.h": "generated-source",
168 "libs/ra8_c6link/src/ra8_media_download.pb-c.c": "generated-source",
169}
170
171# ---------------------------------------------------------------------------
172# Extension -> class. Lower-cased suffix, including the dot.
173# ---------------------------------------------------------------------------
174EXT_CLASS: dict[str, str] = {
175 # C family
176 ".c": "c-family",
177 ".h": "c-family",
178 ".cpp": "c-family",
179 ".hpp": "c-family",
180 ".cc": "c-family",
181 ".cxx": "c-family",
182 ".hh": "c-family",
183 ".hxx": "c-family",
184 ".inc": "c-family",
185 ".m": "c-family",
186 # Assembly. GNU as accepts both; .S is preprocessed, .s is not.
187 ".s": "asm",
188 # Scripting
189 ".py": "python",
190 ".go": "golang",
191 ".sh": "shell",
192 ".bash": "shell",
193 # NOT "shell": shellcheck explicitly refuses zsh input, so calling a .zsh
194 # file shell would claim coverage that does not exist.
195 ".zsh": "zsh",
196 # Build systems
197 ".cmake": "cmake",
198 ".mk": "make",
199 ".make": "make",
200 ".just": "just",
201 ".ld": "linker-script",
202 # Structured config
203 ".yml": "yaml",
204 ".yaml": "yaml",
205 ".json": "json",
206 ".toml": "toml",
207 ".ini": "ini",
208 ".cfg": "ini",
209 ".properties": "ini",
210 ".xml": "xml",
211 ".csv": "csv",
212 ".tsv": "csv",
213 ".opf": "xml",
214 ".user": "tool-config",
215 # Documents and assets
216 ".md": "markdown",
217 ".rst": "restructuredtext",
218 ".html": "html",
219 ".xhtml": "html",
220 ".css": "css",
221 ".js": "javascript",
222 ".txt": "text",
223 ".lock": "text",
224 ".conf": "text",
225 ".dox": "markdown",
226 ".in": "text",
227 ".example": "text",
228 ".args": "text",
229 ".defs": "text",
230 ".env": "text",
231 ".defaults": "text",
232 # docs/sbom/upstream/*.manifest -- generated evidence, one line per vendored
233 # file. Not code and not hand-authored, but not unvalidated either:
234 # check_soup_upstream.py parses every record strictly and fails on a
235 # malformed one, so this class is claimed by that gate rather than a linter.
236 ".manifest": "text",
237 # Binary payloads
238 ".png": "binary",
239 ".jpg": "binary",
240 ".jpeg": "binary",
241 ".webp": "binary",
242 ".svg": "binary",
243 ".gif": "binary",
244 ".pdf": "binary",
245 ".ttf": "binary",
246 ".otf": "binary",
247 ".woff": "binary",
248 ".woff2": "binary",
249 ".epub": "binary",
250 ".cbz": "binary",
251 ".gz": "binary",
252 ".xz": "binary",
253 ".zip": "binary",
254 ".a": "binary",
255 ".bin": "binary",
256 ".elf": "binary",
257 ".hex": "binary",
258 ".iex": "binary",
259 ".yaff": "binary",
260 ".mesh": "binary",
261 ".tflite": "binary",
262}
263
264# ---------------------------------------------------------------------------
265# Exact filename -> class, checked BEFORE the extension table. This is how
266# extensionless build files and dotfile configs get classified.
267# ---------------------------------------------------------------------------
268NAME_CLASS: dict[str, str] = {
269 "CMakeLists.txt": "cmake",
270 "Dockerfile": "dockerfile",
271 "zshrc": "zsh",
272 "justfile": "just",
273 "Justfile": "just",
274 "Doxyfile": "tool-config",
275 "VERSION": "text",
276 "LICENSE": "text",
277 "NOTICE": "text",
278 "mimetype": "fixture",
279 ".clang-format": "tool-config",
280 ".clang-tidy": "tool-config",
281 ".clangd": "tool-config",
282 ".editorconfig": "tool-config",
283 ".dockerignore": "tool-config",
284 ".shellcheckrc": "tool-config",
285 ".pylintrc": "tool-config",
286 ".globalrc": "tool-config",
287 ".cursorrules": "markdown",
288 ".cppcheck-suppressions": "tool-config",
289 # Go module manifests: consumed and validated by the Go toolchain on use.
290 "go.mod": "tool-config",
291 "go.sum": "tool-config",
292 # cppcheck-only C23 nullptr shim, force-included by the cppcheck gate; it is
293 # never compiled into any TU, so clang-tidy cannot claim it as c-family and
294 # it is classified for what it is -- configuration for a named tool.
295 "cppcheck_c23_compat.h": "tool-config",
296 ".style_ignored_dirs": "tool-config",
297 ".rat-excludes": "tool-config",
298 ".gitignore": "vcs-metadata",
299 ".gitattributes": "vcs-metadata",
300 ".gitmodules": "vcs-metadata",
301 ".gitkeep": "vcs-metadata",
302 ".mailmap": "vcs-metadata",
303}
304
305# ---------------------------------------------------------------------------
306# Shebang interpreter -> class. Consulted for files the tables above miss,
307# which is how `scripts/git/pre-commit` (extensionless, #!/usr/bin/env bash)
308# is recognised as shell rather than falling through as unclassified.
309# ---------------------------------------------------------------------------
310SHEBANG_CLASS: tuple[tuple[str, str], ...] = (
311 ("python", "python"),
312 ("bash", "shell"),
313 ("zsh", "shell"),
314 ("/sh", "shell"),
315 ("env sh", "shell"),
316)
317
318# ---------------------------------------------------------------------------
319# EXEMPTIONS -- paths no first-party checker is expected to reach.
320#
321# Every entry is a path prefix with a one-line reason. A prefix without a
322# reason is not accepted by the loader below: an exemption list that grows
323# without justification is exactly how the coverage question became
324# unanswerable in the first place.
325# ---------------------------------------------------------------------------
326EXEMPT_PREFIXES: tuple[tuple[str, str], ...] = (
327 ("libs/third_party/", "vendored platform SOUP; CLAUDE.md exempts it"),
328 ("apps/shared_libs/third_party/", "vendored app SOUP; CLAUDE.md exempts it"),
329 ("libs/ra8_fonts/", "generated glyph tables, not hand-authored"),
330 ("tools/vela/generated/", "emitted by the Vela NPU compiler on every regen"),
331 ("docs/reference/", "committed Renesas datasheet and HUM PDFs"),
332 ("docs/doxygen_theme/", "vendored doxygen-awesome theme"),
333 ("docs/build/", "generated Doxygen HTML output"),
334 ("content/", "EPUB/CBZ book fixtures used as reader test content"),
335 (
336 "apps/board/stand_alone/ereader/content/",
337 "EPUB/CBZ book fixtures used as reader test content",
338 ),
339 # NOT tests/fixtures/ as a whole. A blanket exemption there hid
340 # apps/shared_libs/epub/tests/src/epub_probe.c and tests/fixtures/epub/run_probe.sh -- real
341 # first-party
342 # code that clang-format and shellcheck were in fact already covering. An
343 # exemption that conceals covered code makes the matrix understate reality,
344 # which is the same class of wrongness as one that conceals uncovered code.
345 # Everything else under fixtures/ classifies as data on its own merits.
346 ("tests/fuzz/corpus/", "libFuzzer corpora, machine-generated random inputs"),
347 (".devcontainer/p10k.zsh", "vendored powerlevel10k theme config, not a project script"),
348)
349
350
351# ---------------------------------------------------------------------------
352# KNOWN_GAPS -- code files that NOTHING lints or formats today.
353#
354# This is NOT an exemption list, and the difference matters. An exemption says
355# "this file is not ours to check". A gap says "this file IS ours, nothing
356# checks it, here is the issue and here is exactly how many there are". Every
357# entry carries a recorded count and the gate fails the moment a gap grows past
358# it -- so a hole can be carried deliberately while it is being closed, but it
359# can never widen unnoticed, and it can never quietly become permanent.
360#
361# Closing a gap means deleting its row, not raising its count.
362# ---------------------------------------------------------------------------
363@dataclass(frozen=True)
364class GapCtx:
365 """What a gap predicate gets to look at when deciding if a file is its own."""
366
367 rel: str
368 cls: str
369 text: str
370 """File contents, read lazily and only for files already known uncovered."""
371
372
373@dataclass(frozen=True)
374class Gap:
375 """One recorded hole in lint/format coverage."""
376
377 name: str
378 count: int
379 issue: str
380 reason: str
381 match: Callable[[GapCtx], bool]
382
383
384KNOWN_GAPS: tuple[Gap, ...] = (
385 Gap(
386 "objc-needs-macos-runner",
387 2,
388 "#436",
389 "the two Objective-C host views (ra8_emulator, ra8_viewer) are AppKit / "
390 "CoreGraphics code. clang-tidy can only parse them against the macOS "
391 "SDK, so clang_tidy.sh claims them on Darwin and not on Linux -- where "
392 "CI runs. The C++ half of #370 is fully closed: every .cpp/.cc finding "
393 "is fixed and its tidy-baseline rows are gone. Closing this one needs a "
394 "macOS lint job, which is a runner decision, not a code change",
395 lambda c: c.cls == "c-family" and c.rel.endswith(".m"),
396 ),
397)
398
399
400def exemption_reason(rel: str) -> str | None:
401 """Return the justification for `rel` being exempt, or None if it is not."""
402 for prefix, reason in EXEMPT_PREFIXES:
403 if rel == prefix or rel.startswith(prefix):
404 return reason
405 return None
406
407
408def validate_tables() -> list[str]:
409 """Assert the tables are internally consistent. Returns a list of problems.
410
411 A class named by PATH_CLASS/EXT_CLASS/NAME_CLASS but absent from CLASSES would make
412 the gate crash on a file it was supposed to classify, and an exemption with
413 an empty reason is the rot this list exists to prevent.
414 """
415 problems: list[str] = []
416 for table_name, table in (
417 ("PATH_CLASS", PATH_CLASS),
418 ("EXT_CLASS", EXT_CLASS),
419 ("NAME_CLASS", NAME_CLASS),
420 ):
421 for key, cls in table.items():
422 if cls not in CLASSES:
423 problems.append(f"{table_name}[{key!r}] names unknown class {cls!r}")
424 for _, cls in SHEBANG_CLASS:
425 if cls not in CLASSES:
426 problems.append(f"SHEBANG_CLASS names unknown class {cls!r}")
427 for prefix, reason in EXEMPT_PREFIXES:
428 if not reason.strip():
429 problems.append(f"exemption {prefix!r} carries no reason")
430 return problems