3"""Two ways of looking at C source, and why the auditor needs both.
5The function auditor works on comment-STRIPPED source: it is looking for
6declarations, and a declaration-shaped line inside a comment is not one.
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.
15Both passes plus the brace-matching helpers live here so the two views cannot
16drift into disagreeing about where a comment ends.
19from __future__
import annotations
26def strip_comments(src: str) -> str:
27 """Remove block & line comments without changing line numbers."""
41 if ch ==
"\\" and i + 1 < n:
42 out.append(src[i + 1])
49 if c ==
"/" and i + 1 < n:
53 while i < n
and src[i] !=
"\n":
58 while i + 1 < n
and not (src[i] ==
"*" and src[i + 1] ==
"/"):
69def find_preceding_doxy(src: str, func_offset: int) -> tuple[str, bool]:
70 """Return the doxygen block immediately preceding ``func_offset``.
72 Only whitespace may separate the block from the offset; any intervening
73 code means the block documents something else and is not returned.
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.
81 while j >= 0
and src[j]
in " \t\n\r":
83 if j < 1
or src[j] !=
"/" or src[j - 1] !=
"*":
88 while k >= 1
and not (src[k] ==
"/" and src[k + 1] ==
"*"):
98 if block.startswith((
"/**",
"/*!")):
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()
124def _block_comment_style(raw: str, i: int) -> str |
None:
125 """Classify a ``/* ... */`` comment start at offset ``i``.
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.
131 if raw[i : i + 4] ==
"/**/":
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"
139def _line_comment_style(raw: str, i: int) -> str |
None:
140 """Classify a ``//`` comment start at offset ``i``.
142 Returns "inline" for ``///<`` / ``//!<``, "pre" for ``///`` / ``//!``,
143 or None for a plain ``//`` comment.
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"
151def blank_noncode(raw: str) -> tuple[str, list[tuple[int, int, str |
None]]]:
152 """Blank comment and string interiors to spaces, keeping offsets stable.
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.
170 if ch ==
"\\" and i + 1 < n:
172 out[i + 1] =
"\n" if raw[i + 1] ==
"\n" else " "
182 if c ==
"/" and i + 1 < n
and raw[i + 1] ==
"/":
183 style = _line_comment_style(raw, i)
185 while i < n
and raw[i] !=
"\n":
188 comments.append((start, i, style))
190 if c ==
"/" and i + 1 < n
and raw[i + 1] ==
"*":
191 style = _block_comment_style(raw, i)
196 while i + 1 < n
and not (raw[i] ==
"*" and raw[i + 1] ==
"/"):
209 comments.append((start, i, style))
212 return "".join(out), comments
215def _match_brace(code: str, open_idx: int) -> int |
None:
216 """Offset of the ``}`` matching the ``{`` at ``open_idx``, or None if unbalanced.
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
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.
240 Starts at ``lo + 1``, so the delimiter at ``lo`` is never itself returned.
244 if not code[i].isspace():
250def _body_depth(code: str, body_start: int, pos: int) -> int:
251 """Brace depth of ``pos`` relative to a body opening at ``body_start``.
253 0 means directly inside the aggregate body (not within a nested
254 struct/union). Comment/string braces are already blanked in ``code``.
256 return code.count(
"{", body_start + 1, pos) - code.count(
"}", body_start + 1, pos)