3"""Parse MC/DC deactivations and native C/C++ test-skip controls."""
5from __future__
import annotations
8from dataclasses
import dataclass
10from suppression_catalog
import ownership
11from suppression_comment_lex
import Comment
12from suppression_model
import Finding, Suppression
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'
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",
29RAW_STRING_RE = re.compile(
r'(?:u8|u|U|L)?R"([^ ()\\\t]{0,16})\(')
34 """Cross-line state for blanking C/C++ comments and literals."""
37 block_comment: bool =
False
38 line_comment: bool =
False
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":
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."""
52 if state.line_comment:
53 state.line_comment = char !=
"\n"
57 if state.block_comment:
58 if text.startswith(
"*/", index):
59 _blank(chars, index, index + 2)
60 state.block_comment =
False
63 _blank(chars, index, index + 1)
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
74 if char ==
"\\" and index + 1 < len(chars):
75 _blank(chars, index, index + 2)
77 if char == state.quote:
79 _blank(chars, index, index + 1)
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)
91 if text.startswith(
"//", index):
92 _blank(chars, index, index + 2)
93 state.line_comment =
True
95 if text.startswith(
"/*", index):
96 _blank(chars, index, index + 2)
97 state.block_comment =
True
99 if chars[index]
in {
'"',
"'"}:
100 state.quote = chars[index]
106def _mask_noncode(text: str) -> str:
107 """Blank comments and literals while preserving code coordinates."""
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)
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):
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()
129 statement +=
" " + code
130 if COMPOUND_RE.search(statement):
132 if ";" in code
or "{" in code
or "}" in code:
137def _mcdc_record(path: str, comment: Comment, reason: str, target_line: int) -> Suppression:
138 """Build one decision-scoped MC/DC deactivation row."""
145 "deactivated-condition",
147 f
"decision-line:{target_line}",
152 evidence=(f
"decision-line:{target_line}",
"standard:DO-178C-6.4.4.3"),
153 recommendation=
"revalidate-invariant",
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(
"*"):
167 body = comment.text.strip().lstrip(
"*").strip()
168 match = MCDC_COMMENT_RE.fullmatch(body)
170 if MCDC_COMMENT_HINT_RE.match(body):
171 findings.append(Finding(
"malformed-mcdc-deactivation", body, path, comment.line))
173 reason = match.group(
"reason").strip()
175 findings.append(Finding(
"blank-mcdc-reason", body, path, comment.line))
177 target_line = _target_decision(code_lines, comment.line)
178 if target_line
is None:
181 "unpaired-mcdc-deactivation",
182 "marker does not immediately govern a compound decision",
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))
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":
198 "mcdc-owner-mismatch",
199 f
"deactivation appears in {record.owner} code",
204 return records, findings
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] !=
"(":
212 for index
in range(open_index, len(code)):
213 if code[index] ==
"(":
215 elif code[index] ==
")":
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)
227 invocation = raw[macro.start() : end]
228 invocation_match = MCDC_DEACTIVATED_INVOCATION_RE.fullmatch(invocation)
229 if invocation_match
is None:
231 parts = re.findall(
r'"((?:\\.|[^"\\])*)"', invocation_match.group(
"strings"))
232 return " ".join(part.replace(
r"\"",
'"').replace(
r"\\",
"\\")
for part
in parts).strip()
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"):
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))
250 reason = _macro_reason(text_lines[line_no - 1], code, match)
254 "malformed-mcdc-macro",
255 "reason must be one or more string literals on the annotation line",
261 concerns = (
"blank-reason",)
if not reason
else (
"broad-function-scope",)
269 "deactivated-function",
276 recommendation=
"replace-with-decision-scoped-marker",
279 return records, findings
282@dataclass(frozen=True)
284 """Normalized native-test skip macro fields."""
292def _skip_record(path: str, line: int, skip: NativeSkip) -> Suppression:
293 """Build one native test-control inventory row."""
306 ()
if skip.reason
else (
"blank-reason",),
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)
321 findings.append(Finding(
"malformed-native-test-skip",
"GTEST_SKIP", path, line_no))
331 (valid.group(
"reason")
or "").strip(),
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)
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()
345 findings.append(Finding(
"malformed-native-test-skip", name, path, line_no))
359 return records, findings
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.