ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_c_control_scan.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Parse MC/DC deactivations and native C/C++ test-skip controls."""
4
5from __future__ import annotations
6
7import re
8from dataclasses import dataclass
9
10from suppression_catalog import ownership
11from suppression_comment_lex import Comment
12from suppression_model import Finding, Suppression
13
14MCDC_COMMENT_RE = re.compile(r"mcdc-deactivated\s*:\s*(?P<reason>.+)", re.IGNORECASE)
15MCDC_COMMENT_HINT_RE = re.compile(r"^mcdc-deactivated\b", re.IGNORECASE)
16MCDC_MACRO_RE = re.compile(r"\bRA8_MCDC_(?:DEACTIVATED|EXEMPT|OK)\s*\‍(")
17MCDC_DEACTIVATED_INVOCATION_RE = re.compile(
18 r'RA8_MCDC_DEACTIVATED\s*\‍(\s*(?P<strings>"(?:\\.|[^"\\])*"'
19 r'(?:\s*"(?:\\.|[^"\\])*")*)\s*\‍)\Z'
20)
21COMPOUND_RE = re.compile(r"&&|\|\|")
22GTEST_RE = re.compile(r"\bGTEST_SKIP\s*\‍(")
23UNITY_RE = re.compile(r"\bTEST_IGNORE(?:_MESSAGE)?\s*\‍(")
24C_CONTROL_HINT_RE = re.compile(
25 r"mcdc-deactivated|RA8_MCDC_(?:DEACTIVATED|EXEMPT|OK)|GTEST_SKIP|TEST_IGNORE",
26 re.IGNORECASE,
27)
28PAIRING_WINDOW = 8
29RAW_STRING_RE = re.compile(r'(?:u8|u|U|L)?R"([^ ()\\\t]{0,16})\‍(')
30
31
32@dataclass
33class MaskState:
34 """Cross-line state for blanking C/C++ comments and literals."""
35
36 quote: str = ""
37 block_comment: bool = False
38 line_comment: bool = False
39 raw_end: str = ""
40
41
42def _blank(chars: list[str], start: int, stop: int) -> None:
43 """Blank a source range without changing newline coordinates."""
44 for offset in range(start, stop):
45 if chars[offset] != "\n":
46 chars[offset] = " "
47
48
49def _consume_masked(text: str, chars: list[str], index: int, state: MaskState) -> int | None:
50 """Consume one token while already inside a non-code lexical state."""
51 char = chars[index]
52 if state.line_comment:
53 state.line_comment = char != "\n"
54 if char != "\n":
55 chars[index] = " "
56 return index + 1
57 if state.block_comment:
58 if text.startswith("*/", index):
59 _blank(chars, index, index + 2)
60 state.block_comment = False
61 stop = index + 2
62 else:
63 _blank(chars, index, index + 1)
64 stop = index + 1
65 return stop
66 if state.raw_end:
67 end = text.find(state.raw_end, index)
68 stop = len(chars) if end < 0 else end + len(state.raw_end)
69 _blank(chars, index, stop)
70 state.raw_end = "" if end >= 0 else state.raw_end
71 return stop
72 if not state.quote:
73 return None
74 if char == "\\" and index + 1 < len(chars):
75 _blank(chars, index, index + 2)
76 return index + 2
77 if char == state.quote:
78 state.quote = ""
79 _blank(chars, index, index + 1)
80 return index + 1
81
82
83def _open_masked(text: str, chars: list[str], index: int, state: MaskState) -> int | None:
84 """Open a comment or literal state at one active-code offset."""
85 raw_match = RAW_STRING_RE.match(text, index)
86 if raw_match is not None:
87 state.raw_end = ")" + raw_match.group(1) + '"'
88 stop = raw_match.end()
89 _blank(chars, index, stop)
90 return stop
91 if text.startswith("//", index):
92 _blank(chars, index, index + 2)
93 state.line_comment = True
94 return index + 2
95 if text.startswith("/*", index):
96 _blank(chars, index, index + 2)
97 state.block_comment = True
98 return index + 2
99 if chars[index] in {'"', "'"}:
100 state.quote = chars[index]
101 chars[index] = " "
102 return index + 1
103 return None
104
105
106def _mask_noncode(text: str) -> str:
107 """Blank comments and literals while preserving code coordinates."""
108 chars = list(text)
109 state = MaskState()
110 index = 0
111 while index < len(chars):
112 next_index = _consume_masked(text, chars, index, state)
113 if next_index is None:
114 next_index = _open_masked(text, chars, index, state)
115 index = index + 1 if next_index is None else next_index
116 return "".join(chars)
117
118
119def _target_decision(code_lines: list[str], marker_line: int) -> int | None:
120 """Pair one marker with its same-line or immediately-following decision."""
121 same_line = code_lines[marker_line - 1]
122 if COMPOUND_RE.search(same_line):
123 return marker_line
124 statement = ""
125 for line_no in range(marker_line + 1, min(len(code_lines), marker_line + PAIRING_WINDOW) + 1):
126 code = code_lines[line_no - 1].strip()
127 if not code:
128 continue
129 statement += " " + code
130 if COMPOUND_RE.search(statement):
131 return line_no
132 if ";" in code or "{" in code or "}" in code:
133 break
134 return None
135
136
137def _mcdc_record(path: str, comment: Comment, reason: str, target_line: int) -> Suppression:
138 """Build one decision-scoped MC/DC deactivation row."""
139 return Suppression(
140 path,
141 comment.line,
142 comment.column,
143 "mcdc-deactivation",
144 "llvm-cov",
145 "deactivated-condition",
146 "mcdc-deactivated",
147 f"decision-line:{target_line}",
148 reason,
149 "inline-comment",
150 ownership(path),
151 (),
152 evidence=(f"decision-line:{target_line}", "standard:DO-178C-6.4.4.3"),
153 recommendation="revalidate-invariant",
154 )
155
156
157def _scan_mcdc_comments(
158 path: str, code_lines: list[str], comments: list[Comment]
159) -> tuple[list[Suppression], list[Finding]]:
160 """Parse exact legacy marker grammar and enforce one-to-one decision pairing."""
161 records: list[Suppression] = []
162 findings: list[Finding] = []
163 targets: dict[int, int] = {}
164 for comment in comments:
165 if comment.text.lstrip().startswith("*"):
166 continue
167 body = comment.text.strip().lstrip("*").strip()
168 match = MCDC_COMMENT_RE.fullmatch(body)
169 if match is None:
170 if MCDC_COMMENT_HINT_RE.match(body):
171 findings.append(Finding("malformed-mcdc-deactivation", body, path, comment.line))
172 continue
173 reason = match.group("reason").strip()
174 if not reason:
175 findings.append(Finding("blank-mcdc-reason", body, path, comment.line))
176 continue
177 target_line = _target_decision(code_lines, comment.line)
178 if target_line is None:
179 findings.append(
180 Finding(
181 "unpaired-mcdc-deactivation",
182 "marker does not immediately govern a compound decision",
183 path,
184 comment.line,
185 )
186 )
187 continue
188 if target_line in targets:
189 message = f"decision line {target_line} already governed by line {targets[target_line]}"
190 findings.append(Finding("duplicate-mcdc-deactivation", message, path, comment.line))
191 continue
192 targets[target_line] = comment.line
193 record = _mcdc_record(path, comment, reason, target_line)
194 records.append(record)
195 if record.owner != "first-party":
196 findings.append(
197 Finding(
198 "mcdc-owner-mismatch",
199 f"deactivation appears in {record.owner} code",
200 path,
201 comment.line,
202 )
203 )
204 return records, findings
205
206
207def _balanced_invocation_end(code: str, open_index: int) -> int | None:
208 """Return the exclusive end of one balanced call in masked C/C++ code."""
209 if open_index >= len(code) or code[open_index] != "(":
210 return None
211 depth = 0
212 for index in range(open_index, len(code)):
213 if code[index] == "(":
214 depth += 1
215 elif code[index] == ")":
216 depth -= 1
217 if depth == 0:
218 return index + 1
219 return None
220
221
222def _macro_reason(raw: str, code: str, macro: re.Match[str]) -> str | None:
223 """Return the literal reason bound to one exact balanced annotation call."""
224 end = _balanced_invocation_end(code, macro.end() - 1)
225 if end is None:
226 return None
227 invocation = raw[macro.start() : end]
228 invocation_match = MCDC_DEACTIVATED_INVOCATION_RE.fullmatch(invocation)
229 if invocation_match is None:
230 return None
231 parts = re.findall(r'"((?:\\.|[^"\\])*)"', invocation_match.group("strings"))
232 return " ".join(part.replace(r"\"", '"').replace(r"\\", "\\") for part in parts).strip()
233
234
235def _scan_mcdc_macros(
236 path: str, text_lines: list[str], code_lines: list[str]
237) -> tuple[list[Suppression], list[Finding]]:
238 """Inventory active annotation macros and reject aliases or nonliteral reasons."""
239 records: list[Suppression] = []
240 findings: list[Finding] = []
241 for line_no, code in enumerate(code_lines, start=1):
242 if code.lstrip().startswith("#define"):
243 continue
244 for match in MCDC_MACRO_RE.finditer(code):
245 macro = re.match(r"RA8_MCDC_[A-Z]+", match.group(0))
246 name = macro.group(0) if macro is not None else ""
247 if name != "RA8_MCDC_DEACTIVATED":
248 findings.append(Finding("unknown-mcdc-macro", name, path, line_no))
249 continue
250 reason = _macro_reason(text_lines[line_no - 1], code, match)
251 if reason is None:
252 findings.append(
253 Finding(
254 "malformed-mcdc-macro",
255 "reason must be one or more string literals on the annotation line",
256 path,
257 line_no,
258 )
259 )
260 continue
261 concerns = ("blank-reason",) if not reason else ("broad-function-scope",)
262 records.append(
263 Suppression(
264 path,
265 line_no,
266 match.start() + 1,
267 "mcdc-deactivation",
268 "llvm-cov",
269 "deactivated-function",
270 name,
271 "function",
272 reason,
273 "c-annotation",
274 ownership(path),
275 concerns,
276 recommendation="replace-with-decision-scoped-marker",
277 )
278 )
279 return records, findings
280
281
282@dataclass(frozen=True)
283class NativeSkip:
284 """Normalized native-test skip macro fields."""
285
286 column: int
287 tool: str
288 directive: str
289 reason: str
290
291
292def _skip_record(path: str, line: int, skip: NativeSkip) -> Suppression:
293 """Build one native test-control inventory row."""
294 return Suppression(
295 path,
296 line,
297 skip.column,
298 "test-control",
299 skip.tool,
300 "skip",
301 skip.directive,
302 "test-case",
303 skip.reason,
304 "c-macro",
305 ownership(path),
306 () if skip.reason else ("blank-reason",),
307 )
308
309
310def _scan_native_skips(
311 path: str, text_lines: list[str], code_lines: list[str]
312) -> tuple[list[Suppression], list[Finding]]:
313 """Parse GTest and Unity skip macros without matching strings or comments."""
314 records: list[Suppression] = []
315 findings: list[Finding] = []
316 for line_no, code in enumerate(code_lines, start=1):
317 raw = text_lines[line_no - 1]
318 for match in GTEST_RE.finditer(code):
319 valid = re.search(r'GTEST_SKIP\s*\‍(\s*\‍)\s*(?:<<\s*"(?P<reason>[^"]+)")?\s*;', raw)
320 if valid is None:
321 findings.append(Finding("malformed-native-test-skip", "GTEST_SKIP", path, line_no))
322 continue
323 records.append(
324 _skip_record(
325 path,
326 line_no,
327 NativeSkip(
328 match.start() + 1,
329 "google-test",
330 "GTEST_SKIP",
331 (valid.group("reason") or "").strip(),
332 ),
333 )
334 )
335 for match in UNITY_RE.finditer(code):
336 name_match = re.match(r"TEST_IGNORE(?:_MESSAGE)?", match.group(0))
337 name = name_match.group(0) if name_match is not None else ""
338 if name == "TEST_IGNORE":
339 valid = re.search(r"TEST_IGNORE\s*\‍(\s*\‍)\s*;", raw)
340 reason = ""
341 else:
342 valid = re.search(r'TEST_IGNORE_MESSAGE\s*\‍(\s*"(?P<reason>[^"]+)"\s*\‍)\s*;', raw)
343 reason = "" if valid is None else valid.group("reason").strip()
344 if valid is None:
345 findings.append(Finding("malformed-native-test-skip", name, path, line_no))
346 continue
347 records.append(
348 _skip_record(
349 path,
350 line_no,
351 NativeSkip(
352 match.start() + 1,
353 "unity",
354 name,
355 reason,
356 ),
357 )
358 )
359 return records, findings
360
361
362def scan_c_controls(
363 path: str, text: str, comments: list[Comment]
364) -> tuple[list[Suppression], list[Finding]]:
365 """Parse active MC/DC and native-test controls from one C-family file."""
366 text_lines = text.splitlines()
367 code_lines = _mask_noncode(text).splitlines()
368 records, findings = _scan_mcdc_comments(path, code_lines, comments)
369 macro_records, macro_findings = _scan_mcdc_macros(path, text_lines, code_lines)
370 skip_records, skip_findings = _scan_native_skips(path, text_lines, code_lines)
371 records.extend(macro_records)
372 records.extend(skip_records)
373 findings.extend(macro_findings)
374 findings.extend(skip_findings)
375 return records, findings
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157