3"""Which files ``doxy_audit.py`` reads, in each of its two scopes.
5The function gate covers first-party C under ``libs/``, ``port/``, ``tools/``,
6and ``apps/``, excluding nested test directories. The member gate is wider
7still because it also covers ``examples/`` and ``tests/``. The function debt
8that existed when tools entered the scope is frozen in the function baseline;
9excluding a tool until it became perfect made new gaps in that tool invisible
10and contradicted the repository-wide policy.
12Each scope also owns its own vacuity floor, and :func:`function_files` /
13:func:`member_files` are the materialising accessors every caller must use.
14A generator that yields nothing costs an auditor nothing to consume and makes
15it print ``gaps=0 (PASS)`` -- a perfectly documented tree and a completely
16broken walk are the same output. The floors live beside the scope lists they
17guard because that is the pair that has to stay consistent; both exit 2, so
18"the scan broke" stays distinguishable from "the tree is undocumented".
21from __future__
import annotations
26from collections.abc
import Iterator
27from pathlib
import Path
29from lint_targets
import is_build_output_path
33_REAL_REPO_ROOT = Path(__file__).resolve().parents[2]
36_repo_root = _REAL_REPO_ROOT
39def repo_root() -> Path:
40 """Return the root every path in this auditor is reported relative to."""
44@contextlib.contextmanager
45def override_repo_root(root: Path) -> Iterator[
None]:
46 """Report paths relative to ``root`` for the duration of the block.
48 Used only by the selftest, which audits a synthetic tree in a temporary
49 directory. It used to rebind a module-global ``repo_root()`` instead, which
50 stopped working the moment the auditor was split across modules: the other
51 modules hold their own imported copy, so the rebinding would be invisible
52 and the suite would silently assert against the real checkout.
63SCAN_DIRS = [
"libs",
"port",
"tools",
"apps"]
70EXCLUDE_PARTS = {
"third_party",
"generated",
"build",
".git",
"tests",
"test"}
78MEMBER_SCAN_DIRS = [
"libs",
"port",
"examples",
"tools",
"apps",
"tests"]
79MEMBER_EXCLUDE_PARTS = {
"third_party",
"ra8_fonts",
"build",
"build-cov",
"_deps",
".git"}
86GENERATED_PROTOCOL_FILES = {
87 "libs/ra8_c6link/inc/ra8_media_download.pb-c.h",
88 "libs/ra8_c6link/src/ra8_media_download.pb-c.c",
97FUNCTION_FILE_FLOOR = 700
98MEMBER_FILE_FLOOR = 1700
101MODULE_PATH_MIN_DEPTH = 2
104def _is_generated_protocol_file(path: Path) -> bool:
105 """Return whether ``path`` is one exact reviewed protoc-c output."""
107 relative = path.resolve().relative_to(repo_root().resolve()).as_posix()
110 return relative
in GENERATED_PROTOCOL_FILES
113def _is_build_output(path: Path) -> bool:
114 """Return whether ``path`` is inside any recognized generated build tree."""
116 relative = path.resolve().relative_to(repo_root().resolve()).as_posix()
119 return is_build_output_path(relative)
122def iter_function_files() -> Iterator[Path]:
123 """Yield every first-party .c/.h the FUNCTION gate audits.
125 Both the strict gate and the report generator walked this identically
126 inline; sharing one iterator means the report can never describe a
127 different set of files than the gate enforces over. ``tools/`` is a full
128 top-level root, so adding a first-party tool cannot silently place it
129 outside the documentation gate.
131 for top
in SCAN_DIRS:
132 root = repo_root() / top
133 if not root.is_dir():
135 for dirpath, dirnames, filenames
in os.walk(root):
139 if d
not in EXCLUDE_PARTS
and not _is_build_output(Path(dirpath) / d)
142 if not fn.endswith((
".c",
".h")):
144 p = Path(dirpath) / fn
145 if any(part
in EXCLUDE_PARTS
for part
in p.relative_to(repo_root()).parts):
147 if _is_build_output(p)
or _is_generated_protocol_file(p):
152def _iter_member_files(explicit: list[str]) -> Iterator[Path]:
153 """Yield first-party .c/.h paths for the member audit.
155 ``explicit`` is a list of user-supplied paths; if non-empty those exact
156 files are audited, otherwise MEMBER_SCAN_DIRS is walked.
159 for raw_path
in explicit:
162 candidate = p
if p.is_file()
else repo_root() / raw_path
165 and not _is_build_output(candidate)
166 and not _is_generated_protocol_file(candidate)
170 for top
in MEMBER_SCAN_DIRS:
171 root = repo_root() / top
172 if not root.is_dir():
174 for dirpath, dirnames, filenames
in os.walk(root):
178 if d
not in MEMBER_EXCLUDE_PARTS
and not _is_build_output(Path(dirpath) / d)
181 if not fn.endswith((
".c",
".h")):
183 p = Path(dirpath) / fn
184 if any(part
in MEMBER_EXCLUDE_PARTS
for part
in p.relative_to(repo_root()).parts):
186 if _is_build_output(p)
or _is_generated_protocol_file(p):
191def _fatal_below_floor(kind: str, count: int, floor: int) ->
None:
192 """Abort with exit 2 when a scope walk enumerated fewer files than its floor.
195 kind: Which scope collapsed, named as the caller's flag would spell it
196 (e.g. ``"function"`` or ``"member"``).
197 count: How many files the walk actually yielded.
198 floor: The measured floor that count must reach.
201 SystemExit: Always, with code 2, when ``count`` is below ``floor``.
206 f
"doxy_audit.py: FATAL -- only {count} file(s) in the {kind} scope, "
207 f
"floor is {floor}.\n"
208 " A collapsed scope reports a documented tree because it read nothing.\n"
213def function_files() -> list[Path]:
214 """Every file the FUNCTION scope audits, materialised and floor-checked.
216 The accessor exists so no caller can consume :func:`iter_function_files`
217 lazily and never notice that it yielded nothing: a generator that produces
218 no items looks exactly like a fully documented tree to the auditor
222 The function-scope paths, in walk order.
225 SystemExit: With code 2 when fewer than FUNCTION_FILE_FLOOR files were
226 found -- a collapsed walk must fail, never read as clean.
228 files = list(iter_function_files())
229 _fatal_below_floor(
"function", len(files), FUNCTION_FILE_FLOOR)
233def member_files(explicit: list[str]) -> list[Path]:
234 """Every file the MEMBER scope audits, materialised and floor-checked.
236 The floor applies to the repo-wide walk only. An ``explicit`` list is a
237 deliberately narrowed scope supplied on the command line -- it is allowed
238 to be one file, or to filter to none -- while a collapsed walk is a broken
239 enumeration reporting a documented tree.
242 explicit: User-supplied paths; empty means walk MEMBER_SCAN_DIRS.
245 The member-scope paths, in walk order.
248 SystemExit: With code 2 when the repo-wide walk found fewer than
249 MEMBER_FILE_FLOOR files.
251 files = list(_iter_member_files(explicit))
253 _fatal_below_floor(
"member", len(files), MEMBER_FILE_FLOOR)
257def _top_dir(rel: str) -> str:
258 """Top-level directory of a repo-relative path (e.g. "libs")."""
259 return rel.split(
"/", 1)[0]