4"""Doxygen documentation gap auditor for ra8-firmware.
6Walks every .c/.h under libs/, src/, port/ and tools/ (excluding vendored and
8locates every function definition/prototype, and checks the immediately
9preceding Doxygen block for the required tags listed in CLAUDE.md
10("Doxygen Documentation Requirements").
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.
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
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
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.
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.
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
56Audit-only: never edits source files.
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 /
63from __future__
import annotations
66from collections
import Counter
67from pathlib
import Path
69sys.path.insert(0, str(Path(__file__).resolve().parent))
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
82def run_check() -> int:
83 """Strict gate: exit 0 if zero gaps, else exit 1 with offender list.
85 Used by the pre-commit hook to keep documentation debt from growing.
86 Does not write CSV/MD outputs -- read-only audit.
88 Scope comes from :func:`doxy_scope.function_files`, which exits 2 rather
89 than let a collapsed walk report ``gaps=0 (PASS)``.
92 for path
in function_files():
93 all_rows.extend(audit_file(path))
95 gap_rows = [r
for r
in all_rows
if r[3]]
97 print(
"doxy_audit --check: gaps=0 (PASS; strict, no baseline)")
100 print(f
"doxy_audit --check: gaps={len(gap_rows)} (FAIL; strict, no baseline)")
101 print(
"Offending functions (file:line function -- missing tags):")
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")
109 print(
"Refresh the audit report by running:")
110 print(
" python3 scripts/checks/doxy_audit.py")
114def run_members_report(explicit: list[str], out_csv: str |
None) -> int:
115 """REPORT-ONLY member/enum/macro audit. Always returns 0.
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.
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.
127 for p
in member_files(explicit):
128 all_rows.extend(audit_members_file(p))
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)
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")
144 kinds = [
"enum-value",
"member",
"macro"]
145 dirs = sorted(by_dir)
147 print(
"doxy_audit --members: REPORT-ONLY member/enum/macro documentation audit")
148 print(
"(this mode never fails the build; see issue #246)")
150 print(
"Undocumented offenders per top-level dir:")
151 header = f
" {'dir':<12}" +
"".join(f
"{k:>13}" for k
in kinds) + f
"{'total':>10}"
153 print(
" " +
"-" * (len(header) - 2))
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}")
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:
167 print(f
"Full offender list written to: {out_csv}")
171def run_members_check(explicit: list[str]) -> int:
172 """ENFORCING member gate: exit 0 if zero offenders, else exit 1.
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.
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)``.
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]))
187 print(
"doxy_audit --members --check: offenders=0 (PASS)")
190 by_kind = Counter(r[2]
for r
in all_rows)
191 print(f
"doxy_audit --members --check: offenders={len(all_rows)} (FAIL)")
194 +
", ".join(f
"{k}={by_kind[k]}" for k
in (
"enum-value",
"member",
"macro")
if by_kind[k])
196 print(
"Undocumented members (file:line kind name -- reason):")
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")
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")
210def _parse_members_args(argv: list[str]) -> tuple[list[str], str |
None]:
211 """Split --members argv into (explicit_paths, out_csv)."""
217 if a.startswith(
"--out="):
218 out_csv = a[len(
"--out=") :]
219 elif not a.startswith(
"--"):
221 return explicit, out_csv
225 """Dispatch to the function gate, the member gate, the style gate, or a report.
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.
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
245 if arg.startswith(
"--")
246 and arg
not in {
"--check",
"--members",
"--selftest",
"--style"}
247 and not arg.startswith(
"--out=")
250 sys.stderr.write(f
"doxy_audit: unknown option(s): {' '.join(unknown)}\n")
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)
259 run_members_check(explicit)
261 else run_members_report(explicit, out_csv)
263 if "--check" in args:
268if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.