ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
doxy_report.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""The audit-only report: docs/DOXYGEN_GAPS.csv and docs/DOXYGEN_GAPS.md.
4
5Separate from the gate because it answers a different question. The gate asks
6"is the tree clean" and exits non-zero; this asks "where is the remaining work
7and how much of it is there", writes two artefacts, and always exits 0.
8
9It walks the same files the gate does, through
10:func:`doxy_scope.function_files`, so the report can never describe a
11different set of files than the gate enforces over -- the two used to carry
12the same walk inline, twice. That accessor also carries the shared vacuity
13floor: "always exits 0" is about findings, not about infrastructure, and a
14report sized from a collapsed walk would understate the remaining work as
15zero.
16"""
17
18from __future__ import annotations
19
20from collections import Counter
21
22from doxy_functions import audit_file
23from doxy_scope import MODULE_PATH_MIN_DEPTH, function_files, repo_root
24
25# Maximum number of lines in the generated Markdown report (keep pre-commit
26# output brief).
27MD_REPORT_LINE_CAP = 200
28
29#: Worst-offender table sizes in the Markdown report.
30TOP_MODULES = 10
31TOP_FILES = 30
32
33
34def collect_rows() -> list:
35 """Audit every in-scope file, sorted by path then line."""
36 rows = []
37 for path in function_files():
38 rows.extend(audit_file(path))
39 rows.sort(key=lambda r: (r[0], r[1]))
40 return rows
41
42
43def _write_csv(gap_rows: list) -> None:
44 """Write the full row-per-function gap table."""
45 csv_path = repo_root() / "docs" / "DOXYGEN_GAPS.csv"
46 with csv_path.open("w", encoding="ascii") as f:
47 f.write("source_file,line,function_name,missing_tags,severity\n")
48 for src, line, name, missing, sev in gap_rows:
49 f.write(f"{src},{line},{name},{';'.join(missing)},{sev}\n")
50
51
52def _tag_frequency(gap_rows: list) -> Counter:
53 """Count missing tags, collapsing per-instance spellings into their family.
54
55 ``@param[buf]`` and ``@param[len]`` are two instances of one missing tag,
56 and ``@pre(<2:1)`` records a count rather than a distinct tag; without the
57 collapse the frequency table lists every parameter name in the tree.
58 """
59 counter: Counter = Counter()
60 for r in gap_rows:
61 for t in r[3]:
62 if t.startswith("@param["):
63 counter["@param"] += 1
64 elif t.startswith("@pre"):
65 counter["@pre"] += 1
66 elif t.startswith("@post"):
67 counter["@post"] += 1
68 else:
69 counter[t] += 1
70 return counter
71
72
73def _module_of(path: str) -> str:
74 """Two-segment module label for a repo-relative path (e.g. "libs/ra8_hal")."""
75 parts = path.split("/")
76 if len(parts) >= MODULE_PATH_MIN_DEPTH:
77 return f"{parts[0]}/{parts[1]}"
78 return parts[0]
79
80
81def _table(title: str, header: str, sep: str, rows: list[tuple[str, int]]) -> list[str]:
82 """One Markdown section: a heading and a two-column count table.
83
84 ``sep`` is passed rather than generated so the emitted report stays
85 byte-identical to the committed docs/DOXYGEN_GAPS.md -- the dash widths
86 are cosmetic to a renderer but a spurious diff to a reviewer.
87 """
88 out = [f"## {title}", "", header, sep]
89 out.extend(f"| `{label}` | {count} |" for label, count in rows)
90 out.append("")
91 return out
92
93
94def _render_markdown(rows: list, gap_rows: list) -> str:
95 """Build the human-readable summary report."""
96 total_funcs = len(rows)
97 total_gaps = len(gap_rows)
98 total_missing_tags = sum(len(r[3]) for r in gap_rows)
99
100 lines = [
101 "# Doxygen Documentation Gap Report",
102 "",
103 "Audit-only report generated by `scripts/checks/doxy_audit.py` against the",
104 "Doxygen Documentation Requirements in `CLAUDE.md`. Scope: `libs/`,",
105 "`port/` and all of `tools/` (excluding vendored and generated",
106 "code).",
107 "",
108 "## Summary",
109 "",
110 f"- Total functions audited: {total_funcs}",
111 f"- Functions with gaps: {total_gaps}",
112 f"- Total missing-tag instances: {total_missing_tags}",
113 "",
114 ]
115 lines += _table(
116 "Most-frequently-missing tags",
117 "| Tag | Count |",
118 "|-----|-------|",
119 _tag_frequency(gap_rows).most_common(),
120 )
121 lines += _table(
122 f"Worst {TOP_MODULES} modules by gap count",
123 "| Module | Functions with gaps |",
124 "|--------|---------------------|",
125 Counter(_module_of(r[0]) for r in gap_rows).most_common(TOP_MODULES),
126 )
127 lines += _table(
128 f"Top {TOP_FILES} files by gap count",
129 "| File | Functions with gaps |",
130 "|------|---------------------|",
131 Counter(r[0] for r in gap_rows).most_common(TOP_FILES),
132 )
133 lines += [
134 "## Severity legend",
135 "",
136 "- `high`: missing `@brief` or any `@param[...]`",
137 "- `medium`: missing `@return` / `@retval` / `@pre` / `@post`",
138 "- `low`: only optional / informational tags missing (`@note`, `@since`, ...)",
139 "",
140 "See `docs/DOXYGEN_GAPS.csv` for the full row-by-row data.",
141 "",
142 "## Audit history",
143 "",
144 "| Date | Functions with gaps | Missing-tag instances |",
145 "|------|---------------------|-----------------------|",
146 "| 2026-05-02 (original) | 2557 | 20328 |",
147 "| 2026-05-02 (refresh) | 663 | 4935 |",
148 f"| 2026-05-02 (auditor false-pos fix) | {total_gaps} | {total_missing_tags} |",
149 "",
150 ]
151 # Cap the report so it stays browsable in a pre-commit log.
152 return "\n".join(lines[:MD_REPORT_LINE_CAP])
153
154
155def run_report() -> int:
156 """Write both artefacts and print the headline counts. Always exits 0."""
157 rows = collect_rows()
158 gap_rows = [r for r in rows if r[3]]
159
160 _write_csv(gap_rows)
161 md_path = repo_root() / "docs" / "DOXYGEN_GAPS.md"
162 md_path.write_text(_render_markdown(rows, gap_rows), encoding="ascii")
163
164 total_missing_tags = sum(len(r[3]) for r in gap_rows)
165 print(f"functions={len(rows)} gaps={len(gap_rows)} missing_tags={total_missing_tags}")
166 return 0