ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_doc_attachment.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"""Gate: a Doxygen block must describe the thing it is actually attached to.
5
6The presence gates (``doxy_audit.py --check`` for functions,
7``doxy_audit.py --members --check`` for members/enums/macros) only ask *is a
8block there*. They cannot see a block that is there but describes something
9else, and a misattached block actively **satisfies** them: paste one block
10twice and measured "coverage" goes up while one symbol silently loses its
11documentation and another gains a duplicate. Every defect this gate finds was
12therefore invisible to -- and in some cases rewarded by -- the existing gates.
13
14Real defects that motivated this (both found by eye, in ``tools/rabook_viewer``):
15
16* ``main()``'s block sat immediately above ``viewer_log_sink()``'s block, so
17 ``main()`` was undocumented and the sink was documented twice.
18* ``viewer_compute_tiles()`` carried its block on a forward declaration while
19 the definition below it sat bare.
20
21Why a sibling script rather than a new ``doxy_audit.py`` mode:
22
23* **Different question, different parser.** ``doxy_audit.py`` is regex-driven
24 end to end; its ``FUNC_RE`` + ``strip_comments`` design answers "is a tag
25 present". Attachment needs real parameter names, real return types and real
26 declaration-vs-definition identity -- an AST question. Bolting a libclang
27 mode onto a regex tool means two parsers, two scope constants and two notions
28 of "a function" in one file, which is precisely the confusion that let
29 ``doxy_audit.py``'s own scope hole (no ``tools/``, ``tests/``, ``examples/``)
30 survive unnoticed.
31* **Different dependency.** This gate hard-requires libclang and must fail
32 loudly without it (see ``docattach_ast._require_libclang``). ``doxy_audit.py``
33 has no third-party dependency and must keep working in environments that
34 lack one.
35* **Single Responsibility** (CLAUDE.md, SOLID for C): presence and correctness
36 are separate concerns with separate failure modes and separate fix
37 procedures.
38
39Scope: every first-party ``.c`` / ``.h`` under ``libs/``, ``port/``,
40``examples/``, ``tools/`` and ``tests/``. Vendored SOUP (``libs/third_party``)
41and generated data (``libs/ra8_fonts``) are excluded, matching CLAUDE.md.
42
43Module layout
44-------------
45This file is the driver only. The checker is split by what each part needs to
46do its job (#359), which is also the axis along which the two passes must not
47be allowed to disagree:
48
49 :mod:`docattach_scope` which files are read
50 :mod:`docattach_model` finding codes, tag grammar, and the shared records
51 :mod:`docattach_lex` the findings answerable from text alone
52 :mod:`docattach_ast` libclang setup and the findings needing a parse
53 :mod:`docattach_selftest` both-direction fixtures for every finding code
54
55Run::
56
57 check_doc_attachment.py --check # CI gate (exit 1 on any finding)
58 check_doc_attachment.py # audit listing, exit 0
59 check_doc_attachment.py --selftest # synthetic both-direction fixtures
60 check_doc_attachment.py PATH ... # restrict to the given files/dirs
61
62Exit 0 when clean, 1 on findings in ``--check`` mode, 2 on a selftest failure,
63a missing/unusable libclang, or a whole-tree scan that collapsed below
64FILE_FLOOR.
65"""
66
67from __future__ import annotations
68
69import argparse
70import sys
71from pathlib import Path
72
73sys.path.insert(0, str(Path(__file__).resolve().parent))
74
75from docattach_ast import _include_args, _require_libclang, check_file
76from docattach_model import CODE_HELP, Finding
77from docattach_scope import default_targets, iter_sources
78from docattach_selftest import selftest
79
80# A tree this size cannot legitimately collapse to a handful of files. If the
81# whole-tree enumeration returns less than this, something broke (a renamed
82# root, an unreachable repo root) and reporting ``findings=0 (PASS)`` would be
83# a lie: a misattached block cannot be found in a file nobody parsed. Measured
84# 2026-07-28: 2116 first-party C sources. Same trip-wire as check_ruff.py.
85FILE_FLOOR = 1700
86
87
88def run(targets: list[Path], strict: bool, *, enforce_floor: bool) -> int:
89 """Scan ``targets`` and report.
90
91 Args:
92 targets: Files and/or directories to sweep.
93 strict: When true a finding fails the run (the ``--check`` gate mode);
94 otherwise findings are printed advisory-only.
95 enforce_floor: Apply FILE_FLOOR to the enumerated source list. Set only
96 for the default whole-tree scan -- an explicit argv path list is a
97 deliberately narrowed scope, not a collapsed one.
98
99 Returns:
100 0 when clean or running advisory-only, 1 on a finding under ``strict``,
101 2 when ``enforce_floor`` is set and the enumeration fell below
102 FILE_FLOOR.
103 """
104 cindex = _require_libclang()
105 args = ["-std=c23", "-x", "c", "-DRA8_HOST_BUILD=1", *_include_args(cindex)]
106
107 files = iter_sources(targets)
108 if enforce_floor and len(files) < FILE_FLOOR:
109 print(
110 f"check_doc_attachment.py: FATAL -- only {len(files)} source file(s) in "
111 f"scope, floor is {FILE_FLOOR}. A collapsed scope reports a clean tree "
112 "because it parsed nothing.",
113 file=sys.stderr,
114 )
115 return 2
116 findings: list[Finding] = []
117 for path in files:
118 findings.extend(check_file(path, cindex, args))
119
120 findings.sort(key=lambda f: (f.path, f.line, f.code))
121 if not findings:
122 print(f"check_doc_attachment: files={len(files)} findings=0 (PASS)")
123 return 0
124
125 by_code: dict[str, int] = {}
126 for f in findings:
127 by_code[f.code] = by_code.get(f.code, 0) + 1
128
129 verdict = "FAIL" if strict else "audit"
130 print(f"check_doc_attachment: files={len(files)} findings={len(findings)} ({verdict})")
131 for code in sorted(by_code):
132 print(f" {code} x{by_code[code]:<5} {CODE_HELP[code]}")
133 print()
134 for f in findings:
135 print(f.render())
136 return 1 if strict else 0
137
138
139def main() -> int:
140 """Report Doxygen blocks that describe something other than what they precede.
141
142 ``--check`` is what makes this a gate: without it findings are printed and
143 the process still exits 0, which is the advisory mode used while a module
144 is being cleaned up. CI must pass ``--check`` or the step cannot fail.
145
146 With no positional paths the scan covers every first-party root, so the
147 argument list narrows the sweep and never widens it. FILE_FLOOR is applied
148 only to that default sweep, and exits 2 below it: a narrowed scope is a
149 request, while a collapsed sweep is a broken enumeration reporting a clean
150 tree because it parsed nothing.
151
152 Returns 0 when clean, when running advisory-only, or after a passing
153 ``--selftest``; 1 on a finding under ``--check`` or a failing selftest;
154 2 when the default sweep enumerated too few files to trust.
155 """
156 ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
157 ap.add_argument("--check", action="store_true", help="CI gate: exit 1 on any finding")
158 ap.add_argument("--selftest", action="store_true", help="run the synthetic fixtures")
159 ap.add_argument("paths", nargs="*", help="files/dirs to scan (default: every first-party root)")
160 ns = ap.parse_args()
161
162 if ns.selftest:
163 return selftest()
164
165 targets = [Path(p).resolve() for p in ns.paths] if ns.paths else default_targets()
166 return run(targets, strict=ns.check, enforce_floor=not ns.paths)
167
168
169if __name__ == "__main__":
170 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298