ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
doxy_audit.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"""Doxygen documentation gap auditor for ra8-firmware.
5
6Walks every .c/.h under libs/, src/, port/ and tools/ (excluding vendored and
7generated code),
8locates every function definition/prototype, and checks the immediately
9preceding Doxygen block for the required tags listed in CLAUDE.md
10("Doxygen Documentation Requirements").
11
12The pre-existing function backlog is closed. Every function gap now fails
13immediately; no documentation baseline or update path remains. Machine-emitted
14tool code remains exempt like vendored SOUP.
15
16Modes
17-----
18 (no args) Function audit report -> docs/DOXYGEN_GAPS.csv + .md
19 --check Strict function gate (exit 1 on any gap). Wired into CI
20 and the pre-commit hook.
21 --selftest Regression-test the auditor itself, in both directions,
22 for both enforcing modes. Runs before the real check in
23 the gate: a parser-driven gate that stops recognising a
24 construct reports nothing and looks like a documented
25 tree.
26 --members REPORT-ONLY member audit. Enumerates every undocumented
27 enum value, struct/union member, and macro across the
28 first-party tree and prints an offender count per top-level
29 dir. This mode never fails the build. Optionally takes
30 explicit file paths to audit just those, and --out=PATH to
31 dump the full offender list as CSV.
32 --members --check ENFORCING member gate (exit 1 on any undocumented enum
33 value, struct/union member, or macro). Wired into CI and
34 the pre-commit hook alongside the function gate (issue
35 #246). Optionally takes explicit file paths to gate just
36 those.
37 --style ENFORCING gate for the two docs/STYLE_GUIDE.md tag rules
38 that attach to no symbol, so neither the function nor the
39 member gate ever saw them (#532): the file-header block
40 (@file present and naming this file, @brief, @details) and
41 the @param direction bracket. Unlike --members there is no
42 report-only twin -- a mode that measures instead of failing
43 is how the member gate was once mis-wired.
44
45CLAUDE.md ("Doxygen Documentation Requirements") demands that *every* enum
46value, struct/union member, and macro carry documentation -- an inline
47``/**< ... */`` (or a preceding ``/** ... */`` block) on each aggregate member
48and a ``@brief``/``@def`` block on each macro. ``--members --check`` enforces
49exactly that (issue #246); plain ``--members`` is the sizing report.
50
51Output:
52 - docs/DOXYGEN_GAPS.csv full row-per-function table
53 - docs/DOXYGEN_GAPS.md human-readable summary
54 - (--members) stdout summary; optional --out=PATH member-gap CSV
55
56Audit-only: never edits source files.
57
58Exit 0 when clean or report-only, 1 when an enforcing mode found gaps, 2 when
59a scope walk collapsed below its floor (doxy_scope.FUNCTION_FILE_FLOOR /
60MEMBER_FILE_FLOOR).
61"""
62
63from __future__ import annotations
64
65import sys
66from collections import Counter
67from pathlib import Path
68
69sys.path.insert(0, str(Path(__file__).resolve().parent))
70
71from doxy_functions import audit_file
72from doxy_members import audit_members_file
73from doxy_report import run_report
74from doxy_scope import _top_dir, function_files, member_files, repo_root
75from doxy_selftest import run_selftest
76from doxy_style import run_check as run_style_check
77
78#: Offender lines printed before the gate truncates, so a hook stays readable.
79OFFENDER_CAP = 50
80
81
82def run_check() -> int:
83 """Strict gate: exit 0 if zero gaps, else exit 1 with offender list.
84
85 Used by the pre-commit hook to keep documentation debt from growing.
86 Does not write CSV/MD outputs -- read-only audit.
87
88 Scope comes from :func:`doxy_scope.function_files`, which exits 2 rather
89 than let a collapsed walk report ``gaps=0 (PASS)``.
90 """
91 all_rows = []
92 for path in function_files():
93 all_rows.extend(audit_file(path))
94
95 gap_rows = [r for r in all_rows if r[3]]
96 if not gap_rows:
97 print("doxy_audit --check: gaps=0 (PASS; strict, no baseline)")
98 return 0
99
100 print(f"doxy_audit --check: gaps={len(gap_rows)} (FAIL; strict, no baseline)")
101 print("Offending functions (file:line function -- missing tags):")
102 # cap output to 50 lines so the hook stays readable
103 cap = OFFENDER_CAP
104 for src, line, name, missing, _sev in gap_rows[:cap]:
105 print(f" {src}:{line} {name} -- {';'.join(missing)}")
106 if len(gap_rows) > cap:
107 print(f" ... and {len(gap_rows) - cap} more")
108 print()
109 print("Refresh the audit report by running:")
110 print(" python3 scripts/checks/doxy_audit.py")
111 return 1
112
113
114def run_members_report(explicit: list[str], out_csv: str | None) -> int:
115 """REPORT-ONLY member/enum/macro audit. Always returns 0.
116
117 Enumerates every undocumented enum value, struct/union member, and macro
118 across the first-party tree and prints an offender count per top-level dir
119 so the owner can size the fallout wave (issue #246). This mode intentionally
120 never fails: promoting the member checks to a hard gate is a follow-up.
121
122 "Never fails" covers findings, not infrastructure:
123 :func:`doxy_scope.member_files` still exits 2 on a collapsed repo-wide
124 walk, since a report sized from nothing is worse than no report.
125 """
126 all_rows = []
127 for p in member_files(explicit):
128 all_rows.extend(audit_members_file(p))
129
130 by_dir_kind = Counter((_top_dir(r[0]), r[2]) for r in all_rows)
131 by_dir = Counter(_top_dir(r[0]) for r in all_rows)
132 by_kind = Counter(r[2] for r in all_rows)
133 by_file = Counter(r[0] for r in all_rows)
134
135 if out_csv is not None:
136 out_path = Path(out_csv)
137 if not out_path.is_absolute():
138 out_path = repo_root() / out_path
139 with out_path.open("w", encoding="ascii") as f:
140 f.write("source_file,line,element,name,reason\n")
141 for rel, line, kind, name, reason in sorted(all_rows, key=lambda r: (r[0], r[1])):
142 f.write(f"{rel},{line},{kind},{name},{reason}\n")
143
144 kinds = ["enum-value", "member", "macro"]
145 dirs = sorted(by_dir)
146
147 print("doxy_audit --members: REPORT-ONLY member/enum/macro documentation audit")
148 print("(this mode never fails the build; see issue #246)")
149 print()
150 print("Undocumented offenders per top-level dir:")
151 header = f" {'dir':<12}" + "".join(f"{k:>13}" for k in kinds) + f"{'total':>10}"
152 print(header)
153 print(" " + "-" * (len(header) - 2))
154 for d in dirs:
155 cells = "".join(f"{by_dir_kind[(d, k)]:>13}" for k in kinds)
156 print(f" {d:<12}{cells}{by_dir[d]:>10}")
157 totals = "".join(f"{by_kind[k]:>13}" for k in kinds)
158 print(" " + "-" * (len(header) - 2))
159 print(f" {'TOTAL':<12}{totals}{len(all_rows):>10}")
160 print()
161 print(f"Files scanned with at least one offender: {len(by_file)}")
162 print("Worst 15 files by offender count:")
163 for rel, cnt in by_file.most_common(15):
164 print(f" {cnt:>6} {rel}")
165 if out_csv is not None:
166 print()
167 print(f"Full offender list written to: {out_csv}")
168 return 0
169
170
171def run_members_check(explicit: list[str]) -> int:
172 """ENFORCING member gate: exit 0 if zero offenders, else exit 1.
173
174 Enforces the CLAUDE.md rule that every enum value, struct/union member, and
175 macro carries documentation (issue #246). Wired into the pre-commit hook and
176 CI alongside the function gate. Read-only -- writes no CSV/MD outputs.
177
178 Scope comes from :func:`doxy_scope.member_files`, which exits 2 rather than
179 let a collapsed repo-wide walk report ``offenders=0 (PASS)``.
180 """
181 all_rows = []
182 for p in member_files(explicit):
183 all_rows.extend(audit_members_file(p))
184 all_rows.sort(key=lambda r: (r[0], r[1]))
185
186 if not all_rows:
187 print("doxy_audit --members --check: offenders=0 (PASS)")
188 return 0
189
190 by_kind = Counter(r[2] for r in all_rows)
191 print(f"doxy_audit --members --check: offenders={len(all_rows)} (FAIL)")
192 print(
193 " by kind: "
194 + ", ".join(f"{k}={by_kind[k]}" for k in ("enum-value", "member", "macro") if by_kind[k])
195 )
196 print("Undocumented members (file:line kind name -- reason):")
197 cap = OFFENDER_CAP
198 for rel, line, kind, name, reason in all_rows[:cap]:
199 print(f" {rel}:{line} {kind} {name} -- {reason}")
200 if len(all_rows) > cap:
201 print(f" ... and {len(all_rows) - cap} more")
202 print()
203 print("Every enum value, struct/union member, and macro needs a doc comment:")
204 print(" - aggregate members: an inline /**< ... */ or a preceding /** ... */ block")
205 print(" - macros: a preceding /** @brief ... */ (or @def) block")
206 print("Size the full fallout with: python3 scripts/checks/doxy_audit.py --members")
207 return 1
208
209
210def _parse_members_args(argv: list[str]) -> tuple[list[str], str | None]:
211 """Split --members argv into (explicit_paths, out_csv)."""
212 explicit = []
213 out_csv = None
214 for a in argv:
215 if a == "--members":
216 continue
217 if a.startswith("--out="):
218 out_csv = a[len("--out=") :]
219 elif not a.startswith("--"):
220 explicit.append(a)
221 return explicit, out_csv
222
223
224def main() -> int:
225 """Dispatch to the function gate, the member gate, the style gate, or a report.
226
227 The two member modes are NOT interchangeable: ``--members`` alone is
228 report-only and always exits 0 (it exists to size the #246 fallout), while
229 ``--members --check`` is the enforcing gate. CI must pass both flags, or
230 the step measures the problem instead of failing on it. ``--style`` has no
231 such pair on purpose -- it is always enforcing, so there is no spelling of
232 it that measures instead of failing.
233
234 Returns 0 on a clean gate, a passing selftest, or any report-only run;
235 1 when an enforcing mode found offenders. Exits 2 when a mode could not
236 run at all -- either scope walk collapsing below its measured file floor
237 (:mod:`doxy_scope`), or ``--style``'s file / ``@param`` floors
238 (:mod:`doxy_style`). A scan that read nothing must never report a
239 documented tree.
240 """
241 args = sys.argv[1:]
242 unknown = [
243 arg
244 for arg in args
245 if arg.startswith("--")
246 and arg not in {"--check", "--members", "--selftest", "--style"}
247 and not arg.startswith("--out=")
248 ]
249 if unknown:
250 sys.stderr.write(f"doxy_audit: unknown option(s): {' '.join(unknown)}\n")
251 return 2
252 if "--selftest" in args:
253 return run_selftest()
254 if "--style" in args:
255 return run_style_check()
256 if "--members" in args:
257 explicit, out_csv = _parse_members_args(args)
258 return (
259 run_members_check(explicit)
260 if "--check" in args
261 else run_members_report(explicit, out_csv)
262 )
263 if "--check" in args:
264 return run_check()
265 return run_report()
266
267
268if __name__ == "__main__":
269 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298