4"""Gate: examples shall not hand-encode board connector pins.
6The EK-RA8D2 pinout is a fixed board fact, and the board layer
7(``libs/ra8_board_ek_ra8d2``) is its single source of truth: it exposes the
8USB, I2C/I3C, SD, Ethernet, audio, console, LED and switch pins as
9``k_ra8_board_*`` symbols (and accessors like ``ra8_board_sw_pin``). When an
10application instead re-encodes a pin as ``(port << 8) | pin`` it duplicates
11that fact -- and #251 showed the cost: the four USB-FS pins were copy-pasted,
12byte-identical, across 29 apps under a dozen different local names, so a pin
13correction in one silently skipped the rest.
15This gate forbids the ``((uint16_t)k_ra8_port_N << 8) | (uint16_t)k_ra8_pin_M``
16board-pin encoding idiom anywhere under ``examples/``. The fix is always to
17reference the board symbol, or -- if the pin is a real board connector the
18board layer does not expose yet -- to add it there first, then reference it.
20Scope note: this targets the specific *encoding* idiom that was duplicated,
21not every pin literal. A genuinely app-specific pin (e.g. where an external
22motor driver is wired, which is not an EK-RA8D2 board fact and has no board
23home) is out of scope by construction -- it does not use this idiom.
27 check_example_board_pins.py # scan examples/
28 check_example_board_pins.py path/to/main.c ... # scan listed files
29 check_example_board_pins.py --selftest # prove both directions
31Exit 0 if no example hand-encodes a board pin, exit 1 (with the offenders)
32otherwise, exit 2 when the whole-tree sweep collapses below FILE_FLOOR.
35from __future__
import annotations
39from collections.abc
import Iterable
40from pathlib
import Path
42sys.path.insert(0, str(Path(__file__).resolve().parent))
44from lint_targets
import is_build_output_path
45from selftest_assert
import expect, report
47REPO_ROOT = Path(__file__).resolve().parents[2]
49SOURCE_SUFFIXES = (
".c",
".h",
".cpp",
".hpp")
53ENCODING_RE = re.compile(
r"k_ra8_port_\d+\s*<<\s*8\s*\)\s*\|\s*\(\s*uint16_t\s*\)\s*k_ra8_pin_\d+")
68def _is_source(path: Path) -> bool:
69 return path.suffix
in SOURCE_SUFFIXES
72def _rel(path: Path) -> str:
73 if path.is_relative_to(REPO_ROOT):
74 return str(path.relative_to(REPO_ROOT))
78def _enumerate_targets(arg_paths: Iterable[str]) -> list[Path]:
79 args = list(arg_paths)
84 if not path.is_absolute():
85 path = REPO_ROOT / path
87 for suffix
in SOURCE_SUFFIXES:
88 out.extend(path.rglob(
"*" + suffix))
89 elif _is_source(path):
91 return [p
for p
in out
if not is_build_output_path(p)]
94 for suffix
in SOURCE_SUFFIXES:
95 out.extend((REPO_ROOT / SCAN_ROOT).rglob(
"*" + suffix))
96 return [p
for p
in out
if not is_build_output_path(p)]
100 """Prove the detector fires on the idiom and that build output is excluded.
102 Three things have to hold at once for a clean run to mean anything: the
103 encoding regex must FIRE on the ``(port << 8) | pin`` shape and stay QUIET
104 on a board-symbol reference, the enumeration must DROP an in-source build
105 file (the scope defect #549 fixed) while KEEPING a real example source, and
106 the live sweep must clear ``FILE_FLOOR`` so a collapsed scope cannot pass as
110 0 when every assertion held in both directions, 1 otherwise.
112 failures: list[str] = []
114 idiom =
" cfg.pin = ((uint16_t)k_ra8_port_6 << 8) | (uint16_t)k_ra8_pin_11;"
115 board_ref =
" cfg.pin = ra8_board_sw_pin(k_ra8_board_sw_user);"
116 expect(bool(ENCODING_RE.search(idiom)),
"MUST FIRE: hand-encoded (port << 8) | pin", failures)
118 not ENCODING_RE.search(board_ref),
119 "MUST NOT FIRE: a board-symbol reference",
124 str(p)
for p
in _enumerate_targets([
"examples/x/build/gen.c",
"examples/x/src/main.c"])
127 any(p.endswith(
"examples/x/src/main.c")
for p
in enumerated),
128 "MUST FIRE: a real example source is enumerated",
132 not any(p.endswith(
"examples/x/build/gen.c")
for p
in enumerated),
133 "MUST NOT FIRE: an in-source build file is excluded from the scope",
137 live = _enumerate_targets([])
139 len(live) >= FILE_FLOOR,
140 f
"live sweep sees {len(live)} example file(s) (floor {FILE_FLOOR})",
144 all(
not is_build_output_path(p)
for p
in live),
145 "no enumerated file lives in a build tree",
148 return report(failures)
151def main(argv: list[str]) -> int:
152 """Fail any example that spells a board pin as an inline ``(port << 8) | pin``.
154 The match is textual, on the encoding SHAPE rather than on known pin
155 values, because the defect #251 found was the same four USB-FS pins
156 copy-pasted across 29 apps under a dozen different local names -- a value
157 allowlist would have had to know all twelve names, while the shift-or
158 pattern catches the next one regardless of what it is called.
160 Undecodable bytes are replaced rather than raising, so one bad file cannot
161 abort the sweep; encoding is check-encoding's gate, not this one's.
163 FILE_FLOOR applies to the whole-tree sweep ONLY, and exits 2 below it. An
164 argv file list comes from the pre-commit hook and legitimately filters to
165 nothing when a commit touches no example source, so an empty list there is
166 a real answer; an empty SWEEP is a broken enumeration reporting a clean
167 tree because it read nothing.
169 Returns 1 listing each site, 0 when clean or when argv filtered to nothing,
170 2 when the whole-tree sweep enumerated too few files to trust.
172 if "--selftest" in argv[1:]:
175 targets = _enumerate_targets(paths)
176 if not paths
and len(targets) < FILE_FLOOR:
178 f
"check_example_board_pins.py: FATAL -- only {len(targets)} example file(s) "
179 f
"in scope, floor is {FILE_FLOOR}. A collapsed sweep reports a clean tree "
180 "because it scanned nothing.",
185 print(
"check_example_board_pins.py: no files to scan", file=sys.stderr)
191 text = path.read_text(encoding=
"utf-8", errors=
"replace")
194 for lineno, line
in enumerate(text.splitlines(), 1):
195 if ENCODING_RE.search(line):
196 hits.append((_rel(path), lineno, line.strip()))
200 f
"check_example_board_pins.py: {len(targets)} example file(s) "
201 "scanned, none hand-encode a board pin."
206 f
"check_example_board_pins.py: {len(hits)} hand-encoded board pin(s) in examples:\n",
209 for path, lineno, snippet
in hits:
210 print(f
" {path}:{lineno} {snippet}", file=sys.stderr)
212 "\nThe EK-RA8D2 pinout belongs to the board layer, not to each app.\n"
213 "Reference the board symbol (k_ra8_board_*_pin_*, or an accessor like\n"
214 "ra8_board_sw_pin) instead of re-encoding (port << 8 | pin). If the pin\n"
215 "is a real board connector the board layer does not expose yet, add it\n"
216 "to libs/ra8_board_ek_ra8d2 first, then reference it here.",
222if __name__ ==
"__main__":
223 sys.exit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.