ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
docattach_lex.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""The checks that need only the source text, not a parse.
4
5Two of this gate's seven findings are answerable lexically, and answering them
6without libclang is deliberate: DOC004 (two doc blocks in a row with nothing
7between them) and DOC007 (banned definition-site boilerplate) are statements
8about the *comment stream*, which a parse discards.
9
10Everything here therefore works on text that has had comments and literals
11blanked out but line structure preserved, so a brace inside a string or a
12declaration-looking line inside a comment cannot be mistaken for code.
13"""
14
15from __future__ import annotations
16
17import itertools
18import re
19
20from docattach_model import (
21 _T,
22 BANNED_BOILERPLATE_RE,
23 COMMENTED_DEFINE_RE,
24 EXPLICIT_REF_RE,
25 GROUP_MARKER_RE,
26 STANDALONE_TAG_RE,
27 DocBlock,
28 Finding,
29)
30
31
32# ---------------------------------------------------------------------------
33# Lexical pass
34# ---------------------------------------------------------------------------
35def _blank_comments_and_literals(text: str) -> str:
36 """Blank every comment and string/char literal, preserving line structure.
37
38 Length and newline positions are preserved so the result can be indexed by
39 the same line numbers as the original. Blanking (rather than deleting) is
40 what lets the DOC004 "is there code between these two blocks" test look at
41 code only -- an earlier version advanced past comments without clearing
42 them, so a member's own trailing ``/**< ... */`` counted as intervening
43 code and the check silently never fired on real member runs.
44 """
45 out = list(text)
46
47 def blank(lo: int, hi: int) -> None:
48 for k in range(lo, min(hi, len(text))):
49 if text[k] != "\n":
50 out[k] = " "
51
52 i, n = 0, len(text)
53 while i < n:
54 ch = text[i]
55 if ch in {'"', "'"}:
56 quote = ch
57 start = i
58 i += 1
59 while i < n:
60 if text[i] == "\\" and i + 1 < n:
61 i += 2
62 continue
63 if text[i] == quote:
64 i += 1
65 break
66 i += 1
67 blank(start, i)
68 continue
69 if ch == "/" and i + 1 < n and text[i + 1] == "/":
70 start = i
71 while i < n and text[i] != "\n":
72 i += 1
73 blank(start, i)
74 continue
75 if ch == "/" and i + 1 < n and text[i + 1] == "*":
76 start = i
77 i += 2
78 while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
79 i += 1
80 i += 2
81 blank(start, i)
82 continue
83 i += 1
84 return "".join(out)
85
86
87def _skip_literal(text: str, i: int, line: int) -> tuple[int, int]:
88 """Advance past the string/char literal starting at ``i``."""
89 quote = text[i]
90 n = len(text)
91 i += 1
92 while i < n:
93 if text[i] == "\\" and i + 1 < n:
94 i += 2
95 continue
96 if text[i] == quote:
97 i += 1
98 break
99 if text[i] == "\n":
100 line += 1
101 i += 1
102 return i, line
103
104
105def _scan_block_comment(text: str, i: int, line: int) -> tuple[int, int, bool]:
106 """Advance past the ``/* ... */`` at ``i``; report whether it is a doc block."""
107 n = len(text)
108 is_doc = i + 2 < n and text[i + 2] in {"*", "!"}
109 # `/**/` and `/***/` are terminators, not documentation.
110 if is_doc and i + 3 < n and text[i + 2] == "*" and text[i + 3] == "/":
111 is_doc = False
112 j = i + 2
113 while j + 1 < n and not (text[j] == "*" and text[j + 1] == "/"):
114 if text[j] == "\n":
115 line += 1
116 j += 1
117 return j + 2, line, is_doc
118
119
120def extract_doc_blocks(text: str) -> list[DocBlock]:
121 """Return every ``/**`` / ``/*!`` block in ``text``, in source order.
122
123 Blocks inside string literals are skipped. Single-line ``///`` and ``//!``
124 comments are captured too so the duplicate check sees them.
125 """
126 blocks: list[DocBlock] = []
127 i, n = 0, len(text)
128 line = 1
129 while i < n:
130 ch = text[i]
131 if ch == "\n":
132 line += 1
133 i += 1
134 continue
135 if ch in {'"', "'"}:
136 i, line = _skip_literal(text, i, line)
137 continue
138 if ch == "/" and i + 1 < n and text[i + 1] == "*":
139 start_line = line
140 end, line, is_doc = _scan_block_comment(text, i, line)
141 if is_doc:
142 blocks.append(DocBlock(start_line, line, text[i:end]))
143 i = end
144 continue
145 if ch == "/" and i + 1 < n and text[i + 1] == "/":
146 is_doc = i + 2 < n and text[i + 2] in {"/", "!"}
147 chunk_start = i
148 while i < n and text[i] != "\n":
149 i += 1
150 if is_doc:
151 blocks.append(DocBlock(line, line, text[chunk_start:i]))
152 continue
153 i += 1
154 for b in blocks:
155 b.standalone = bool(STANDALONE_TAG_RE.search(b.text) or GROUP_MARKER_RE.search(b.text))
156 b.trailing = bool(re.match(r"/(?:\*[*!]|//|/!)<", b.text))
157 return blocks
158
159
160def check_consecutive_blocks(path: str, text: str) -> list[Finding]:
161 """DOC004 -- two doc blocks in a row with nothing declared between them.
162
163 This is the owner's literal complaint and the shape that makes a presence
164 gate *reward* the defect. A ``@file`` / ``@defgroup`` / ``@{`` block
165 followed by a real block is normal and is exempted via ``standalone``.
166 """
167 findings: list[Finding] = []
168 blocks = extract_doc_blocks(text)
169 lines = _blank_comments_and_literals(text).splitlines()
170 raw_lines = text.splitlines()
171 for prev, cur in itertools.pairwise(blocks):
172 if prev.standalone or prev.trailing or cur.trailing:
173 continue
174 # A block carrying an explicit @def / @fn / @struct / ... tag is
175 # resolved by Doxygen *by name*, not by position, so it does not need a
176 # following declaration at all. Config headers rely on this to
177 # document deliberately-disabled options:
178 # /** \def MBEDTLS_AES_ROM_TABLES ... */
179 # //#define MBEDTLS_AES_ROM_TABLES
180 # The commented-out define is not code, so a purely positional test
181 # calls that a duplicate. Accept the block when the very next non-blank
182 # source line mentions the symbol it names -- which still leaves a block
183 # whose symbol is declared further down, past another symbol, reported.
184 refs = [ref for _, ref in EXPLICIT_REF_RE.findall(prev.text)]
185 if refs:
186 following = next(
187 (ln for ln in raw_lines[prev.end_line : cur.start_line - 1] if ln.strip()),
188 "",
189 ) or next((ln for ln in raw_lines[prev.end_line :] if ln.strip()), "")
190 if any(re.search(r"\b" + re.escape(r) + r"\b", following) for r in refs):
191 continue
192 # A *commented-out* preprocessor directive is the config-header idiom
193 # for "here is an option, documented and deliberately off":
194 # /** Enable the Everest ECDH backend ... */
195 # //#define MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED
196 # The block documents that option, so it is attached -- but the
197 # directive is lexically a comment, so `_blank_comments_and_literals`
198 # erases it and a purely positional test sees an empty gap and calls
199 # the block a duplicate. Consult the raw text for this one shape.
200 # Deliberately narrow: only a commented-out #define / #undef counts,
201 # so two genuinely adjacent blocks are still reported.
202 raw_between = raw_lines[prev.end_line : cur.start_line - 1]
203 if any(COMMENTED_DEFINE_RE.match(seg) for seg in raw_between):
204 continue
205 # Anything but whitespace between the two blocks means the first one is
206 # attached to something -- not a duplicate.
207 between = lines[prev.end_line : cur.start_line - 1]
208 if any(seg.strip() for seg in between):
209 continue
210 findings.append(
211 Finding(
212 path,
213 prev.start_line,
214 "DOC004",
215 "(block)",
216 f"doc block is followed by another doc block at line {cur.start_line} "
217 "with no declaration between them; the first documents nothing",
218 )
219 )
220 return findings
221
222
223def check_banned_boilerplate(path: str, text: str) -> list[Finding]:
224 """DOC007 -- CLAUDE.md's banned pointer-only definition-site comment.
225
226 Only fires on blocks whose *entire* informational content is the pointer.
227 A block that says "see the header for the full contract" and then adds a
228 real implementation note is allowed by CLAUDE.md, so the presence of any
229 other Doxygen tag or a ``--`` note clause clears it.
230 """
231 findings: list[Finding] = []
232 for block in extract_doc_blocks(text):
233 if not BANNED_BOILERPLATE_RE.search(block.text):
234 continue
235 body = re.sub(r"^\s*/\*+<?|\*+/\s*$", " ", block.text)
236 body = re.sub(r"^\s*\*", " ", body, flags=re.MULTILINE)
237 tags = set(re.findall(_T + r"([a-z]+)", body))
238 # @brief alone is still boilerplate; any other tag means real content.
239 if tags - {"brief"}:
240 continue
241 # Strip everything the idiom is *allowed* to contain -- the @brief tag,
242 # the "Implementation of <symbol>()" lead-in (backticked or not), the
243 # pointer phrase itself, and punctuation. Whatever survives is the
244 # real implementation note CLAUDE.md requires. If nothing survives,
245 # the comment says only "see the header" and is banned.
246 residue = BANNED_BOILERPLATE_RE.sub(" ", body)
247 residue = re.sub(_T + r"brief", " ", residue)
248 residue = re.sub(
249 r"[Ii]mplementation of\s+`?[A-Za-z_][A-Za-z_0-9]*`?(?:\s*\‍(\‍))?`?", " ", residue
250 )
251 residue = re.sub(r"[`(){}.,;:*/\s-]", "", residue)
252 if residue:
253 continue
254 findings.append(
255 Finding(
256 path,
257 block.start_line,
258 "DOC007",
259 "(block)",
260 "pointer-only definition-site boilerplate (CLAUDE.md bans it): the comment "
261 "carries no information the signature does not. Delete it, or replace it "
262 "with the single-line form carrying a real implementation note",
263 )
264 )
265 return findings
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157