ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_no_silent_stubs.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Gate: no function shall exist only to satisfy the linker.
5
6A *silent stub* is a function whose body does no work -- it discards its
7arguments and hands back a canned answer -- yet whose presence lets the build
8link and the program claim a capability it does not have. The failure mode is
9worse than a missing feature: the tool links clean, advertises support, and
10fails at runtime (or, worse, silently succeeds having done nothing).
11
12The former ``tools/rabook_imagepack/src/webp_stub.c`` and
13``apps/host/mdl/src/webp_stub.c`` were the
14motivating case. Each defined the real symbol
15``jof_priv_webp_transcode()``, discarded both arguments and returned
16``k_ra8_err_not_supported`` -- purely so the JOF producer would link
17without compiling the vendored libwebp decoder. A complete WebP decoder was
18already vendored, wrapped, tested and fuzzed in this very repository; the tools
19just were not compiling it. Both tools shipped a WebP feature that could never
20work.
21
22Calibration
23-----------
24Most no-op bodies in this tree are *legitimate* -- an early revision of this
25gate flagged 27 candidates of which 24 were correct by design:
26
27 * platform alternatives (``tools/ra8_emulator/src/display/board_view_stub.c`` is the
28 headless stand-in for the Cocoa window layer on Linux CI; it reports
29 failure honestly so callers take their headless branch);
30 * the fail-closed ``#else`` half of the placeholder-crypto guard, which
31 ``check_stub_crypto_guarded.py`` *requires* to return a hard error;
32 * callbacks matching a vtable / registry signature that genuinely have
33 nothing to do (USBX activate / deactivate hooks, an empty ISR completion
34 callback, a ra8_emulator MMIO write handler for a deliberately inert
35 peripheral);
36 * MMIO read handlers that return module state rather than a constant.
37
38A gate that fires on those is noise, and a noisy gate gets disabled -- which is
39worse than no gate. So this one does not ask "is the body empty?". It asks
40the two much narrower questions that separate the webp stubs from all 24:
41
42Rule SHADOW
43 A no-op body that provides a *second* definition of a symbol which is also
44 defined for real elsewhere in first-party code. Both definitions must
45 have external linkage: translation-unit-local `static` helpers can reuse a
46 descriptive name without presenting competing symbols to the linker. An
47 external duplicate is always a defect: whichever definition the linker
48 picks, one of them is a lie, and the build silently disables working code
49 that exists in the tree. This is exactly the webp case.
50
51Rule CANNED
52 A function that returns an explicit "unsupported / unimplemented" error
53 constant, discards every parameter, and has no other statement. Such a
54 function claims an operation was attempted and refused, when in fact no
55 implementation exists at all. Callbacks returning ``void``/``bool``/a
56 pointer, and handlers returning module state, are outside this rule by
57 construction.
58
59Hardware waiver
60---------------
61A capability whose *hardware does not physically exist yet* may legitimately
62have no implementation. Such a function is waived by carrying an explicit
63marker naming the missing part, in the form CLAUDE.md mandates::
64
65 TODO(ESP32-C6 radio module not yet on the bench): ...
66
67The marker must name something -- a bare ``TODO`` or an empty ``TODO()`` is
68rejected, so the waiver cannot become a catch-all. It is keyed on the marker
69and not on a filename pattern, so it waives exactly the function that carries
70it and nothing else in the file. Note that a waiver is *not* available to
71Rule SHADOW: if a real implementation exists in the tree, the hardware plainly
72exists too.
73
74Run::
75
76 check_no_silent_stubs.py # scan the whole tree
77 check_no_silent_stubs.py FILE ... # scan listed files
78 check_no_silent_stubs.py --selftest # prove the detector both fires and
79 # stays silent on the right inputs
80
81Exit 0 if no silent stub is found, 1 otherwise.
82"""
83
84from __future__ import annotations
85
86import argparse
87import re
88import subprocess
89import sys
90import tempfile
91from pathlib import Path
92
93# First-party roots. Both registered third-party trees (SOUP) and the generated
94# font tree are out of scope, matching every other repository gate.
95ROOTS = ("libs", "tools", "apps", "examples", "port")
96EXCLUDED = ("libs/third_party/", "apps/shared_libs/third_party/", "libs/ra8_fonts/")
97
98# Error constants that mean "this operation has no implementation". A function
99# whose entire body hands one of these back, having discarded its arguments,
100# has not refused an operation -- it never had one.
101CANNED_ERRORS = frozenset(
102 {
103 "k_ra8_err_not_supported",
104 "k_ra8_err_not_implemented",
105 "k_ra8_err_unsupported",
106 "k_ra8_err_unimplemented",
107 }
108)
109
110# A waiver must name the missing hardware: TODO(<something>). Bare TODO or
111# TODO() is deliberately not accepted.
112WAIVER_RE = re.compile(r"TODO\‍(\s*([^)]*?)\s*\‍)")
113
114# Keywords that can precede a parenthesised block but are not function
115# definitions.
116NOT_A_FUNCTION = frozenset(
117 {"if", "for", "while", "switch", "return", "sizeof", "do", "else", "catch"}
118)
119
120# A function definition: an optional return type, a name, a parameter list with
121# no nested braces or semicolons, then an opening brace. Newlines are allowed
122# between the parameter list and the brace -- the house style puts the brace of
123# a definition on its own line, and missing that made an early revision of this
124# detector report zero findings on a file that was a known stub.
125FUNCTION_RE = re.compile(
126 r"(?:^|\n)[ \t]*(?:[A-Za-z_][\w \t\*\n]*?)\b(\w+)[ \t\n]*\‍(([^;{}]*)\‍)[ \t\n]*\{",
127 re.DOTALL,
128)
129
130DISCARD_RE = re.compile(r"\‍(\s*void\s*\‍)\s*(\w+)\Z")
131RETURN_CONST_RE = re.compile(r"return\s+([A-Za-z_]\w*|-?\d+|nullptr|NULL|true|false)\Z")
132PARAM_NAME_RE = re.compile(r"(\w+)\s*(?:\‍[\s*\‍])?\s*\Z")
133
134
135def blank_noncode(text: str) -> str:
136 """Blank out comments and string/char literals, preserving offsets.
137
138 Offsets are preserved (and newlines kept) so that reported line numbers and
139 brace matching still refer to the original file.
140 """
141 out: list[str] = []
142 i, n = 0, len(text)
143 while i < n:
144 c = text[i]
145 if c == "/" and i + 1 < n and text[i + 1] == "*":
146 end = text.find("*/", i + 2)
147 end = n if end < 0 else end + 2
148 out.append(re.sub(r"[^\n]", " ", text[i:end]))
149 i = end
150 elif c == "/" and i + 1 < n and text[i + 1] == "/":
151 end = text.find("\n", i)
152 end = n if end < 0 else end
153 out.append(" " * (end - i))
154 i = end
155 elif c in "\"'":
156 j = i + 1
157 while j < n and text[j] != c:
158 j += 2 if text[j] == "\\" else 1
159 j = min(j + 1, n)
160 out.append(re.sub(r"[^\n]", " ", text[i:j]))
161 i = j
162 else:
163 out.append(c)
164 i += 1
165 return "".join(out)
166
167
168def body_span(text: str, brace_idx: int) -> tuple[int, int] | None:
169 """Return the (start, end) offsets of a function body by brace matching."""
170 depth = 0
171 for i in range(brace_idx, len(text)):
172 if text[i] == "{":
173 depth += 1
174 elif text[i] == "}":
175 depth -= 1
176 if depth == 0:
177 return (brace_idx + 1, i)
178 return None
179
180
181def param_names(params: str) -> set[str]:
182 """Extract the declared parameter NAMES from a C parameter list.
183
184 Feeds the discard test in ``classify_body``, which has to know whether a
185 ``(void)x;`` names a real parameter or some unrelated local -- so what is
186 wanted here is the identifiers, with the types thrown away.
187
188 ``void`` as the whole list yields the empty set rather than a name, which
189 is what makes a zero-parameter function fall out as "discards everything
190 it was given" and stay eligible for the CANNED rule.
191 """
192 names: set[str] = set()
193 for raw_part in params.split(","):
194 part = raw_part.strip()
195 if not part or part == "void":
196 continue
197 m = PARAM_NAME_RE.search(part)
198 if m:
199 names.add(m.group(1))
200 return names
201
202
203def classify_body(body: str, params: str) -> str | None:
204 """Return the returned constant if the body is a pure discard-and-return.
205
206 Returns None when the body does real work. A body qualifies only when every
207 statement is either a ``(void)param;`` discard or a single ``return
208 <constant>;``, AND at least one discarded name is an actual parameter --
209 that last condition is what separates "ignores its inputs" from a handler
210 that legitimately returns module state.
211 """
212 statements = [s.strip() for s in body.split(";") if s.strip()]
213 if not statements:
214 return None # a genuinely empty body is a callback shape, not a stub
215 discards: list[str] = []
216 returns: list[str] = []
217 for stmt in statements:
218 m = DISCARD_RE.match(stmt)
219 if m:
220 discards.append(m.group(1))
221 continue
222 m = RETURN_CONST_RE.match(stmt)
223 if m:
224 returns.append(m.group(1))
225 continue
226 return None # any other statement means real work
227 if len(returns) != 1:
228 return None
229 declared = param_names(params)
230 # Takes parameters: at least one must actually be discarded. That is what
231 # separates "ignores its inputs" from a handler that legitimately returns
232 # module state.
233 if declared and not (set(discards) & declared):
234 return None
235 # Takes NO parameters, so "discards every parameter" is vacuously true and
236 # the discard test above cannot speak. Only a canned-error return qualifies
237 # here: `ra8_widget_calibrate(void) { return k_ra8_err_not_supported; }` is
238 # every bit the stub its one-argument form is, but a bare `return s_state;`
239 # getter is module state and must stay silent. Requiring an empty discard
240 # list keeps a body that pokes at file-scope names out of the "pure canned
241 # return" shape.
242 if not declared and (discards or returns[0] not in CANNED_ERRORS):
243 return None
244 return returns[0]
245
246
247def waiver_in(text: str) -> str | None:
248 """Return the named missing dependency from a TODO(...) marker, if any."""
249 for m in WAIVER_RE.finditer(text):
250 named = m.group(1).strip()
251 if named:
252 return named
253 return None
254
255
256def preceding_doc(text: str, start: int) -> str:
257 """Return the ~40 lines before a definition (its comment block)."""
258 head = text[:start]
259 return "\n".join(head.splitlines()[-40:])
260
261
262def in_failclosed_crypto_guard(raw: str, offset: int) -> bool:
263 """True if the offset sits in the #else half of the placeholder-crypto guard.
264
265 ``check_stub_crypto_guarded.py`` REQUIRES those bodies to return a hard
266 error so a production image cannot ship fake crypto. Firing on them would
267 pit one gate against another.
268 """
269 guard = "defined(RA8_INSECURE_STUB_CRYPTO)"
270 if guard not in raw[:offset]:
271 return False
272 idx = raw.rfind(guard, 0, offset)
273 between = raw[idx:offset]
274 return "#else" in between and between.count("#endif") == 0
275
276
277def scan_text(raw: str, path: str) -> list[dict]:
278 """Find discard-and-return functions in one translation unit."""
279 code = blank_noncode(raw)
280 found: list[dict] = []
281 for m in FUNCTION_RE.finditer(code):
282 name, params = m.group(1), m.group(2)
283 if name in NOT_A_FUNCTION:
284 continue
285 span = body_span(code, m.end() - 1)
286 if span is None:
287 continue
288 returned = classify_body(code[span[0] : span[1]], params)
289 if returned is None:
290 continue
291 if in_failclosed_crypto_guard(raw, m.start()):
292 continue
293 region = preceding_doc(raw, m.start()) + raw[m.start() : span[1]]
294 found.append(
295 {
296 "path": path,
297 "line": code[: m.start()].count("\n") + 1,
298 "name": name,
299 "returns": returned,
300 "waiver": waiver_in(region),
301 "internal": re.search(r"\bstatic\b", code[m.start() : m.start(1)]) is not None,
302 }
303 )
304 return found
305
306
307def real_definitions(files: list[Path]) -> dict[str, list[dict[str, str | bool]]]:
308 """Map symbol -> real definitions and whether each has internal linkage."""
309 defs: dict[str, list[dict[str, str | bool]]] = {}
310 for path in files:
311 raw = path.read_text(errors="replace")
312 code = blank_noncode(raw)
313 for m in FUNCTION_RE.finditer(code):
314 name = m.group(1)
315 if name in NOT_A_FUNCTION:
316 continue
317 span = body_span(code, m.end() - 1)
318 if span is None:
319 continue
320 if classify_body(code[span[0] : span[1]], m.group(2)) is None:
321 defs.setdefault(name, []).append(
322 {
323 "path": str(path),
324 "internal": re.search(r"\bstatic\b", code[m.start() : m.start(1)])
325 is not None,
326 }
327 )
328 return defs
329
330
331def first_party_sources(explicit: list[str]) -> list[Path]:
332 """Enumerate worktree first-party .c files under ROOTS.
333
334 Tracked or untracked-but-not-ignored, not globbed. A bare rglob also sweeps
335 in build output -- every
336 configured app leaves a CMake compiler-probe TU at
337 ``<app>/build/CMakeFiles/*/CompilerIdC/CMakeCCompilerId.c`` -- so the set
338 scanned depended on whether the caller had built, and generated code got
339 held to a first-party rule. CI never saw it (it runs against a clean
340 snapshot of committed ``HEAD``) but the pre-commit hook runs in the working
341 tree, which is exactly where a spurious finding costs the most trust.
342 Including untracked files and dropping deleted tracked paths makes the gate
343 accurate during an unstaged move: it scans the destination, not a vanished
344 index-only source. Ignored build output remains excluded.
345 """
346 if explicit:
347 return [Path(p) for p in explicit]
348 try:
349 pathspec = [f"{root}/**/*.c" for root in ROOTS]
350 listed = subprocess.run( # noqa: S603 # fixed git argv, no shell
351 [ # noqa: S607 # trusted: fixed git executable
352 "git",
353 "ls-files",
354 "--cached",
355 "--others",
356 "--exclude-standard",
357 "-z",
358 "--",
359 *pathspec,
360 ],
361 capture_output=True,
362 text=True,
363 check=True,
364 ).stdout
365 except (OSError, subprocess.CalledProcessError) as exc:
366 sys.exit(
367 f"check_no_silent_stubs.py: FATAL -- cannot list tracked sources: {exc}\n"
368 " This gate enumerates via git and must not fall back to a glob:\n"
369 " a glob silently scans build output and changes the verdict."
370 )
371 return [
372 path
373 for name in listed.split("\0")
374 if name
375 if (path := Path(name)).is_file()
376 if not any(name.startswith(x) for x in EXCLUDED)
377 ]
378
379
380def _selftest_source_scope(tmp: Path) -> list[str]:
381 """Assert moved first-party sources stay in scope while both SOUP roots do not."""
382 failures: list[str] = []
383 app_source = tmp / "apps/shared_libs/compress/src/codec.c"
384 app_vendor = tmp / "apps/shared_libs/third_party/miniz/miniz.c"
385 platform_vendor = tmp / "libs/third_party/stack/source.c"
386 for path in (app_source, app_vendor, platform_vendor):
387 path.parent.mkdir(parents=True, exist_ok=True)
388 path.write_text("int source;\n", encoding="ascii")
389
390 names = [
391 "apps/shared_libs/compress/src/codec.c",
392 "apps/shared_libs/third_party/miniz/miniz.c",
393 "libs/third_party/stack/source.c",
394 ]
395 included = [name for name in names if not any(name.startswith(x) for x in EXCLUDED)]
396 if names[0] not in included:
397 failures.append(" moved app-owned first-party source fell out of scan scope")
398 if names[1] in included or names[2] in included:
399 failures.append(" a registered SOUP-root source entered first-party scan scope")
400 return failures
401
402
403def analyse(files: list[Path]) -> list[tuple[str, dict]]:
404 """Return (rule, finding) violations across the given files."""
405 defs = real_definitions(files)
406 violations: list[tuple[str, dict]] = []
407 for path in files:
408 for f in scan_text(path.read_text(errors="replace"), str(path)):
409 # Rule SHADOW: a real definition of the same symbol exists elsewhere.
410 shadowed = [
411 str(d["path"])
412 for d in defs.get(f["name"], [])
413 if d["path"] != f["path"] and not d["internal"] and not f["internal"]
414 ]
415 if shadowed:
416 f["shadows"] = shadowed
417 violations.append(("SHADOW", f))
418 continue
419 # Rule CANNED: claims an operation was refused when none exists.
420 if f["returns"] in CANNED_ERRORS and not f["waiver"]:
421 violations.append(("CANNED", f))
422 return violations
423
424
425SELFTEST_CASES: list[tuple[str, str, bool, str]] = [
426 (
427 "shadowing stub (the webp case)",
428 """
429 ra8_err_t real_thing(state_t* st, pull_t* pfx)
430 {
431 st->count += 1;
432 return do_work(st, pfx);
433 }
434 """,
435 False,
436 "real.c",
437 ),
438 (
439 "shadowing stub (the webp case)",
440 """
441 ra8_err_t real_thing(state_t* st, pull_t* pfx)
442 {
443 (void)st;
444 (void)pfx;
445 return k_ra8_err_not_supported;
446 }
447 """,
448 True,
449 "stub.c",
450 ),
451 (
452 "canned unsupported with no implementation anywhere",
453 """
454 ra8_err_t lonely_feature(uint32_t mask)
455 {
456 (void)mask;
457 return k_ra8_err_not_supported;
458 }
459 """,
460 True,
461 "canned.c",
462 ),
463 (
464 "same-named static helpers have no cross-TU linker collision",
465 """
466 static bool internal_ready(state_t* st)
467 {
468 st->checks += 1U;
469 return st->ready;
470 }
471 """,
472 False,
473 "static_real.c",
474 ),
475 (
476 "same-named static helpers have no cross-TU linker collision",
477 """
478 static bool internal_ready(state_t* st)
479 {
480 (void)st;
481 return false;
482 }
483 """,
484 False,
485 "static_canned.c",
486 ),
487 (
488 "hardware-blocked, waived by a named TODO",
489 """
490 /* TODO(ESP32-C6 radio module has been ordered but has not arrived) */
491 ra8_err_t wifi_connect(const char* ssid)
492 {
493 (void)ssid;
494 return k_ra8_err_not_supported;
495 }
496 """,
497 False,
498 "waived.c",
499 ),
500 (
501 "bare TODO is not a waiver",
502 """
503 /* TODO: wire this up later */
504 ra8_err_t someday(uint32_t x)
505 {
506 (void)x;
507 return k_ra8_err_not_supported;
508 }
509 """,
510 True,
511 "bare_todo.c",
512 ),
513 (
514 "thin wrapper delegating to a real call",
515 """
516 ra8_err_t wrapper(uint8_t* buf, size_t len)
517 {
518 return backend_write(buf, len);
519 }
520 """,
521 False,
522 "wrapper.c",
523 ),
524 (
525 "platform alternative reporting failure honestly",
526 """
527 view_t* board_view_open(uint16_t w, uint16_t h, const char* title)
528 {
529 (void)w;
530 (void)h;
531 (void)title;
532 return nullptr;
533 }
534 """,
535 False,
536 "platform.c",
537 ),
538 (
539 "intentionally empty ISR / vtable callback",
540 """
541 static void on_complete(void* ctx, uint16_t status)
542 {
543 (void)ctx;
544 (void)status;
545 }
546 """,
547 False,
548 "callback.c",
549 ),
550 (
551 "MMIO read handler returning module state",
552 """
553 static uint64_t reset1_read(uc_engine* uc, uint64_t addr, unsigned size)
554 {
555 (void)uc;
556 (void)addr;
557 (void)size;
558 return s_rstsr1;
559 }
560 """,
561 False,
562 "mmio.c",
563 ),
564 (
565 "zero-parameter canned return is still a stub",
566 """
567 ra8_err_t ra8_widget_calibrate(void)
568 {
569 return k_ra8_err_not_supported;
570 }
571 """,
572 True,
573 "zero_param.c",
574 ),
575 (
576 "zero-parameter getter returning module state",
577 """
578 uint32_t ra8_time_ms(void)
579 {
580 return s_tick_ms;
581 }
582 """,
583 False,
584 "getter.c",
585 ),
586 (
587 "fail-closed half of the placeholder-crypto guard",
588 """
589 #if defined(RA8_INSECURE_STUB_CRYPTO) || defined(RA8_OFF_TARGET)
590 ra8_err_t ra8_rsip_tamper_enable(uint32_t sources)
591 {
592 return simulate(sources);
593 }
594 #else
595 ra8_err_t ra8_rsip_tamper_enable(uint32_t sources)
596 {
597 (void)sources;
598 return k_ra8_err_not_supported;
599 }
600 #endif
601 """,
602 False,
603 "crypto.c",
604 ),
605]
606
607
608def selftest(tmp: Path) -> int:
609 """Assert the detector fires on stubs and stays silent on legitimate code."""
610 failures: list[str] = []
611 failures.extend(_selftest_source_scope(tmp))
612 groups: dict[str, list[tuple[str, str, bool]]] = {}
613 for label, body, should_fire, fname in SELFTEST_CASES:
614 groups.setdefault(label, []).append((body, fname, should_fire))
615
616 for label, cases in groups.items():
617 d = tmp / re.sub(r"\W+", "_", label)
618 d.mkdir(parents=True, exist_ok=True)
619 files = []
620 expect: dict[str, bool] = {}
621 for body, fname, should_fire in cases:
622 p = d / fname
623 p.write_text(body)
624 files.append(p)
625 expect[str(p)] = should_fire
626 fired = {v[1]["path"] for v in analyse(files)}
627 for path, should_fire in expect.items():
628 did = path in fired
629 if did != should_fire:
630 verb = "did not fire" if should_fire else "fired"
631 failures.append(f" {label} [{Path(path).name}]: gate {verb} (unexpected)")
632
633 if failures:
634 print("check_no_silent_stubs.py --selftest: FAILED\n", file=sys.stderr)
635 print("\n".join(failures), file=sys.stderr)
636 return 1
637 total = len(SELFTEST_CASES)
638 fires = sum(1 for c in SELFTEST_CASES if c[2])
639 print(
640 f"check_no_silent_stubs.py --selftest: PASS "
641 f"({total} cases: {fires} must fire, {total - fires} must stay silent)"
642 )
643 return 0
644
645
646def _scan_set_is_usable(files: list[Path]) -> bool:
647 """Whether the resolved scan set can be trusted; prints FATAL when not.
648
649 Fail loudly rather than silently passing on a broken scan: a gate that
650 reports success because it looked at nothing is worse than no gate. The
651 roots resolve relative to the current directory, so running this from
652 anywhere but the repository root finds nothing.
653 """
654 if not files:
655 print(
656 "check_no_silent_stubs.py: FATAL -- no first-party sources found.\n"
657 f"Expected .c files under {', '.join(ROOTS)} relative to the current\n"
658 "directory. Run this from the repository root.",
659 file=sys.stderr,
660 )
661 return False
662 missing = [str(p) for p in files if not p.is_file()]
663 if missing:
664 print(
665 "check_no_silent_stubs.py: FATAL -- these paths do not exist:\n "
666 + "\n ".join(missing),
667 file=sys.stderr,
668 )
669 return False
670 return True
671
672
673def _report_violations(violations: list[tuple[str, dict]]) -> None:
674 """List every stub found, then explain what each rule means and how to fix it.
675
676 The trailing prose is long on purpose: both rules reject code that
677 compiles, links and looks deliberate, so a bare file:line would leave a
678 reader with no idea why the gate objects.
679 """
680 print(
681 f"check_no_silent_stubs.py: {len(violations)} silent stub(s) found:\n",
682 file=sys.stderr,
683 )
684 for rule, f in sorted(violations, key=lambda v: (v[1]["path"], v[1]["line"])):
685 print(f" [{rule}] {f['path']}:{f['line']} {f['name']}()", file=sys.stderr)
686 if rule == "SHADOW":
687 for other in f["shadows"]:
688 print(f" real implementation lives in {other}", file=sys.stderr)
689 else:
690 print(f" discards its arguments, returns {f['returns']}", file=sys.stderr)
691 print(
692 "\n[SHADOW] A second, do-nothing definition of a symbol that is really\n"
693 "implemented elsewhere in this tree. Whichever one the linker picks, the\n"
694 "build silently disables working code. Compile the real implementation\n"
695 "instead of redefining the symbol -- if it did not link, fix the build\n"
696 "recipe, do not fake the symbol.\n"
697 "\n[CANNED] The function reports that an operation was refused when no\n"
698 "implementation exists at all. Implement it, or delete it and update\n"
699 "every call site in the same change.\n"
700 "\nIf -- and only if -- the capability is blocked on hardware that does\n"
701 "not physically exist yet, mark it with a TODO naming the missing part:\n"
702 " TODO(ESP32-C6 radio module ordered, not yet on the bench)\n"
703 "A bare TODO with no named dependency is not a waiver, and no waiver is\n"
704 "available when a real implementation already exists in the tree.",
705 file=sys.stderr,
706 )
707
708
709def main(argv: list[str]) -> int:
710 """Scan first-party C for SHADOW and CANNED stubs, or run the detector selftest.
711
712 An empty source set is FATAL rather than clean, and the message says to run
713 from the repository root: the roots are resolved relative to the current
714 directory, so invoking this from elsewhere finds nothing and would
715 otherwise report a stub-free tree it never opened.
716
717 CI runs ``--selftest`` before the scan for the same reason in the other
718 direction -- a detector whose patterns stopped matching would also report
719 a clean tree, and only an assertion in both directions can tell the two
720 apart.
721
722 Returns 0 when no stub is found, 1 on a finding, on an empty source set,
723 on a named file that does not exist, or on a failing selftest.
724 """
725 ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
726 ap.add_argument("files", nargs="*", help="specific files to scan")
727 ap.add_argument(
728 "--selftest",
729 action="store_true",
730 help="prove the detector fires on stubs and not on legitimate code",
731 )
732 args = ap.parse_args(argv[1:])
733
734 if args.selftest:
735 with tempfile.TemporaryDirectory() as td:
736 return selftest(Path(td))
737
738 files = first_party_sources(args.files)
739 if not _scan_set_is_usable(files):
740 return 1
741
742 violations = analyse(files)
743 if not violations:
744 print(f"check_no_silent_stubs.py: OK ({len(files)} files scanned, no silent stubs)")
745 return 0
746
747 _report_violations(violations)
748 return 1
749
750
751if __name__ == "__main__":
752 sys.exit(main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157