ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_pointer_boilerplate.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"""Reject generated pointer-only definition comments in apps and examples.
5
6Application and example definitions inherit their contracts from declarations.
7The exact ``see header for the documented contract`` sentence adds no useful
8information and was emitted repeatedly during the source-layout migration.
9Legacy library wording is outside this narrow regression guard.
10"""
11
12from __future__ import annotations
13
14import re
15import shutil
16import subprocess
17import sys
18from pathlib import Path
19
20REPO_ROOT = Path(__file__).resolve().parents[2]
21SCOPED_PREFIXES = ("apps/", "examples/")
22SOURCE_SUFFIXES = frozenset({".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".m", ".mm"})
23MIN_SCOPED_FILES = 850
24BANNED_RE = re.compile(
25 r"^\s*/\*\s*see (?:the )?(?:internal )?header for the documented contract\.\s*\*/\s*$",
26 re.IGNORECASE,
27)
28
29
30def is_banned(line: str) -> bool:
31 """Return whether one source line is the generated pointer-only comment."""
32 return BANNED_RE.fullmatch(line) is not None
33
34
35def scoped_files() -> list[str]:
36 """Return present tracked and untracked application/example source files."""
37 git = shutil.which("git") or "git"
38 result = subprocess.run( # noqa: S603 -- resolved Git executable, fixed arguments
39 [git, "ls-files", "--cached", "--others", "--exclude-standard", "-z"],
40 cwd=REPO_ROOT,
41 check=True,
42 capture_output=True,
43 )
44 selected = set()
45 for rel in result.stdout.decode("utf-8", errors="strict").split("\0"):
46 path = REPO_ROOT / rel
47 if (
48 rel.startswith(SCOPED_PREFIXES)
49 and path.suffix.lower() in SOURCE_SUFFIXES
50 and path.is_file()
51 ):
52 selected.add(rel)
53 return sorted(selected)
54
55
56def scan(rels: list[str]) -> list[str]:
57 """Return every path and line carrying the banned generated sentence."""
58 findings: list[str] = []
59 for rel in rels:
60 text = (REPO_ROOT / rel).read_text(encoding="utf-8")
61 findings.extend(scan_text(rel, text))
62 return findings
63
64
65def scan_text(rel: str, text: str) -> list[str]:
66 """Run the CI scanner over one named source text."""
67 return [
68 f"{rel}:{number}"
69 for number, line in enumerate(text.splitlines(), start=1)
70 if is_banned(line)
71 ]
72
73
74def selftest() -> int:
75 """Drive the CI scanner through must-fire and must-stay-quiet source texts."""
76 cases = (
77 ("/* see header for the documented contract. */", True, "plain generated form fires"),
78 (
79 "/* See the internal header for the documented contract. */",
80 True,
81 "internal-header generated form fires",
82 ),
83 ("/* see header for full description */", False, "legacy wording stays quiet"),
84 (
85 "/* See header for the documented contract -- bounded scan. */",
86 False,
87 "an implementation-specific note stays quiet",
88 ),
89 (
90 'const char* text = "see header for the documented contract.";',
91 False,
92 "a string literal stays quiet",
93 ),
94 )
95 failures = [
96 label
97 for line, expected, label in cases
98 if bool(scan_text("apps/example/src/main.c", line)) != expected
99 ]
100 if failures:
101 for failure in failures:
102 print(f"check_pointer_boilerplate.py --selftest: FAIL: {failure}", file=sys.stderr)
103 return 1
104 print(f"check_pointer_boilerplate.py --selftest: PASS ({len(cases)} both-direction cases)")
105 return 0
106
107
108def main() -> int:
109 """Run the detector self-test or scan the live source tree."""
110 if sys.argv[1:] == ["--selftest"]:
111 return selftest()
112 if sys.argv[1:]:
113 print("usage: check_pointer_boilerplate.py [--selftest]", file=sys.stderr)
114 return 2
115 try:
116 rels = scoped_files()
117 findings = scan(rels)
118 except (OSError, subprocess.CalledProcessError, UnicodeError) as exc:
119 print(f"check_pointer_boilerplate.py: cannot scan source tree: {exc}", file=sys.stderr)
120 return 2
121 if len(rels) < MIN_SCOPED_FILES:
122 print(
123 f"check_pointer_boilerplate.py: scope collapsed to {len(rels)} file(s); "
124 f"expected at least {MIN_SCOPED_FILES}",
125 file=sys.stderr,
126 )
127 return 2
128 if findings:
129 print("Generated pointer-only definition comment(s):", file=sys.stderr)
130 for finding in findings:
131 print(f" {finding}", file=sys.stderr)
132 print("Delete the comment; the declaration owns the contract.", file=sys.stderr)
133 return 1
134 print(f"check_pointer_boilerplate.py: clean ({len(rels)} app/example source files)")
135 return 0
136
137
138if __name__ == "__main__":
139 raise SystemExit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298