ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
doxy_lex.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Two ways of looking at C source, and why the auditor needs both.
4
5The function auditor works on comment-STRIPPED source: it is looking for
6declarations, and a declaration-shaped line inside a comment is not one.
7
8The member auditor cannot do that -- the documentation it audits *is* the
9comments. So it uses a blanking pass instead: comment and string-literal
10interiors become spaces (never removed) and every comment start is recorded
11with its Doxygen style. Because the blanked view is exactly as long as the
12raw source, one offset means the same character position in both, and no dual
13line/column bookkeeping is needed.
14
15Both passes plus the brace-matching helpers live here so the two views cannot
16drift into disagreeing about where a comment ends.
17"""
18
19from __future__ import annotations
20
21# Length of a doc-comment introducer ("/**", "/*!", "///", "//!"); the char at
22# this offset past the introducer decides inline (`<`) vs preceding style.
23DOC_INTRO_LEN = 3
24
25
26def strip_comments(src: str) -> str:
27 """Remove block & line comments without changing line numbers."""
28 out = []
29 i = 0
30 n = len(src)
31 while i < n:
32 c = src[i]
33 # preserve string literals
34 if c in {'"', "'"}:
35 quote = c
36 out.append(c)
37 i += 1
38 while i < n:
39 ch = src[i]
40 out.append(ch)
41 if ch == "\\" and i + 1 < n:
42 out.append(src[i + 1])
43 i += 2
44 continue
45 i += 1
46 if ch == quote:
47 break
48 continue
49 if c == "/" and i + 1 < n:
50 nxt = src[i + 1]
51 if nxt == "/":
52 # line comment, keep newlines
53 while i < n and src[i] != "\n":
54 i += 1
55 continue
56 if nxt == "*":
57 i += 2
58 while i + 1 < n and not (src[i] == "*" and src[i + 1] == "/"):
59 if src[i] == "\n":
60 out.append("\n")
61 i += 1
62 i += 2
63 continue
64 out.append(c)
65 i += 1
66 return "".join(out)
67
68
69def find_preceding_doxy(src: str, func_offset: int) -> tuple[str, bool]:
70 """Return the doxygen block immediately preceding ``func_offset``.
71
72 Only whitespace may separate the block from the offset; any intervening
73 code means the block documents something else and is not returned.
74
75 Returns ``(block_text, True)`` when a block is attached, or ``("", False)``
76 when none is -- the flag rather than an empty-string test, so a genuinely
77 empty block is still reported as present.
78 """
79 # walk backward over whitespace
80 j = func_offset - 1
81 while j >= 0 and src[j] in " \t\n\r":
82 j -= 1
83 if j < 1 or src[j] != "/" or src[j - 1] != "*":
84 return "", False
85 # find start of block
86 end = j + 1
87 k = j - 2
88 while k >= 1 and not (src[k] == "/" and src[k + 1] == "*"):
89 k -= 1
90 if k < 0:
91 return "", False
92 block = src[k:end]
93 # Doxygen blocks start with /** or /*!. Definition-side stub blocks that
94 # use /* (single asterisk) and contain "see header for full description"
95 # are also accepted as satisfying audit -- they exist purely to mark the
96 # function as "documented in header" without producing a duplicate
97 # doxygen render.
98 if block.startswith(("/**", "/*!")):
99 return block, True
100 if block.startswith("/*") and (
101 "see header for full description" in block
102 or "see surrounding code and HUM citations" in block
103 or "See the public header for the documented contract" in block
104 or "see header for the documented contract" in block
105 or "see implementation for details" in block.lower()
106 ):
107 return block, True
108 return "", False
109
110
111# =============================================================================
112# Member / enum-value / macro audit (report-only, issue #246)
113# =============================================================================
114#
115# The function auditor above works on comment-stripped source. The member
116# auditor needs to *see* the comments (that is where the documentation lives),
117# so it uses a single blanking pass that keeps offsets stable: comment and
118# string-literal interiors are replaced by spaces (never removed) and every
119# comment start is recorded with its Doxygen style. Because the blanked
120# "code-only" view is exactly as long as the raw source, an offset means the
121# same character position in both -- no dual-view line/column bookkeeping.
122
123
124def _block_comment_style(raw: str, i: int) -> str | None:
125 """Classify a ``/* ... */`` comment start at offset ``i``.
126
127 Returns "inline" for the Doxygen trailing form (``/**<`` / ``/*!<``),
128 "pre" for a preceding doc block (``/**`` / ``/*!``), or None for a plain
129 comment. The empty comment ``/**/`` is plain.
130 """
131 if raw[i : i + 4] == "/**/":
132 return None
133 if raw[i : i + DOC_INTRO_LEN] in {"/**", "/*!"}:
134 after = raw[i + DOC_INTRO_LEN] if i + DOC_INTRO_LEN < len(raw) else ""
135 return "inline" if after == "<" else "pre"
136 return None
137
138
139def _line_comment_style(raw: str, i: int) -> str | None:
140 """Classify a ``//`` comment start at offset ``i``.
141
142 Returns "inline" for ``///<`` / ``//!<``, "pre" for ``///`` / ``//!``,
143 or None for a plain ``//`` comment.
144 """
145 if raw[i : i + DOC_INTRO_LEN] in {"///", "//!"}:
146 after = raw[i + DOC_INTRO_LEN] if i + DOC_INTRO_LEN < len(raw) else ""
147 return "inline" if after == "<" else "pre"
148 return None
149
150
151def blank_noncode(raw: str) -> tuple[str, list[tuple[int, int, str | None]]]: # noqa: PLR0912, PLR0915 # char scanner, splitting hurts clarity
152 """Blank comment and string interiors to spaces, keeping offsets stable.
153
154 Returns ``(codeonly, comments)`` where ``codeonly`` is the same length as
155 ``raw`` (newlines preserved) with every comment and string-literal interior
156 replaced by spaces, and ``comments`` is a list of ``(start, end, style)``
157 tuples (style in {"pre", "inline", None}) for each comment, in source order.
158 """
159 out = list(raw)
160 comments = []
161 i = 0
162 n = len(raw)
163 while i < n:
164 c = raw[i]
165 if c in {'"', "'"}:
166 quote = c
167 i += 1
168 while i < n:
169 ch = raw[i]
170 if ch == "\\" and i + 1 < n:
171 out[i] = " "
172 out[i + 1] = "\n" if raw[i + 1] == "\n" else " "
173 i += 2
174 continue
175 if ch == quote:
176 break
177 if ch != "\n":
178 out[i] = " "
179 i += 1
180 i += 1
181 continue
182 if c == "/" and i + 1 < n and raw[i + 1] == "/":
183 style = _line_comment_style(raw, i)
184 start = i
185 while i < n and raw[i] != "\n":
186 out[i] = " "
187 i += 1
188 comments.append((start, i, style))
189 continue
190 if c == "/" and i + 1 < n and raw[i + 1] == "*":
191 style = _block_comment_style(raw, i)
192 start = i
193 out[i] = " "
194 out[i + 1] = " "
195 i += 2
196 while i + 1 < n and not (raw[i] == "*" and raw[i + 1] == "/"):
197 if raw[i] != "\n":
198 out[i] = " "
199 i += 1
200 if i + 1 < n:
201 out[i] = " "
202 out[i + 1] = " "
203 i += 2
204 else: # unterminated block comment: blank the tail
205 while i < n:
206 if raw[i] != "\n":
207 out[i] = " "
208 i += 1
209 comments.append((start, i, style))
210 continue
211 i += 1
212 return "".join(out), comments
213
214
215def _match_brace(code: str, open_idx: int) -> int | None:
216 """Offset of the ``}`` matching the ``{`` at ``open_idx``, or None if unbalanced.
217
218 Returns None rather than raising on an unterminated brace: this runs over
219 partially-valid source, and an unbalanced file should be skipped, not
220 crash the sweep.
221 """
222 depth = 0
223 i = open_idx
224 n = len(code)
225 while i < n:
226 ch = code[i]
227 if ch == "{":
228 depth += 1
229 elif ch == "}":
230 depth -= 1
231 if depth == 0:
232 return i
233 i += 1
234 return None
235
236
237def _first_code_offset(code: str, lo: int, hi: int) -> int | None:
238 """First non-whitespace offset in the exclusive range ``(lo, hi)``, else None.
239
240 Starts at ``lo + 1``, so the delimiter at ``lo`` is never itself returned.
241 """
242 i = lo + 1
243 while i < hi:
244 if not code[i].isspace():
245 return i
246 i += 1
247 return None
248
249
250def _body_depth(code: str, body_start: int, pos: int) -> int:
251 """Brace depth of ``pos`` relative to a body opening at ``body_start``.
252
253 0 means directly inside the aggregate body (not within a nested
254 struct/union). Comment/string braces are already blanked in ``code``.
255 """
256 return code.count("{", body_start + 1, pos) - code.count("}", body_start + 1, pos)