ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_comment_lex.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Syntax-aware comment extraction for the suppression inventory."""
4
5from __future__ import annotations
6
7import io
8import re
9import tokenize
10from dataclasses import dataclass
11from pathlib import Path
12
13from suppression_catalog import language
14from suppression_hash_lex import hash_lines
15from suppression_model import Finding
16
17
18@dataclass(frozen=True)
19class Comment:
20 """One lexical comment with its source location."""
21
22 line: int
23 column: int
24 text: str
25
26
27@dataclass
28class CLexState:
29 """Cross-line lexical state for C comments, strings, and raw strings."""
30
31 in_block_comment: bool = False
32 in_line_comment: bool = False
33 quote: str = ""
34 raw_end: str = ""
35
36
37def _python_comments(text: str) -> tuple[list[Comment], list[Finding]]:
38 """Extract real Python comments while ignoring strings and docstrings."""
39 comments: list[Comment] = []
40 try:
41 tokens = tokenize.generate_tokens(io.StringIO(text).readline)
42 comments.extend(
43 Comment(token.start[0], token.start[1] + 1, token.string[1:])
44 for token in tokens
45 if token.type == tokenize.COMMENT
46 )
47 except (IndentationError, tokenize.TokenError) as exc:
48 return comments, [Finding("python-tokenize", str(exc))]
49 return comments, []
50
51
52def _raw_string_end(raw: str, index: int) -> tuple[str, int] | None:
53 """Return a C++ raw-string terminator and body start at one source offset."""
54 if index > 0 and (raw[index - 1].isalnum() or raw[index - 1] == "_"):
55 return None
56 match = re.match(r'(?:u8|u|U|L)?R"([^ ()\\\t]{0,16})\‍(', raw[index:])
57 if match is None:
58 return None
59 return ")" + match.group(1) + '"', index + match.end()
60
61
62def _continue_block_comment(
63 raw: str, line_no: int, index: int, state: CLexState, comments: list[Comment]
64) -> int:
65 """Consume one line fragment of an active C block comment."""
66 end = raw.find("*/", index)
67 stop = len(raw) if end < 0 else end
68 comments.append(Comment(line_no, index + 1, raw[index:stop]))
69 if end < 0:
70 return len(raw)
71 state.in_block_comment = False
72 return end + 2
73
74
75def _continue_raw_string(raw: str, index: int, state: CLexState) -> int:
76 """Consume one line fragment of an active C++ raw string."""
77 end = raw.find(state.raw_end, index)
78 if end < 0:
79 return len(raw)
80 index = end + len(state.raw_end)
81 state.raw_end = ""
82 return index
83
84
85def _continue_quoted_string(raw: str, index: int, state: CLexState) -> int:
86 """Consume one token from an active ordinary C string or character literal."""
87 char = raw[index]
88 if char == "\\":
89 return index + 2
90 if char == state.quote:
91 state.quote = ""
92 return index + 1
93
94
95def _continue_c_state(
96 raw: str,
97 line_no: int,
98 index: int,
99 state: CLexState,
100 comments: list[Comment],
101) -> tuple[int, bool]:
102 """Consume an active block comment, raw string, or ordinary string state."""
103 if state.in_block_comment:
104 return _continue_block_comment(raw, line_no, index, state, comments), True
105 if state.raw_end:
106 return _continue_raw_string(raw, index, state), True
107 if state.quote:
108 return _continue_quoted_string(raw, index, state), True
109 return index, False
110
111
112def _scan_c_line(raw: str, line_no: int, state: CLexState) -> list[Comment]:
113 """Extract C/C++ comments from one line and carry block-comment state."""
114 comments: list[Comment] = []
115 if state.in_line_comment:
116 comments.append(Comment(line_no, 1, raw))
117 state.in_line_comment = raw.endswith("\\")
118 return comments
119 index = 0
120 while index < len(raw):
121 index, handled = _continue_c_state(raw, line_no, index, state, comments)
122 if handled:
123 continue
124 char = raw[index]
125 raw_string = _raw_string_end(raw, index)
126 if raw_string is not None:
127 state.raw_end, index = raw_string
128 elif char in {'"', "'"}:
129 state.quote = char
130 index += 1
131 elif raw.startswith("//", index):
132 comments.append(Comment(line_no, index + 1, raw[index + 2 :]))
133 state.in_line_comment = raw.endswith("\\")
134 return comments
135 elif raw.startswith("/*", index):
136 state.in_block_comment = True
137 index += 2
138 else:
139 index += 1
140 return comments
141
142
143def _c_comments(text: str) -> tuple[list[Comment], list[Finding]]:
144 """Extract lexical C/C++ comments without matching string literals."""
145 comments: list[Comment] = []
146 state = CLexState()
147 for line_no, raw in enumerate(text.splitlines(), start=1):
148 comments.extend(_scan_c_line(raw, line_no, state))
149 if state.quote and not raw.endswith("\\"):
150 state.quote = ""
151 findings: list[Finding] = []
152 if state.in_block_comment:
153 findings.append(Finding("unterminated-comment", "unterminated C block comment"))
154 if state.in_line_comment:
155 findings.append(Finding("unterminated-line-comment-splice", "line splice reaches EOF"))
156 if state.raw_end:
157 findings.append(Finding("unterminated-string", "unterminated C++ raw string"))
158 if state.quote:
159 findings.append(Finding("unterminated-string", "unterminated backslash-spliced string"))
160 return comments, findings
161
162
163def _hash_comments(path: str, text: str) -> tuple[list[Comment], list[Finding]]:
164 """Extract comments from syntax-aware shell, CMake, YAML, and config lines."""
165 lines, findings = hash_lines(path, text)
166 comments = [
167 Comment(line.line, line.comment_column, line.comment) for line in lines if line.comment
168 ]
169 return comments, findings
170
171
172def _html_comments(text: str) -> tuple[list[Comment], list[Finding]]:
173 """Extract single- and multi-line HTML comments used by policy markers."""
174 comments: list[Comment] = []
175 buffer: list[str] = []
176 start_line = 0
177 start_column = 0
178 for line_no, raw in enumerate(text.splitlines(), start=1):
179 index = 0
180 while index < len(raw):
181 if buffer:
182 end = raw.find("-->", index)
183 if end < 0:
184 buffer.append(raw[index:])
185 break
186 buffer.append(raw[index:end])
187 comments.append(Comment(start_line, start_column, " ".join(buffer)))
188 buffer = []
189 index = end + 3
190 continue
191 start = raw.find("<!--", index)
192 if start < 0:
193 break
194 end = raw.find("-->", start + 4)
195 if end >= 0:
196 comments.append(Comment(line_no, start + 1, raw[start + 4 : end]))
197 index = end + 3
198 else:
199 start_line = line_no
200 start_column = start + 1
201 buffer = [raw[start + 4 :]]
202 break
203 findings = []
204 if buffer:
205 message = f"unterminated HTML comment starting at line {start_line}"
206 findings.append(Finding("unterminated-html-comment", message, line=start_line))
207 return comments, findings
208
209
210def _mask_markdown_code(text: str) -> str:
211 """Blank fenced and inline code while preserving source coordinates."""
212 masked: list[str] = []
213 fence = ""
214 inline_ticks = 0
215 for raw in text.splitlines(keepends=True):
216 content = raw.rstrip("\r\n")
217 newline = raw[len(content) :]
218 fence_match = re.match(r"^ {0,3}(`{3,}|~{3,})", content)
219 if fence:
220 closes = re.match(rf"^ {{0,3}}{re.escape(fence[0])}{{{len(fence)},}}\s*$", content)
221 if closes:
222 fence = ""
223 masked.append(" " * len(content) + newline)
224 continue
225 if not inline_ticks and fence_match:
226 fence = fence_match.group(1)
227 masked.append(" " * len(content) + newline)
228 continue
229 chars = list(content)
230 index = 0
231 while index < len(content):
232 if inline_ticks:
233 closing = "`" * inline_ticks
234 end = content.find(closing, index)
235 limit = len(content) if end < 0 else end + inline_ticks
236 chars[index:limit] = " " * (limit - index)
237 index = limit
238 if end >= 0:
239 inline_ticks = 0
240 continue
241 if content[index] != "`":
242 index += 1
243 continue
244 end = index
245 while end < len(content) and content[end] == "`":
246 end += 1
247 inline_ticks = end - index
248 chars[index:end] = " " * inline_ticks
249 index = end
250 masked.append("".join(chars) + newline)
251 return "".join(masked)
252
253
254def extract_comments(path: str, text: str) -> tuple[list[Comment], list[Finding]]:
255 """Dispatch to the lexical comment extractor appropriate for one file."""
256 first_line = text.partition("\n")[0]
257 kind = language(path, first_line)
258 if kind == "python":
259 comments, findings = _python_comments(text)
260 elif kind == "c-family":
261 comments, findings = _c_comments(text)
262 elif kind == "hash":
263 comments, findings = _hash_comments(path, text)
264 else:
265 comments, findings = [], []
266 if kind == "text":
267 html_source = _mask_markdown_code(text) if Path(path).suffix == ".md" else text
268 html_comments, html_findings = _html_comments(html_source)
269 comments.extend(html_comments)
270 findings.extend(html_findings)
271 return comments, findings