4"""check_no_goto_setjmp.py -- NASA Power-of-10 Rule 1 textual backstop.
6NASA/JPL Power-of-10 Rule 1 forbids unstructured control flow: no ``goto``,
7no ``setjmp`` / ``longjmp``, and no recursion. Three of those four constructs
8-- ``goto``, ``setjmp``, ``longjmp`` -- are single keywords / library calls
9that a textual scan can find reliably, so this gate closes them off with a
10parser-independent sweep of first-party C/C++.
12Why a dedicated gate. Until now ``goto`` / ``setjmp`` were enforced only
13indirectly, via the MISRA cppcheck ratchet (Rule 15.1 forbids ``goto``,
14Rule 21.4 forbids ``<setjmp.h>``). That ratchet runs cppcheck at
15``--std=c11`` because the pinned cppcheck (2.13) cannot parse C23 -- the
16codebase's ``enum : uint8_t`` typed enums and ``[[...]]`` attributes raise
17``syntaxError`` and the affected translation units are only partially parsed
18(see ``scripts/checks/misra_check_inner.sh`` and ADR-0002). A construct on a
19line cppcheck skipped is a construct the ratchet never rules on. A textual
20scan does not depend on a parse, so it covers the whole tree uniformly and
21closes that blind spot for these three tokens.
23Recursion is deliberately OUT of scope here: detecting it needs a call graph,
24which is covered separately by ``scripts/checks/annot_rules.py``
25(``RA8_NO_RECURSION``) and MISRA Rule 17.2.
27The three tokens are matched only in CODE positions. Occurrences inside
28comments and string / character literals are prose, not control flow, and are
29never flagged -- the tree has a ``goto`` inside a comment in
30``libs/ra8_hal/src/ra8_flash_config.c`` and ``setjmp`` / ``longjmp`` named in
31the prose of ``libs/ra8_core/inc/ra8_err.h`` and
32``libs/ra8_core/src/ra8_exception.c``, none of which is a violation.
34Scope is the firmware and host-tool tree -- ``libs/``, ``src/``,
35``examples/``, ``port/``, ``tools/``. ``tests/`` is deliberately out of
36scope: the host unit-test harness legitimately uses ``setjmp`` / ``longjmp``
37to trap the firmware's ``[[noreturn]]`` fatal paths under test, which is test
38scaffolding rather than firmware control flow (the same reason ``tests/`` is
39exempt from MC/DC re-test and the magic-number gate). Vendored trees under
40``libs/third_party/``, generated data under ``libs/ra8_fonts/``, and build output
45 check_no_goto_setjmp.py # scan staged files (pre-commit)
46 check_no_goto_setjmp.py FILE [FILE ...] # scan an explicit file list
47 check_no_goto_setjmp.py --all # scan every tracked source file
48 check_no_goto_setjmp.py --selftest # self-check the detector
50Returns 0 on clean, 1 on findings, 2 on usage error.
53from __future__
import annotations
60from collections.abc
import Iterable
62sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
64from lint_targets
import is_build_output_path
66REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
82ROOT_DIRS = (
"libs",
"examples",
"port",
"tools",
"apps")
90 "apps/shared_libs/third_party/",
94EXTENSIONS = {
".c",
".h",
".cpp",
".hpp"}
101BANNED_RE = re.compile(
r"\b(goto|setjmp|longjmp)\b")
108def _strip_noncode(line: str) -> str:
117 nxt = line[i + 1]
if i + 1 < n
else ""
119 if c ==
"*" and nxt ==
"/":
126 if c ==
"\\" and i + 1 < n:
134 if c ==
"\\" and i + 1 < n:
141 if c ==
"/" and nxt ==
"/":
143 if c ==
"/" and nxt ==
"*":
160def scan_text(text: str) -> list[tuple[int, str, str]]:
161 """Report every code-position ``goto`` / ``setjmp`` / ``longjmp`` in ``text``.
163 Comments (both ``//`` and multi-line ``/* ... */``) and string / character
164 literals are blanked before the token match, so a banned word appearing in
165 prose is not reported -- that is the whole point of the gate, since the tree
166 legitimately names these tokens in comments.
169 text: The full source text of one translation unit.
172 One ``(line_no, token, line_text)`` tuple per finding, in file order.
174 findings: list[tuple[int, str, str]] = []
175 in_block_comment =
False
176 for n, raw
in enumerate(text.splitlines(), 1):
184 in_block_comment =
False
188 if bo != -1
and cur.find(
"*/", bo + 2) == -1:
190 in_block_comment =
True
191 code = _strip_noncode(cur)
192 findings.extend((n, m.group(1), raw.strip())
for m
in BANNED_RE.finditer(code))
196def find_violations(path: pathlib.Path) -> list[tuple[int, str, str]]:
197 """Report every banned control-flow token in one file.
199 An unreadable file yields an empty list rather than raising, matching the
200 sibling gates: a file that cannot be read is not evidence of a violation.
203 path: The source file to scan.
206 One ``(line_no, token, line_text)`` tuple per finding.
209 text = path.read_text(encoding=
"utf-8", errors=
"replace")
212 return scan_text(text)
215def needs_check(path: pathlib.Path) -> bool:
216 """Whether ``path`` is a first-party C/C++ file subject to this gate.
219 path: Candidate path, absolute or repo-relative.
222 True when the suffix is C/C++, the path is under a first-party root,
223 and it is neither vendored / generated nor build output.
225 if path.suffix.lower()
not in EXTENSIONS:
227 text = str(path).replace(
"\\",
"/")
228 if is_build_output_path(text):
230 if any(frag
in text
for frag
in EXCLUDE_FRAGMENTS):
232 rel = path.relative_to(REPO_ROOT)
if path.is_relative_to(REPO_ROOT)
else path
233 return rel.parts[0]
in ROOT_DIRS
if rel.parts
else False
236def _git_lines(args: list[str]) -> list[str]:
237 """Run a ``git`` command under the repo root and return its stdout lines.
240 args: Argument vector following ``git`` (e.g. ``["ls-files"]``).
243 Non-empty stripped stdout lines; empty on any git failure.
245 proc = subprocess.run(
252 if proc.returncode != 0:
253 sys.stderr.write(proc.stderr)
255 return [line.strip()
for line
in proc.stdout.splitlines()
if line.strip()]
258def iter_tracked_files() -> Iterable[pathlib.Path]:
259 """Yield every checkable tracked file, for the ``--all`` sweep.
261 Enumerating via ``git ls-files`` keeps gitignored build artifacts out and
262 picks up a new first-party root the day it is added -- there is no allowlist
266 An iterable of absolute paths that pass ``needs_check``.
268 for rel
in _git_lines([
"ls-files",
"--cached",
"--others",
"--exclude-standard"]):
269 path = REPO_ROOT / rel
270 if needs_check(path):
274def iter_staged_files() -> Iterable[pathlib.Path]:
275 """Yield every checkable staged file, for the default pre-commit sweep.
278 An iterable of absolute paths of added/copied/modified/renamed staged
279 files that pass ``needs_check``.
281 for rel
in _git_lines([
"diff",
"--cached",
"--name-only",
"--diff-filter=ACMR"]):
282 path = REPO_ROOT / rel
283 if needs_check(path):
309/* This routine once used a goto done; jump -- rewritten as a loop. */
310// setjmp / longjmp are banned by NASA Power-of-10 Rule 1.
311static const char *s_note = "no goto, setjmp, or longjmp here";
316 for (int goto_count = 0; goto_count < 4; ++goto_count) {
323def selftest() -> int:
324 """Prove the detector fires on real violations and is silent on prose.
327 0 when both directions hold, 1 when either fails.
329 failures: list[str] = []
331 fired = {tok
for _, tok, _
in scan_text(_BAD_FIXTURE)}
333 f
" must-fire: bad fixture did not report `{token}`"
334 for token
in (
"goto",
"setjmp",
"longjmp")
335 if token
not in fired
338 quiet = scan_text(_GOOD_FIXTURE)
340 f
" must-stay-quiet: good fixture reported `{tok}` at line {ln}" for ln, tok, _
in quiet
344 sys.stderr.write(
"check_no_goto_setjmp.py --selftest: FAILED\n")
345 sys.stderr.write(
"\n".join(failures) +
"\n")
348 print(
"check_no_goto_setjmp.py --selftest: OK (fires on code, silent on comments/strings).")
353 """Fail on any code-position ``goto`` / ``setjmp`` / ``longjmp``.
355 With no arguments the staged files are scanned, which is how the pre-commit
356 hook stays fast; ``--all`` sweeps every tracked source file; an explicit
357 file list scans exactly those. ``--selftest`` self-checks the detector.
360 1 listing each finding, 0 when clean, 2 on a usage error.
362 parser = argparse.ArgumentParser(description=__doc__)
363 parser.add_argument(
"--all", action=
"store_true", help=
"scan all tracked source files")
364 parser.add_argument(
"--selftest", action=
"store_true", help=
"self-check the detector and exit")
366 "files", nargs=
"*", type=pathlib.Path, help=
"explicit file list (e.g. staged files)"
368 args = parser.parse_args()
374 candidates = list(iter_tracked_files())
376 candidates = [p
for p
in args.files
if needs_check(p)]
378 candidates = list(iter_staged_files())
381 for path
in candidates:
382 rel = path.relative_to(REPO_ROOT)
if path.is_relative_to(REPO_ROOT)
else path
383 for line, token, snippet
in find_violations(path):
385 f
"{rel}:{line}: `{token}` is banned "
386 f
"(NASA Power-of-10 Rule 1: no goto/setjmp/longjmp): {snippet}",
393 f
"\n{total} banned control-flow token(s) found. NASA Power-of-10 "
394 "Rule 1 forbids goto, setjmp, and longjmp. Restructure the control "
395 "flow; the tokens are allowed only inside comments and string "
400 print(f
"check_no_goto_setjmp.py: {len(candidates)} file(s) scanned, 0 findings.")
404if __name__ ==
"__main__":
405 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.