ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
docattach_model.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""What ``check_doc_attachment.py`` finds, and the shapes it reasons over.
4
5The finding codes, the Doxygen tag grammar, and the three records the two
6passes exchange: a :class:`Finding`, a :class:`DocBlock` (one comment and the
7line it attaches to) and :class:`DocTags` (what that comment claims).
8
9Kept apart from both passes so the lexical pass and the AST pass cannot drift
10into disagreeing about what a doc block is -- the entire gate rests on the two
11agreeing.
12"""
13
14from __future__ import annotations
15
16import re
17from dataclasses import dataclass, field
18
19# ---------------------------------------------------------------------------
20# Finding codes. Ordered by signal strength: a DOC001 is near-certain paste
21# evidence, a DOC005 is strong, a DOC004 is the owner's literal complaint.
22# ---------------------------------------------------------------------------
23CODE_HELP = {
24 "DOC001": "@param names a parameter the signature does not have",
25 "DOC002": "partially documented signature -- some parameters have no @param",
26 "DOC003": "@return/@retval on a function returning void",
27 "DOC004": "two doc blocks in a row with no declaration between them",
28 "DOC005": "block names a different symbol than the one it is attached to",
29 "DOC006": "block sits on a forward declaration whose definition is bare",
30 "DOC007": "banned pointer-only definition-site boilerplate",
31}
32
33# ---------------------------------------------------------------------------
34# Doxygen tag scanning. Doxygen accepts both `@tag` and `\tag`.
35# ---------------------------------------------------------------------------
36_T = r"[@\\]"
37
38#: ``@param[in] name`` / ``@param[in,out] a,b`` / ``@param name``.
39PARAM_RE = re.compile(
40 _T + r"param\s*(?:\‍[[^\‍]]*\‍])?\s+"
41 r"([A-Za-z_][A-Za-z_0-9]*(?:\s*,\s*[A-Za-z_][A-Za-z_0-9]*)*)"
42)
43RETURN_RE = re.compile(_T + r"returns?\b")
44RETVAL_RE = re.compile(_T + r"retval\b")
45COPY_RE = re.compile(_T + r"copy(?:doc|details|brief)\b")
46
47#: Explicit "this block documents symbol X" tags. These are unambiguous: if
48#: the tag names X and the block is attached to Y, one of the two is wrong.
49EXPLICIT_REF_RE = re.compile(
50 _T + r"(fn|struct|enum|union|def|var|typedef|class)\s+([A-Za-z_][A-Za-z_0-9]*)"
51)
52
53#: The CLAUDE.md-sanctioned definition-site single-line form:
54#: /** @brief Implementation of `ra8_err_to_str()` -- linear-scan lookup. */
55#: The backticked name is a machine-checkable claim about which function this
56#: is; a pasted block carries the wrong one.
57IMPL_OF_RE = re.compile(r"[Ii]mplementation of\s+`([A-Za-z_][A-Za-z_0-9]*)\s*\‍(\‍)`")
58
59#: Blocks that legitimately stand alone (documentation structure, not a
60#: symbol). A block of this kind directly above another block is normal and
61#: must never be reported as a duplicate.
62STANDALONE_TAG_RE = re.compile(
63 _T + r"(file|dir|mainpage|page|subpage|section|subsection|defgroup|addtogroup"
64 r"|ingroup|weakgroup|name|cond|endcond|example|internal|endinternal"
65 r"|copyright|brief\s*$)"
66)
67
68#: Doxygen grouping markers -- ``/** @{ */`` and ``/** @} */`` sit between two
69#: real blocks all the time.
70GROUP_MARKER_RE = re.compile(r"[@\\][{}]")
71
72#: A commented-out preprocessor directive: the config-header idiom for a
73#: documented-but-disabled build option (``//#define MBEDTLS_FOO``). It is a
74#: real subject for the block above it even though it is lexically a comment.
75COMMENTED_DEFINE_RE = re.compile(r"\s*(?://+|/\*)\s*#\s*(?:define|undef)\b")
76
77#: An explicit statement that a function yields no value -- either because it
78#: returns void ("Nothing.", "None.") or because it never returns at all
79#: ("This function never returns.", on the ``[[noreturn]] void`` handlers and
80#: park loops). Doxygen prefers these be omitted, but they are a deliberate,
81#: self-consistent house style here and they state the truth about the
82#: signature, so they are not a contradiction. ``@retval`` on a void function
83#: is different: it enumerates return *values* that cannot exist.
84RETURN_NOTHING_RE = re.compile(
85 _T + r"returns?\s+"
86 r"(?:nothing|none|void|n/?a|no value"
87 r"|(?:this function |the function )?(?:never returns|does not return|no return))"
88 r"\b[.\s]*",
89 re.IGNORECASE,
90)
91
92#: CLAUDE.md "BANNED (rejected in review)" definition-site boilerplate: a
93#: comment whose only content is a pointer back at the header.
94BANNED_BOILERPLATE_RE = re.compile(
95 r"(?:see (?:the )?(?:public )?header for "
96 r"(?:the |full )?(?:documented )?(?:contract|description)"
97 r"|see header for full contract)",
98 re.IGNORECASE,
99)
100
101
102@dataclass(frozen=True)
103class Finding:
104 """One gate finding."""
105
106 path: str
107 line: int
108 code: str
109 symbol: str
110 detail: str
111
112 def render(self) -> str:
113 """One aligned report line for this finding.
114
115 The leading two spaces are part of the format: findings are printed
116 under a summary header, and the indent is what visually subordinates
117 them to it.
118 """
119 return f" {self.path}:{self.line} {self.code} {self.symbol} -- {self.detail}"
120
121
122@dataclass
123class DocBlock:
124 """A lexically-extracted ``/** ... */`` or ``/*! ... */`` block."""
125
126 start_line: int
127 end_line: int
128 text: str
129 #: True for blocks that document the file / a group rather than a symbol.
130 standalone: bool = False
131 #: True for the ``/**<`` "documents the *preceding* member" form. These
132 #: are trailing comments on struct fields and enum values; a run of them on
133 #: consecutive lines is the normal shape and must never read as duplicates.
134 trailing: bool = False
135
136
137@dataclass
138class DocTags:
139 """The claims a doc block makes, extracted once."""
140
141 params: list[str] = field(default_factory=list)
142 has_return: bool = False
143 has_retval: bool = False
144 has_copy: bool = False
145 explicit_refs: list[tuple[str, str]] = field(default_factory=list)
146 impl_of: str | None = None
147
148
149def parse_tags(block_text: str) -> DocTags:
150 """Extract the claims a block makes.
151
152 ``@code``/``@endcode`` bodies are removed first: a usage example may
153 legitimately mention another function's parameters, and reading tags out of
154 one produces false positives.
155 """
156 body = re.sub(r"[@\\]code\b.*?[@\\]endcode\b", " ", block_text, flags=re.DOTALL)
157 params: list[str] = []
158 for m in PARAM_RE.finditer(body):
159 params.extend(p.strip() for p in m.group(1).split(","))
160 # A bare "@return Nothing." on a void function states the truth; only a
161 # @return that promises an actual value contradicts a void signature.
162 return DocTags(
163 params=params,
164 has_return=bool(RETURN_RE.search(RETURN_NOTHING_RE.sub(" ", body))),
165 has_retval=bool(RETVAL_RE.search(body)),
166 has_copy=bool(COPY_RE.search(body)),
167 explicit_refs=EXPLICIT_REF_RE.findall(body),
168 impl_of=(m.group(1) if (m := IMPL_OF_RE.search(body)) else None),
169 )