4"""Gate: a Doxygen block must describe the thing it is actually attached to.
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.
14Real defects that motivated this (both found by eye, in ``tools/rabook_viewer``):
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.
21Why a sibling script rather than a new ``doxy_audit.py`` mode:
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/``)
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
35* **Single Responsibility** (CLAUDE.md, SOLID for C): presence and correctness
36 are separate concerns with separate failure modes and separate fix
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.
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:
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
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
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
67from __future__
import annotations
71from pathlib
import Path
73sys.path.insert(0, str(Path(__file__).resolve().parent))
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
88def run(targets: list[Path], strict: bool, *, enforce_floor: bool) -> int:
89 """Scan ``targets`` and report.
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.
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
104 cindex = _require_libclang()
105 args = [
"-std=c23",
"-x",
"c",
"-DRA8_HOST_BUILD=1", *_include_args(cindex)]
107 files = iter_sources(targets)
108 if enforce_floor
and len(files) < FILE_FLOOR:
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.",
116 findings: list[Finding] = []
118 findings.extend(check_file(path, cindex, args))
120 findings.sort(key=
lambda f: (f.path, f.line, f.code))
122 print(f
"check_doc_attachment: files={len(files)} findings=0 (PASS)")
125 by_code: dict[str, int] = {}
127 by_code[f.code] = by_code.get(f.code, 0) + 1
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]}")
136 return 1
if strict
else 0
140 """Report Doxygen blocks that describe something other than what they precede.
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.
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.
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.
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)")
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)
169if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.