ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_c_controls.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Syntax-aware compiler and allocation suppression inventory."""
4
5from __future__ import annotations
6
7import re
8from dataclasses import dataclass
9from pathlib import Path
10
11from check_no_dynamic_alloc import allocation_symbols
12from suppression_catalog import ALLOC_ALLOW_RE, C_FAMILY_SUFFIXES, ownership
13from suppression_comment_lex import Comment, extract_comments
14from suppression_model import Finding, Suppression
15
16PRAGMA_HINT_RE = re.compile(
17 r"^\s*#\s*pragma\s+(?P<tool>GCC|clang)\s+diagnostic\b",
18 re.IGNORECASE,
19)
20PRAGMA_KIND_RE = re.compile(
21 r"^\s*#\s*pragma\s+(?P<tool>GCC|clang)\s+diagnostic\s+"
22 r"(?P<kind>push|pop|ignored|warning|error)\b(?P<tail>.*)$"
23)
24PRAGMA_CONTROL_RE = re.compile(
25 r"^\s*#\s*pragma\s+(?P<tool>GCC|clang)\s+diagnostic\s+"
26 r'(?P<kind>ignored|warning|error)\s+"(?P<flag>-W[A-Za-z0-9_.=+\-]+)"\s*'
27 r"(?:(?://.*)|(?:/\*.*\*/\s*))?$"
28)
29PRAGMA_OPERATOR_HINT_RE = re.compile(r"\b_Pragma\b")
30PRAGMA_OPERATOR_RE = re.compile(r'\b_Pragma\s*\‍(\s*"(?P<payload>(?:\\.|[^"\\])*)"\s*\‍)')
31PRAGMA_PAYLOAD_HINT_RE = re.compile(
32 r"^(?:GCC|clang)\s+diagnostic\b",
33 re.IGNORECASE,
34)
35PRAGMA_PAYLOAD_KIND_RE = re.compile(
36 r"^(?P<tool>GCC|clang)\s+diagnostic\s+"
37 r"(?P<kind>push|pop|ignored|warning|error)\b(?P<tail>.*)$"
38)
39PRAGMA_PAYLOAD_CONTROL_RE = re.compile(
40 r"^(?P<tool>GCC|clang)\s+diagnostic\s+(?P<kind>ignored|warning|error)\s+"
41 r'"(?P<flag>-W[A-Za-z0-9_.=+\-]+)"$'
42)
43ATTRIBUTE_BLOCK_RE = re.compile(r"\‍[\‍[(?P<body>.*?)\‍]\‍]", re.DOTALL)
44ATTRIBUTE_CONTROL_HINT_RE = re.compile(
45 r"\bmaybe_unused\b|\bgnu::(?:__)?unused(?:__)?\b|\bclang::no_sanitize\b"
46)
47GNU_ATTRIBUTE_HINT_RE = re.compile(r"\b__attribute(?:__)?\b")
48GNU_CONTROL_HINT_RE = re.compile(
49 r"\b(?:__)?unused(?:__)?\b|\b(?:__)?no_sanitize(?:_[A-Za-z0-9_]+)?(?:__)?\b"
50)
51GNU_UNUSED_ITEM_RE = re.compile(r"^(?:__)?unused(?:__)?$")
52GNU_SANITIZER_CALL_RE = re.compile(
53 r"^(?:__)?no_sanitize(?:__)?\s*\‍((?P<args>.*)\‍)$",
54 re.DOTALL,
55)
56GNU_SANITIZER_SUFFIX_RE = re.compile(
57 r"^(?:__)?no_sanitize_(?P<rule>address|memory|thread|undefined)(?:__)?$"
58)
59STANDARD_MAYBE_RE = re.compile(r"^maybe_unused$")
60STANDARD_GNU_UNUSED_RE = re.compile(r"^gnu::(?:__)?unused(?:__)?$")
61STANDARD_CLANG_SANITIZER_RE = re.compile(
62 r"^clang::no_sanitize\s*\‍((?P<args>.*)\‍)$",
63 re.DOTALL,
64)
65SANITIZER_ARGS_RE = re.compile(
66 r'^\s*"[A-Za-z0-9_.+\-]+"(?:\s*,\s*"[A-Za-z0-9_.+\-]+")*\s*$',
67 re.DOTALL,
68)
69SANITIZER_NAME_RE = re.compile(r'"([A-Za-z0-9_.+\-]+)"')
70NASA_RE = re.compile(r"\bRA8_NASA_RULE_3_OK\b")
71NASA_CALL_RE = re.compile(r'RA8_NASA_RULE_3_OK\s*\‍(\s*"(?P<reason>(?:\\.|[^"\\])+)"\s*\‍)')
72ALLOC_HINT_RE = re.compile(r"\balloc-allow\b", re.IGNORECASE)
73C_HINT_RE = re.compile(
74 r"#\s*pragma\s+(?:GCC|clang)\s+diagnostic|\b_Pragma\b|"
75 r"__attribute(?:__)?|\‍[\‍[\s*(?:maybe_unused|gnu::(?:__)?unused|"
76 r"clang::no_sanitize)|RA8_NASA_RULE_3_OK|alloc-allow",
77 re.IGNORECASE,
78)
79
80
81@dataclass(frozen=True)
82class LogicalLine:
83 """One backslash-spliced preprocessing line with its source location."""
84
85 line: int
86 offset: int
87 code: str
88 raw: str
89
90
91@dataclass(frozen=True)
92class ControlSpec:
93 """Normalized identity fields for one parsed C-family control."""
94
95 family: str
96 tool: str
97 rule: str
98 directive: str
99 scope: str
100 provenance: str
101
102
103@dataclass
104class PragmaScan:
105 """Mutable state shared by one diagnostic pragma scan."""
106
107 path: str
108 comments: list[Comment]
109 records: list[Suppression]
110 findings: list[Finding]
111 depth: dict[str, int]
112
113
114def _blank(out: list[str], raw: str, start: int, end: int) -> None:
115 """Blank one non-code span while preserving newlines and offsets."""
116 for index in range(start, min(end, len(raw))):
117 if raw[index] not in "\r\n":
118 out[index] = " "
119
120
121def _raw_string(raw: str, index: int) -> tuple[str, int] | None:
122 """Return a C++ raw-string terminator and body start at one offset."""
123 if index > 0 and (raw[index - 1].isalnum() or raw[index - 1] == "_"):
124 return None
125 match = re.match(r'(?:u8|u|U|L)?R"([^ ()\\\t]{0,16})\‍(', raw[index:])
126 if match is None:
127 return None
128 return ")" + match.group(1) + '"', index + match.end()
129
130
131def _code_view(raw: str) -> str:
132 """Blank C comments and literals without changing source coordinates."""
133 out = list(raw)
134 index = 0
135 while index < len(raw):
136 raw_string = _raw_string(raw, index)
137 if raw_string is not None:
138 terminator, body = raw_string
139 end = raw.find(terminator, body)
140 stop = len(raw) if end < 0 else end + len(terminator)
141 _blank(out, raw, index, stop)
142 index = stop
143 continue
144 if raw.startswith("//", index):
145 end = raw.find("\n", index)
146 stop = len(raw) if end < 0 else end
147 _blank(out, raw, index, stop)
148 index = stop
149 continue
150 if raw.startswith("/*", index):
151 end = raw.find("*/", index + 2)
152 stop = len(raw) if end < 0 else end + 2
153 _blank(out, raw, index, stop)
154 index = stop
155 continue
156 if raw[index] in {'"', "'"}:
157 quote = raw[index]
158 start = index
159 index += 1
160 while index < len(raw):
161 if raw[index] == "\\" and index + 1 < len(raw):
162 index += 2
163 continue
164 if raw[index] == quote:
165 index += 1
166 break
167 if raw[index] == "\n":
168 break
169 index += 1
170 _blank(out, raw, start, index)
171 continue
172 index += 1
173 return "".join(out)
174
175
176def _logical_lines(raw: str, code: str) -> list[LogicalLine]:
177 """Join preprocessing line splices while retaining the first location."""
178 raw_lines = raw.splitlines(keepends=True)
179 code_lines = code.splitlines(keepends=True)
180 result: list[LogicalLine] = []
181 offset = 0
182 index = 0
183 while index < len(code_lines):
184 start = index
185 raw_parts = [raw_lines[index]]
186 code_parts = [code_lines[index]]
187 while code_parts[-1].rstrip("\r\n").endswith("\\") and index + 1 < len(code_lines):
188 index += 1
189 raw_parts.append(raw_lines[index])
190 code_parts.append(code_lines[index])
191 joined_raw = re.sub(r"\\\r?\n", "", "".join(raw_parts))
192 joined_code = re.sub(r"\\\r?\n", "", "".join(code_parts))
193 result.append(LogicalLine(start + 1, offset, joined_code.rstrip(), joined_raw.rstrip()))
194 offset += sum(len(part) for part in code_lines[start : index + 1])
195 index += 1
196 return result
197
198
199def _line_column(raw: str, offset: int) -> tuple[int, int]:
200 """Convert a source offset to a one-based line and column."""
201 line = raw.count("\n", 0, offset) + 1
202 start = raw.rfind("\n", 0, offset)
203 return line, offset - start
204
205
206def _concerns(path: str, rule: str, reason: str, *, required: bool) -> tuple[str, ...]:
207 """Return first-party review concerns for one narrow control."""
208 if ownership(path) != "first-party":
209 return ()
210 concerns: list[str] = []
211 if required and not reason:
212 concerns.append("blank-reason")
213 if rule in {"*", "all", "-W"}:
214 concerns.append("broad-rule")
215 return tuple(concerns)
216
217
218def _suppression(
219 path: str,
220 location: tuple[int, int],
221 spec: ControlSpec,
222 reason: str,
223 *,
224 reason_required: bool,
225) -> Suppression:
226 """Build one compiler-control row with ownership-aware review policy."""
227 line, column = location
228 return Suppression(
229 path,
230 line,
231 column,
232 spec.family,
233 spec.tool,
234 spec.rule,
235 spec.directive,
236 spec.scope,
237 reason,
238 spec.provenance,
239 ownership(path),
240 _concerns(path, spec.rule, reason, required=reason_required),
241 )
242
243
244def _comment_reason(comments: list[Comment], line: int) -> str:
245 """Return a local diagnostic-pragmas rationale on one source line."""
246 for comment in comments:
247 if comment.line not in {line - 1, line}:
248 continue
249 text = comment.text.strip().lstrip("*").strip()
250 if text.startswith("Suppression rationale:"):
251 return text.removeprefix("Suppression rationale:").strip()
252 return ""
253
254
255def _record_pragma_region(
256 scan: PragmaScan,
257 logical: LogicalLine,
258 tool: str,
259 kind: str,
260 column: int,
261) -> None:
262 """Record one exact diagnostic push/pop and update its tool-specific depth."""
263 scope = "region-start" if kind == "push" else "region-end"
264 spelling = "#pragma" if logical.raw.lstrip().startswith("#") else "_Pragma"
265 spec = ControlSpec(
266 "compiler",
267 tool,
268 "diagnostic-state",
269 f"{spelling} {tool} diagnostic {kind}",
270 scope,
271 "compiler-pragma",
272 )
273 scan.records.append(
274 _suppression(
275 scan.path,
276 (logical.line, column),
277 spec,
278 "",
279 reason_required=False,
280 )
281 )
282 if kind == "push":
283 scan.depth[tool] += 1
284 elif scan.depth[tool] == 0:
285 scan.findings.append(Finding("unmatched-diagnostic-pop", tool, scan.path, logical.line))
286 else:
287 scan.depth[tool] -= 1
288
289
290def _record_pragma_control(
291 scan: PragmaScan,
292 logical: LogicalLine,
293 tool: str,
294 kind: str,
295 column: int,
296) -> None:
297 """Record one exact warning control or fail closed on malformed grammar."""
298 control = PRAGMA_CONTROL_RE.fullmatch(logical.raw)
299 if control is None:
300 control = PRAGMA_PAYLOAD_CONTROL_RE.fullmatch(logical.raw)
301 if control is None:
302 scan.findings.append(
303 Finding(
304 "malformed-diagnostic-pragma",
305 logical.raw.strip(),
306 scan.path,
307 logical.line,
308 )
309 )
310 return
311 flag = control.group("flag")
312 spelling = "#pragma" if logical.raw.lstrip().startswith("#") else "_Pragma"
313 reason = _comment_reason(scan.comments, logical.line)
314 spec = ControlSpec(
315 "compiler",
316 tool,
317 flag,
318 f"{spelling} {tool} diagnostic {kind}",
319 "diagnostic-state",
320 "compiler-pragma",
321 )
322 scan.records.append(
323 _suppression(
324 scan.path,
325 (logical.line, column),
326 spec,
327 reason,
328 reason_required=kind == "ignored",
329 )
330 )
331 if scan.depth[tool] == 0:
332 scan.findings.append(Finding("unscoped-diagnostic-control", tool, scan.path, logical.line))
333
334
335def _destringize_pragma(payload: str) -> str | None:
336 """Apply the C _Pragma string-literal destringization rules."""
337 if re.search(r'\\(?!["\\])', payload):
338 return None
339 return payload.replace('\\"', '"').replace("\\\\", "\\")
340
341
342def _scan_pragma_operator_line(scan: PragmaScan, logical: LogicalLine) -> None:
343 """Parse every diagnostic _Pragma operator on one logical source line."""
344 for hint in PRAGMA_OPERATOR_HINT_RE.finditer(logical.code):
345 tail = logical.raw[hint.start() :]
346 operator = PRAGMA_OPERATOR_RE.match(logical.raw, hint.start())
347 if operator is None:
348 if re.search(r"(?:GCC|clang)\s+diagnostic", tail, re.IGNORECASE):
349 scan.findings.append(
350 Finding("malformed-diagnostic-pragma", tail.strip(), scan.path, logical.line)
351 )
352 continue
353 payload = _destringize_pragma(operator.group("payload"))
354 if payload is None:
355 if re.search(r"(?:GCC|clang)\s+diagnostic", tail, re.IGNORECASE):
356 scan.findings.append(
357 Finding("malformed-diagnostic-pragma", tail.strip(), scan.path, logical.line)
358 )
359 continue
360 if PRAGMA_PAYLOAD_HINT_RE.match(payload) is None:
361 continue
362 match = PRAGMA_PAYLOAD_KIND_RE.fullmatch(payload)
363 if match is None:
364 scan.findings.append(
365 Finding("malformed-diagnostic-pragma", payload, scan.path, logical.line)
366 )
367 continue
368 tool = match.group("tool")
369 kind = match.group("kind")
370 column = hint.start() + 1
371 operator_line = LogicalLine(logical.line, logical.offset, payload, payload)
372 if kind in {"push", "pop"}:
373 if match.group("tail").strip():
374 scan.findings.append(
375 Finding("malformed-diagnostic-pragma", payload, scan.path, logical.line)
376 )
377 else:
378 _record_pragma_region(scan, operator_line, tool, kind, column)
379 continue
380 _record_pragma_control(scan, operator_line, tool, kind, column)
381
382
383def _scan_pragmas(
384 path: str, raw: str, code: str, comments: list[Comment]
385) -> tuple[list[Suppression], list[Finding]]:
386 """Parse GCC/Clang diagnostic state controls and their paired regions."""
387 records: list[Suppression] = []
388 findings: list[Finding] = []
389 scan = PragmaScan(path, comments, records, findings, {"GCC": 0, "clang": 0})
390 for logical in _logical_lines(raw, code):
391 _scan_pragma_operator_line(scan, logical)
392 hint = PRAGMA_HINT_RE.match(logical.code)
393 if hint is None:
394 continue
395 match = PRAGMA_KIND_RE.fullmatch(logical.code)
396 if match is None:
397 findings.append(
398 Finding("malformed-diagnostic-pragma", logical.raw.strip(), path, logical.line)
399 )
400 continue
401 tool = match.group("tool")
402 kind = match.group("kind").lower()
403 column = hint.start() + 1
404 if kind not in {"push", "pop"}:
405 _record_pragma_control(scan, logical, tool, kind, column)
406 elif match.group("tail").strip():
407 findings.append(
408 Finding("malformed-diagnostic-pragma", logical.raw.strip(), path, logical.line)
409 )
410 else:
411 _record_pragma_region(scan, logical, tool, kind, column)
412 return records, findings
413
414
415def _balanced_end(code: str, start: int) -> int | None:
416 """Return the offset after the parenthesis matching ``start``."""
417 depth = 0
418 for index in range(start, len(code)):
419 if code[index] == "(":
420 depth += 1
421 elif code[index] == ")":
422 depth -= 1
423 if depth == 0:
424 return index + 1
425 return None
426
427
428def _attribute_items(code: str, raw: str, start: int, end: int) -> list[tuple[int, str, str]]:
429 """Split one attribute list on top-level commas while preserving offsets."""
430 items: list[tuple[int, str, str]] = []
431 item_start = start
432 depth = 0
433 for index in range(start, end):
434 if code[index] == "(":
435 depth += 1
436 elif code[index] == ")":
437 depth = max(0, depth - 1)
438 elif code[index] == "," and depth == 0:
439 items.extend(_attribute_item(code, raw, item_start, index))
440 item_start = index + 1
441 items.extend(_attribute_item(code, raw, item_start, end))
442 return items
443
444
445def _attribute_item(code: str, raw: str, start: int, end: int) -> list[tuple[int, str, str]]:
446 """Return one nonempty, left-trimmed attribute item."""
447 code_part = code[start:end]
448 leading = len(code_part) - len(code_part.lstrip())
449 offset = start + leading
450 if not code[offset:end].strip():
451 return []
452 return [(offset, code[offset:end].strip(), raw[offset:end].strip())]
453
454
455def _macro_parameter(raw: str, offset: int, name: str) -> bool:
456 """Return whether one identifier is a parameter of its enclosing macro."""
457 line_start = raw.rfind("\n", 0, offset) + 1
458 line_end = raw.find("\n", offset)
459 if line_end < 0:
460 line_end = len(raw)
461 match = re.match(
462 r"\s*#\s*define\s+[A-Za-z_]\w*\s*\‍((?P<params>[^)]*)\‍)",
463 raw[line_start:line_end],
464 )
465 if match is None:
466 return False
467 params = {item.strip() for item in match.group("params").split(",")}
468 return name in params
469
470
471def _sanitizer_rule(raw: str, offset: int, args: str, *, macro_ok: bool) -> str | None:
472 """Normalize an exact sanitizer string list or a live macro parameter."""
473 if SANITIZER_ARGS_RE.fullmatch(args):
474 return ",".join(SANITIZER_NAME_RE.findall(args))
475 argument = args.strip()
476 if macro_ok and re.fullmatch(r"[A-Za-z_]\w*", argument):
477 return argument if _macro_parameter(raw, offset, argument) else None
478 return None
479
480
481def _compiler_attribute(
482 path: str,
483 raw: str,
484 offset: int,
485 rule: str,
486 directive: str,
487) -> Suppression:
488 """Build one declaration-scoped compiler attribute inventory row."""
489 tool = "language-attribute" if directive == "[[maybe_unused]]" else "compiler-attribute"
490 return _suppression(
491 path,
492 _line_column(raw, offset),
493 ControlSpec(
494 "compiler",
495 tool,
496 rule,
497 directive,
498 "declaration",
499 "compiler-attribute",
500 ),
501 "",
502 reason_required=False,
503 )
504
505
506def _gnu_body_span(code: str, start: int) -> tuple[int, int, int] | None:
507 """Return the GNU attribute body and full-expression end offsets."""
508 cursor = start
509 while cursor < len(code) and code[cursor].isspace():
510 cursor += 1
511 if cursor >= len(code) or code[cursor] != "(":
512 return None
513 outer = cursor
514 cursor += 1
515 while cursor < len(code) and code[cursor].isspace():
516 cursor += 1
517 if cursor >= len(code) or code[cursor] != "(":
518 return None
519 inner = cursor
520 inner_end = _balanced_end(code, inner)
521 outer_end = _balanced_end(code, outer)
522 if inner_end is None or outer_end is None:
523 return None
524 if re.fullmatch(r"\s*\‍)", code[inner_end:outer_end]) is None:
525 return None
526 return inner + 1, inner_end - 1, outer_end
527
528
529def _gnu_item(
530 path: str, raw: str, offset: int, code_item: str, raw_item: str
531) -> tuple[Suppression | None, Finding | None]:
532 """Parse one GNU attribute item, failing closed on supported-name lookalikes."""
533 rule: str | None = None
534 directive = ""
535 if GNU_UNUSED_ITEM_RE.fullmatch(raw_item):
536 rule = "unused"
537 directive = "__attribute__((unused))"
538 else:
539 suffix = GNU_SANITIZER_SUFFIX_RE.fullmatch(raw_item)
540 call = GNU_SANITIZER_CALL_RE.fullmatch(raw_item)
541 if suffix is not None:
542 rule = suffix.group("rule")
543 directive = "__attribute__((no_sanitize))"
544 elif call is not None:
545 rule = _sanitizer_rule(raw, offset, call.group("args"), macro_ok=True)
546 directive = "__attribute__((no_sanitize))"
547 if rule is not None:
548 return _compiler_attribute(path, raw, offset, rule, directive), None
549 if GNU_CONTROL_HINT_RE.search(code_item) is None:
550 return None, None
551 line, _ = _line_column(raw, offset)
552 return None, Finding("malformed-gnu-attribute", raw_item, path, line)
553
554
555def _scan_gnu_attributes(path: str, raw: str, code: str) -> tuple[list[Suppression], list[Finding]]:
556 """Inventory exact GNU unused and sanitizer-disable attribute grammar."""
557 records: list[Suppression] = []
558 findings: list[Finding] = []
559 for marker in GNU_ATTRIBUTE_HINT_RE.finditer(code):
560 span = _gnu_body_span(code, marker.end())
561 if span is None:
562 lookahead = code[marker.end() : marker.end() + 512]
563 if GNU_CONTROL_HINT_RE.search(lookahead) is not None:
564 line, _ = _line_column(raw, marker.start())
565 findings.append(Finding("malformed-gnu-attribute", marker.group(), path, line))
566 continue
567 body_start, body_end, _ = span
568 for offset, code_item, raw_item in _attribute_items(code, raw, body_start, body_end):
569 record, finding = _gnu_item(path, raw, offset, code_item, raw_item)
570 if record is not None:
571 records.append(record)
572 if finding is not None:
573 findings.append(finding)
574 return records, findings
575
576
577def _standard_item(
578 path: str, raw: str, offset: int, code_item: str, raw_item: str
579) -> tuple[Suppression | None, Finding | None]:
580 """Parse one C23/C++ attribute item from the supported suppression family."""
581 if STANDARD_MAYBE_RE.fullmatch(raw_item):
582 return _compiler_attribute(path, raw, offset, "unused", "[[maybe_unused]]"), None
583 if STANDARD_GNU_UNUSED_RE.fullmatch(raw_item):
584 return _compiler_attribute(path, raw, offset, "unused", "[[gnu::unused]]"), None
585 sanitizer = STANDARD_CLANG_SANITIZER_RE.fullmatch(raw_item)
586 if sanitizer is not None:
587 rule = _sanitizer_rule(raw, offset, sanitizer.group("args"), macro_ok=False)
588 if rule is not None:
589 return _compiler_attribute(
590 path,
591 raw,
592 offset,
593 rule,
594 "[[clang::no_sanitize]]",
595 ), None
596 if ATTRIBUTE_CONTROL_HINT_RE.search(code_item) is None:
597 return None, None
598 line, _ = _line_column(raw, offset)
599 code_name = (
600 "malformed-maybe-unused"
601 if re.search(r"\bmaybe_unused\b", code_item)
602 else "malformed-standard-attribute"
603 )
604 return None, Finding(code_name, raw_item, path, line)
605
606
607def _scan_standard_attributes(
608 path: str, raw: str, code: str
609) -> tuple[list[Suppression], list[Finding]]:
610 """Inventory standard/scoped unused and sanitizer-disable attributes."""
611 records: list[Suppression] = []
612 findings: list[Finding] = []
613 spans: list[tuple[int, int]] = []
614 for block in ATTRIBUTE_BLOCK_RE.finditer(code):
615 spans.append(block.span())
616 for offset, code_item, raw_item in _attribute_items(
617 code, raw, block.start("body"), block.end("body")
618 ):
619 record, finding = _standard_item(path, raw, offset, code_item, raw_item)
620 if record is not None:
621 records.append(record)
622 if finding is not None:
623 findings.append(finding)
624 for hint in ATTRIBUTE_CONTROL_HINT_RE.finditer(code):
625 if any(start <= hint.start() < end for start, end in spans):
626 continue
627 line, _ = _line_column(raw, hint.start())
628 code_name = (
629 "malformed-maybe-unused"
630 if hint.group().startswith("maybe_unused")
631 else "malformed-standard-attribute"
632 )
633 findings.append(Finding(code_name, "invalid attribute", path, line))
634 return records, findings
635
636
637def _scan_nasa(path: str, raw: str, code: str) -> tuple[list[Suppression], list[Finding]]:
638 """Inventory reason-bearing NASA Rule 3 function waivers."""
639 records: list[Suppression] = []
640 findings: list[Finding] = []
641 for marker in NASA_RE.finditer(code):
642 line, column = _line_column(raw, marker.start())
643 line_start = raw.rfind("\n", 0, marker.start()) + 1
644 if re.match(r"\s*#\s*define\b", code[line_start : marker.start()]):
645 continue
646 call = NASA_CALL_RE.match(raw, marker.start())
647 reason = call.group("reason").strip() if call is not None else ""
648 records.append(
649 _suppression(
650 path,
651 (line, column),
652 ControlSpec(
653 "project-policy",
654 "annotation-checker",
655 "NASA-P10-3",
656 "RA8_NASA_RULE_3_OK",
657 "function",
658 "annotation",
659 ),
660 reason,
661 reason_required=True,
662 )
663 )
664 if call is None:
665 findings.append(
666 Finding("malformed-nasa-rule-3-waiver", "reason string required", path, line)
667 )
668 return records, findings
669
670
671def _scan_alloc_allow(
672 path: str, code: str, comments: list[Comment]
673) -> tuple[list[Suppression], list[Finding]]:
674 """Inventory same-line, reasoned dynamic-allocation checker waivers."""
675 records: list[Suppression] = []
676 findings: list[Finding] = []
677 code_lines = code.splitlines()
678 brace_depths: list[int] = []
679 brace_depth = 0
680 for source_line in code_lines:
681 brace_depths.append(brace_depth)
682 if not source_line.lstrip().startswith("#"):
683 brace_depth += source_line.count("{") - source_line.count("}")
684 brace_depth = max(0, brace_depth)
685 for comment in comments:
686 hint = ALLOC_HINT_RE.search(comment.text)
687 if hint is None:
688 continue
689 match = ALLOC_ALLOW_RE.search(comment.text.strip())
690 if match is None:
691 findings.append(
692 Finding("malformed-alloc-allow", comment.text.strip(), path, comment.line)
693 )
694 continue
695 source_line = code_lines[comment.line - 1] if comment.line <= len(code_lines) else ""
696 line_depth = brace_depths[comment.line - 1] if comment.line <= len(brace_depths) else 0
697 symbols = allocation_symbols(source_line, line_depth)
698 if not symbols:
699 findings.append(
700 Finding("orphan-alloc-allow", "no allocator call on this line", path, comment.line)
701 )
702 continue
703 records.extend(
704 _suppression(
705 path,
706 (comment.line, comment.column + hint.start()),
707 ControlSpec(
708 "dynamic-allocation",
709 "check_no_dynamic_alloc.py",
710 symbol,
711 "alloc-allow",
712 "line",
713 "inline-comment",
714 ),
715 match.group("reason").strip(),
716 reason_required=True,
717 )
718 for symbol in symbols
719 )
720 return records, findings
721
722
723def scan_c_control_file(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
724 """Scan one C-family file through every assigned compiler-control grammar."""
725 if Path(path).suffix.lower() not in C_FAMILY_SUFFIXES or C_HINT_RE.search(text) is None:
726 return [], []
727 code = _code_view(text)
728 comments, _ = extract_comments(path, text)
729 records: list[Suppression] = []
730 findings: list[Finding] = []
731 for scanner in (_scan_pragmas,):
732 found, problems = scanner(path, text, code, comments)
733 records.extend(found)
734 findings.extend(problems)
735 for scanner in (_scan_gnu_attributes, _scan_standard_attributes, _scan_nasa):
736 found, problems = scanner(path, text, code)
737 records.extend(found)
738 findings.extend(problems)
739 found, problems = _scan_alloc_allow(path, code, comments)
740 records.extend(found)
741 findings.extend(problems)
742 return records, findings
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157