ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_generated_markers.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Canonical generated/file-size waiver grammar shared by both gates."""
4
5from __future__ import annotations
6
7import re
8from dataclasses import dataclass
9from pathlib import Path, PurePosixPath
10
11from suppression_catalog import ownership
12from suppression_model import Finding, Suppression
13
14HEAD_SCAN_LINES = 40
15_COMMENT = r"(?:#|//|/\*+|\*)"
16_GENERATED_RE = re.compile(
17 rf"^\s*{_COMMENT}\s*@generated\s+by\s+"
18 r"(?P<generator>[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+)\s+"
19 r"(?P<reason>\S.*?)\s*(?:\*/)?\s*$",
20 re.IGNORECASE,
21)
22_FILE_SIZE_RE = re.compile(rf"^\s*{_COMMENT}\s*FILE-SIZE-OK\s*:\s*(?P<reason>\S.*?)\s*(?:\*/)?\s*$")
23_GENERATED_HINT_RE = re.compile(rf"^\s*{_COMMENT}\s*@generated\b", re.IGNORECASE)
24_FILE_SIZE_HINT_RE = re.compile(rf"^\s*{_COMMENT}\s*FILE-SIZE-OK\b", re.IGNORECASE)
25
26
27@dataclass(frozen=True)
28class HeadWaiver:
29 """One canonical head marker and its bound provenance."""
30
31 line: int
32 kind: str
33 generator: str
34 reason: str
35
36
37def parse_head_waivers(text: str) -> tuple[list[HeadWaiver], list[Finding]]:
38 """Parse exact standalone comment markers in the first forty lines."""
39 waivers: list[HeadWaiver] = []
40 findings: list[Finding] = []
41 for line_no, raw in enumerate(text.splitlines()[:HEAD_SCAN_LINES], start=1):
42 generated = _GENERATED_RE.fullmatch(raw)
43 file_size = _FILE_SIZE_RE.fullmatch(raw)
44 if generated is not None:
45 generator = generated.group("generator")
46 normalized = PurePosixPath(generator)
47 if normalized.is_absolute() or ".." in normalized.parts:
48 findings.append(
49 Finding("malformed-generated-marker", "unsafe generator path", line=line_no)
50 )
51 continue
52 reason = generated.group("reason").strip()
53 if not _substantive_reason(reason):
54 findings.append(
55 Finding(
56 "non-substantive-waiver-reason",
57 "generated marker rationale must contain a letter or digit",
58 line=line_no,
59 )
60 )
61 else:
62 waivers.append(HeadWaiver(line_no, "generated", generator, reason))
63 elif file_size is not None:
64 reason = file_size.group("reason").strip()
65 if not _substantive_reason(reason):
66 findings.append(
67 Finding(
68 "non-substantive-waiver-reason",
69 "file-size rationale must contain a letter or digit",
70 line=line_no,
71 )
72 )
73 else:
74 waivers.append(HeadWaiver(line_no, "file-size", "", reason))
75 elif _GENERATED_HINT_RE.match(raw) or _FILE_SIZE_HINT_RE.match(raw):
76 findings.append(
77 Finding(
78 "malformed-generated-marker",
79 "directive-like waiver does not match the canonical standalone grammar",
80 line=line_no,
81 )
82 )
83 if len(waivers) > 1:
84 findings.append(
85 Finding(
86 "duplicate-file-size-waiver",
87 "a file head may carry only one generated/file-size waiver",
88 line=waivers[1].line,
89 )
90 )
91 return waivers, findings
92
93
94def _substantive_reason(reason: str) -> bool:
95 """Reject empty and punctuation/delimiter-only waiver rationales."""
96 return any(character.isalnum() for character in reason)
97
98
99def effective_head_waiver(
100 text: str,
101 *,
102 tracked_paths: frozenset[str] | None = None,
103 artifact_path: str = "",
104) -> tuple[HeadWaiver | None, list[Finding]]:
105 """Return the sole valid waiver, rejecting untracked generated provenance."""
106 waivers, findings = parse_head_waivers(text)
107 if len(waivers) != 1:
108 return None, findings
109 waiver = waivers[0]
110 if waiver.kind == "generated" and artifact_path and waiver.generator == artifact_path:
111 findings.append(
112 Finding(
113 "self-generated-provenance",
114 "generated artifact must name a distinct generator",
115 line=waiver.line,
116 )
117 )
118 return None, findings
119 if waiver.kind == "generated" and not _has_substantive_generated_body(text):
120 findings.append(
121 Finding(
122 "generated-marker-without-body",
123 "generated marker has no substantive generated body",
124 line=waiver.line,
125 )
126 )
127 return None, findings
128 if (
129 waiver.kind == "generated"
130 and tracked_paths is not None
131 and waiver.generator not in tracked_paths
132 ):
133 findings.append(
134 Finding(
135 "untracked-generated-provenance",
136 f"generator is not tracked: {waiver.generator}",
137 line=waiver.line,
138 )
139 )
140 return None, findings
141 return waiver, findings
142
143
144def _has_substantive_generated_body(text: str) -> bool:
145 """Require code/data beyond blank lines and standalone comment prose."""
146 without_blocks = _without_block_comments(text)
147 for raw in without_blocks.splitlines():
148 line = raw.strip()
149 if not line or line.startswith(("#", "//", "*")):
150 continue
151 return True
152 return False
153
154
155def _without_block_comments(text: str) -> str:
156 """Remove closed or unterminated C block comments without executing a lexer."""
157 parts: list[str] = []
158 cursor = 0
159 while cursor < len(text):
160 start = text.find("/*", cursor)
161 if start < 0:
162 parts.append(text[cursor:])
163 break
164 parts.append(text[cursor:start])
165 end = text.find("*/", start + 2)
166 if end < 0:
167 break
168 cursor = end + 2
169 return "".join(parts)
170
171
172def _same_file_identity(artifact: Path, generator: Path) -> bool:
173 """Return whether two resolved paths name one filesystem object."""
174 if artifact == generator:
175 return True
176 try:
177 artifact_stat = artifact.stat()
178 generator_stat = generator.stat()
179 except OSError:
180 return False
181 return (artifact_stat.st_dev, artifact_stat.st_ino) == (
182 generator_stat.st_dev,
183 generator_stat.st_ino,
184 )
185
186
187def _generator_identity_finding(
188 artifact: Path, generator_rel: str, repo_root: Path
189) -> Finding | None:
190 """Reject symlinked, missing, out-of-repo, nonregular, and self-identical generators."""
191 generator = repo_root / generator_rel
192 if generator.is_symlink():
193 return Finding(
194 "symlinked-generated-provenance",
195 f"generator must be a regular repository file, not a symlink: {generator_rel}",
196 )
197 try:
198 resolved = generator.resolve(strict=True)
199 resolved.relative_to(repo_root.resolve(strict=True))
200 regular = resolved.is_file()
201 except (OSError, ValueError):
202 resolved = None
203 regular = False
204 if resolved is None or not regular:
205 return Finding(
206 "missing-generated-provenance",
207 f"generator is not a regular repository file: {generator_rel}",
208 )
209 if _same_file_identity(artifact.resolve(), resolved):
210 return Finding(
211 "self-generated-provenance",
212 f"generator and artifact share one file identity: {generator_rel}",
213 )
214 return None
215
216
217def generated_records(
218 path: str,
219 text: str,
220 tracked_paths: frozenset[str],
221 repo_root: Path,
222) -> tuple[list[Suppression], list[Finding]]:
223 """Inventory one canonical generated artifact, never marker prose/templates."""
224 waiver, findings = effective_head_waiver(text, tracked_paths=tracked_paths, artifact_path=path)
225 if waiver is None or waiver.kind != "generated":
226 return [], [Finding(item.code, item.message, path, item.line) for item in findings]
227 identity_problem = _generator_identity_finding(repo_root / path, waiver.generator, repo_root)
228 if identity_problem is not None:
229 findings.append(Finding(identity_problem.code, identity_problem.message, line=waiver.line))
230 return [], [Finding(item.code, item.message, path, item.line) for item in findings]
231 record = Suppression(
232 path,
233 waiver.line,
234 1,
235 "generated-artifact",
236 "file-size",
237 "generated-source",
238 "@generated by",
239 f"file:{path}",
240 waiver.reason,
241 f"generator:{waiver.generator}",
242 ownership(path),
243 (),
244 )
245 located = [Finding(item.code, item.message, path, item.line) for item in findings]
246 return [record], located
247
248
249def path_has_effective_waiver(
250 path: Path,
251 repo_root: Path,
252 tracked_paths: frozenset[str],
253) -> bool:
254 """Apply the shared grammar for the file-size gate's real filesystem path."""
255 try:
256 text = path.read_text(encoding="utf-8", errors="replace")
257 except OSError:
258 return False
259 try:
260 artifact_path = (
261 path.resolve(strict=True).relative_to(repo_root.resolve(strict=True)).as_posix()
262 )
263 except ValueError:
264 artifact_path = path.name
265 except OSError:
266 return False
267 waiver, findings = effective_head_waiver(
268 text, tracked_paths=tracked_paths, artifact_path=artifact_path
269 )
270 if waiver is None or findings:
271 return False
272 if waiver.kind == "file-size":
273 return True
274 return _generator_identity_finding(path, waiver.generator, repo_root) is None