3"""Synthetic fixtures proving each finding fires -- and that none over-fires.
5Both directions, deliberately. A doc-attachment gate that cries wolf gets
6switched off, and a switched-off gate is indistinguishable from a clean tree,
7so every defect class here has a must-fire fixture AND the tricky-but-legal
8form that must stay silent beside it.
10Split out of the checker (#359): the fixture table is 600-odd lines of C, and
11keeping it inline meant the rules it proves were unreachable by eye.
14from __future__
import annotations
19from pathlib
import Path
20from types
import ModuleType
22from docattach_ast
import (
25 check_forward_decl_blocks,
27from docattach_lex
import check_banned_boilerplate, check_consecutive_blocks
28from docattach_model
import Finding
36SELFTEST_CASES: list[tuple[str, str, set[str]]] = [
39 "param name pasted from another function",
42 * @brief Add two numbers.
43 * @param[in] reader Open reader.
44 * @param[in] tile Tile index.
47int add_values(int lhs, int rhs) { return lhs + rhs; }
52 "defect behind an RA8_* annotation macro is still seen",
56 * @brief Emit one char-decl pair.
57 * @param[in] conn_handle Connection handle.
58 * @param[in] pdu Raw PDU.
62static void internal_emit_pair(unsigned char* resp, int pos) { (void)resp; (void)pos; }
67 "correct block behind an RA8_* annotation macro does not fire",
71 * @brief Emit one char-decl pair.
72 * @param[out] resp Response buffer.
73 * @param[in] pos Cursor into resp.
77static void internal_emit_pair(unsigned char* resp, int pos) { (void)resp; (void)pos; }
82 "param documented that does not exist",
85 * @brief Add two numbers.
86 * @param[in] lhs Left.
87 * @param[in] rhs Right.
88 * @param[in] carry Nonexistent.
91int add_values(int lhs, int rhs) { return lhs + rhs; }
96 "partially documented signature (1 of 3)",
99 * @brief Download one chapter.
100 * @param[in] url Source URL.
103int download_chapter(const char* url, const char* dest, int retries) { return 0; }
108 "retval on a void function",
111 * @brief Reset the widget.
112 * @param[in] id Widget id.
115void widget_reset(int id) { (void)id; }
120 "@return promising a value on a void function",
123 * @brief Reset the widget.
124 * @param[in] id Widget id.
125 * @return The number of registers cleared.
127void widget_reset(int id) { (void)id; }
132 "two doc blocks in a row (the ra8_viewer main/log_sink shape)",
135 * @brief Program entry point.
136 * @return 0 on success.
141 * @param[in] byte Byte to emit.
143static void log_sink(unsigned char byte) { (void)byte; }
148 "identical block pasted twice",
151 * @brief Compute the checksum.
152 * @param[in] len Length.
156 * @brief Compute the checksum.
157 * @param[in] len Length.
160int checksum(int len) { return len; }
165 "@fn naming a different function",
168 * @fn viewer_open_comic
169 * @brief Open an RTA1 atlas.
170 * @param[in] r Reader.
173int viewer_open_rta1(int r) { return r; }
178 "@struct naming a different struct",
181 * @struct lcd_config_t
182 * @brief Panel timing.
184struct panel_timing_t { int hsync; };
189 "@enum naming a different enum",
193 * @brief Reader states.
195enum reader_state_t { k_idle = 0 };
200 "definition-site 'Implementation of `X()`' naming a different function",
202/** @brief Implementation of `ra8_err_to_str()` -- linear-scan lookup. */
203int ra8_err_to_code(int c) { return c; }
208 "block on a forward declaration separated from its definition by other code",
211 * @brief Probe and cache every tile's native size.
212 * @param[in] r Reader.
215static int compute_tiles(int r);
218 * @brief Unrelated helper standing between the declaration and the body.
219 * @param[in] v Value.
222static int passthrough(int v) { return v; }
224static int compute_tiles(int r) { return r; }
229 "-Wmissing-prototypes idiom: local prototype directly above its definition",
232 * @brief Non-maskable interrupt handler.
235void NMI_Handler(void);
236void NMI_Handler(void) { }
241 "banned pointer-only boilerplate",
243/** @brief Implementation of ra8_foo (see header for full contract). */
244int ra8_foo(void) { return 0; }
249 "two adjacent blocks are still caught when the gap is only whitespace",
252 * Enable a build option that nothing below actually declares.
256 * @brief Widget identifier width.
258#define WIDGET_ID_BITS 8
263 "a real comment between two blocks does not license a duplicate",
266 * Enable a build option that nothing below actually declares.
268/* an ordinary comment, not a commented-out directive */
271 * @brief Widget identifier width.
273#define WIDGET_ID_BITS 8
279 "untagged block documenting a commented-out config option",
282 * Enable the verified implementations of ECDH primitives from Project Everest.
284 * The Everest code is Apache-2.0 only, so enabling this is incompatible with
285 * taking the library under GPL-2.0-or-later.
287//#define MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED
290 * \\def MBEDTLS_GCM_LARGE_TABLE
292 * Use a larger GCM table to speed up AES-GCM.
294//#define MBEDTLS_GCM_LARGE_TABLE
299 "commented-out #undef also counts as the documented subject",
302 * Disable the built-in entropy sources.
304// #undef MBEDTLS_ENTROPY_C
307 * @brief Widget identifier width.
309#define WIDGET_ID_BITS 8
314 "correct function block",
317 * @brief Add two numbers.
318 * @param[in] lhs Left operand.
319 * @param[in] rhs Right operand.
321 * @retval 0 Both operands were zero.
323int add_values(int lhs, int rhs) { return lhs + rhs; }
328 "correct void function (no @return/@retval)",
331 * @brief Reset the widget.
332 * @param[in] id Widget id.
334void widget_reset(int id) { (void)id; }
339 "@file block directly above a symbol block",
343 * @brief Demo translation unit.
346 * @brief Add two numbers.
347 * @param[in] lhs Left.
348 * @param[in] rhs Right.
351int add_values(int lhs, int rhs) { return lhs + rhs; }
356 "@defgroup and @{ grouping markers between blocks",
359 * @defgroup lcd LCD driver
363 * @brief Clear the framebuffer.
372 "@copydoc block with another symbol's parameter names",
374/** @copydoc ra8_gpio_output_init */
375int ra8_gpio_output_init_impl(int port, int pin) { return port + pin; }
380 "sanctioned definition-site single-line form, correct name",
382/** @brief Implementation of `ra8_err_to_str()` -- linear-scan lookup. */
383int ra8_err_to_str(int code) { return code; }
388 "undocumented parameters with no @param at all (doxy_audit's job, not ours)",
391 * @brief Add two numbers.
394int add_values(int lhs, int rhs) { return lhs + rhs; }
399 "@param inside a @code example naming other symbols",
402 * @brief Register a handler.
403 * @param[in] handler Callback.
406 * // @param[in] port Port identifier
407 * ra8_isr_register(handler);
410int ra8_isr_register(int handler) { return handler; }
415 "variadic function documenting only its named parameters",
418 * @brief Formatted log.
419 * @param[in] fmt Format string.
420 * @return Bytes written.
422int ra8_logf(const char* fmt, ...) { (void)fmt; return 0; }
427 "forward declaration bare, definition documented (the correct shape)",
429static int compute_tiles(int r);
432 * @brief Probe and cache every tile's native size.
433 * @param[in] r Reader.
436static int compute_tiles(int r) { return r; }
441 "header declaration documented, definition bare (CLAUDE.md's prescribed split)",
444 * @brief Add two numbers.
445 * @param[in] lhs Left.
446 * @param[in] rhs Right.
449int add_values(int lhs, int rhs);
454 "namesake statics in different files must not merge (keyed per file)",
457 * @brief Zero a buffer.
458 * @param[in] len Length.
460static void internal_zero_bytes(int len) { (void)len; }
465 "pointer-back note WITH a real implementation note is allowed",
467/** @brief Implementation of `ra8_foo()` -- O(1) table lookup, see HUM Ch 5.2. */
468int ra8_foo(void) { return 0; }
473 "\\def block documenting a deliberately commented-out config option",
476 * \\def MBEDTLS_AES_ROM_TABLES
478 * Use precomputed AES tables stored in ROM.
480//#define MBEDTLS_AES_ROM_TABLES
483 * \\def MBEDTLS_AES_FEWER_TABLES
485 * Use less ROM/RAM for AES tables.
487//#define MBEDTLS_AES_FEWER_TABLES
492 "@var block stranded above another symbol's block, real variable left bare",
496 * @brief Captured release code.
498/** @brief Sentinel for the release code. */
499typedef enum : unsigned {
500 k_err_none = 0U, /**< None yet. */
503volatile unsigned g_release_err = 0U;
508 "'@return This function never returns.' on a [[noreturn]] void handler",
511 * @brief Park the core forever.
512 * @return This function never returns.
514[[noreturn]] void park_forever(void) { for (;;) { } }
519 "@retval on a [[noreturn]] void handler is still a contradiction",
522 * @brief Park the core forever.
523 * @return This function never returns.
524 * @retval (none) The core spins in place.
526[[noreturn]] void park_forever(void) { for (;;) { } }
531 "house-style '@return Nothing.' on a void function is not a contradiction",
534 * @brief Reset the widget.
535 * @param[in] id Widget id.
538void widget_reset(int id) { (void)id; }
543 "typedef'd anonymous struct named by its @struct tag (the C23 house shape)",
547 * @brief Parsed command line.
550 int verbose; /**< Verbosity level. */
556 "typedef'd anonymous enum named by its @enum tag",
560 * @brief Panel states.
562typedef enum : unsigned char {
563 k_lcd_state_idle = 0, /**< Idle. */
569 "struct with correctly-named @struct tag and documented members",
572 * @struct panel_timing_t
573 * @brief Panel timing.
575struct panel_timing_t {
576 int hsync; /**< Horizontal sync width. */
577 int vsync; /**< Vertical sync width. */
585def _findings_for(path: Path, cindex: ModuleType, args: list[str]) -> list[Finding]:
586 """Every finding for one fixture, through the same code the gate runs.
588 Deliberately the production helpers rather than a walk of its own. This
589 used to re-implement ``check_file``'s cursor loop inline, which meant the
590 suite could keep passing while the code the gate actually runs drifted
591 away from it -- proving the fixtures against a second implementation
595 text = path.read_text(encoding=
"ascii")
596 own = os.path.realpath(str(path))
597 tu = cindex.Index.create().parse(str(path), args=args)
599 *check_consecutive_blocks(rel, text),
600 *check_banned_boilerplate(rel, text),
601 *_check_declarations(tu, cindex, rel, own, text),
602 *check_forward_decl_blocks(tu, cindex, rel, own, text),
606def selftest() -> int:
607 """Run the synthetic fixtures in both directions."""
608 cindex = _require_libclang()
609 args = [
"-std=c23",
"-x",
"c",
"-DRA8_HOST_BUILD=1"]
611 with tempfile.TemporaryDirectory()
as td:
612 for idx, (name, src, expected)
in enumerate(SELFTEST_CASES):
613 path = Path(td) / f
"case_{idx:02d}.c"
614 path.write_text(src, encoding=
"ascii")
615 got = _findings_for(path, cindex, args)
617 codes = {f.code
for f
in got}
618 if codes != expected:
621 f
" FAIL [{idx:02d}] {name}\n"
622 f
" expected {sorted(expected) or '<clean>'}\n"
623 f
" got {sorted(codes) or '<clean>'}\n"
626 sys.stderr.write(f
" {f.code} {f.symbol}: {f.detail}\n")
630 f
"check_doc_attachment.py: selftest FAILED ({failures}/{len(SELFTEST_CASES)} cases).\n"
633 fires = sum(1
for _, _, e
in SELFTEST_CASES
if e)
634 clean = len(SELFTEST_CASES) - fires
636 f
"check_doc_attachment.py: selftest passed "
637 f
"({len(SELFTEST_CASES)} cases: {fires} must-fire, {clean} must-not-fire)."