ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_magic_numbers.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 magic numbers -- every integer literal belongs in a named enum.
5
6Floating-point constants (which C enums cannot hold) use a
7``const``. Macros are NOT an acceptable home for an integer constant
8(CLAUDE.md "Constants and Macros": enums always, macros only for code
9de-duplication, conditional compilation, or build-configuration flags).
10
11The project's `.clang-tidy` already configures
12``readability-magic-numbers``, but clang-tidy only checks files that are
13present in the build's ``compile_commands.json``. ``clang_tidy.sh`` runs
14against the host unit-test build, which drops every ARM-cross-compiled
15translation unit and -- crucially -- contains **no** example ``src/main.c``
16at all. The result was that every ``examples/<tier>/.../<app>/src/main.c``
17was invisible to the magic-number check (a bare ``ra8_delay_ms(500U)``
18sailed straight through both the pre-commit hook and CI).
19
20This checker is a backstop that walks the source text directly so the
21rule is enforced for **every** ``.c`` file under ``libs/``, ``port/``,
22and ``examples/`` regardless of which compile database it
23ended up in. It is the magic-number analogue of
24``check_function_size.py``.
25
26Detection rules (kept deliberately aligned with the ``.clang-tidy``
27``readability-magic-numbers`` configuration):
28
29* Numeric literals (decimal, ``0x`` hex, ``0b`` binary, octal, and
30 floating-point, with optional ``u``/``l``/``f`` suffixes) are flagged.
31* The ignored-value set matches ``.clang-tidy``:
32 integers ``0;1;2;3;4;6;8;16;32`` and floats ``0.0;1.0``.
33* Literals inside an ``enum`` body are the *allowed* home for a constant
34 and are never flagged.
35* Preprocessor directives (``#define``, ``#if`` ...) are skipped here --
36 but note that an object-like ``#define FOO 500`` is itself a policy
37 violation (integer constants must be enums); the pre-commit hook's
38 "bare numeric #define" gate is what flags that pattern.
39* Numbers embedded in identifiers (``uint8_t``, ``crc32``, ``s_buf2``)
40 are not literals and are never flagged.
41* Comments and string / character literals are stripped before scanning.
42* clang-tidy ``NOLINT`` suppressions are honoured: a
43 ``NOLINT`` / ``NOLINTNEXTLINE`` / ``NOLINTBEGIN`` .. ``NOLINTEND`` that
44 is bare or lists ``readability-magic-numbers`` (or the
45 ``cppcoreguidelines-avoid-magic-numbers`` alias) suppresses the same
46 lines a clang-tidy run would. A backstop for clang-tidy must respect
47 clang-tidy's own waiver mechanism -- the codebase uses it heavily to
48 exempt the crypto, JPEG-codec, and BLE-host translation units.
49
50Per-line opt-out: append ``MAGIC-OK: <reason>`` to a line to suppress
51it (mirrors the ``LEGACY-OK`` / ``WAVE-OK`` / ``AI-OK`` opt-outs used by
52the sibling gates). Use it sparingly and only with a written reason;
53prefer a scoped ``NOLINT`` where a whole block is genuinely exempt.
54
55Run::
56
57 check_magic_numbers.py # scan the whole tree
58 check_magic_numbers.py path/to/file.c ... # scan listed files
59
60Exit 0 if no magic numbers remain, exit 1 (with a diagnostic table) if
61any are found.
62"""
63
64from __future__ import annotations
65
66import math
67import re
68import shutil
69import subprocess
70import sys
71import tempfile
72from collections.abc import Iterable
73from pathlib import Path
74
75sys.path.insert(0, str(Path(__file__).resolve().parent))
76
77from lint_targets import is_build_output_path
78
79REPO_ROOT = Path(__file__).resolve().parents[2]
80
81# Under ``examples/`` only the application ``src/main.c`` was once scanned -- this
82# matches the scope ``clang_tidy.sh`` already uses for examples. The
83# per-app boot boilerplate (``vector_table.c``, ``system_init.c``,
84# ``secure_exception.c``, ``trustzone_init.c``) is copied verbatim into
85# every app and is full of sequential IRQ-slot indices and fixed vector
86# addresses; flagging it would bury real application magic numbers under
87# ~200 boilerplate hits per app. Pass such a file explicitly to scan it.
88# Per-app boot boilerplate -- copied verbatim into every app under examples/
89# AND into the firmware products under apps/, and full of sequential IRQ-slot
90# indices and fixed vector addresses. Exempt by filename anywhere (not just
91# examples/), matching the former examples/<app>/src/main.c-only scope. Pass such a
92# file explicitly to scan it.
93BOOT_BOILERPLATE = frozenset(
94 {"vector_table.c", "system_init.c", "secure_exception.c", "trustzone_init.c"}
95)
96
97# Path fragments that exclude the file from the scan. Vendor / build
98# trees are SOUP and exempt; their literals are the upstream
99# maintainer's call, not ours. Test code is exempt too: unit tests are
100# built from literal stimulus and expected-value vectors
101# (``TEST_ASSERT_EQ(0x9D5A1A, ...)``) -- naming every test constant as a
102# typed enum would obscure the vectors, not clarify them.
103EXCLUDE_FRAGMENTS = (
104 "libs/third_party/",
105 "apps/shared_libs/third_party/",
106 "libs/ra8_fonts/",
107 "port/threadx/",
108 "tests/",
109)
110
111# Integer values exempt from the rule. Mirrors
112# ``.clang-tidy``: readability-magic-numbers.IgnoredIntegerValues.
113IGNORED_INT = {0, 1, 2, 3, 4, 6, 8, 16, 32}
114
115# Floating-point values exempt from the rule. Mirrors
116# ``.clang-tidy``: readability-magic-numbers.IgnoredFloatingPointValues.
117IGNORED_FLOAT = {0.0, 1.0}
118
119# Suffixes this gate owns. HEADERS ARE IN SCOPE: they carry array extents,
120# fixture lengths, geometry constants and -- in one HAL header -- four bare
121# absolute register addresses, and none of that was ever scanned while the
122# enumeration stopped at *.c. Widening it found 23 real findings that had sat
123# unflagged, four of them exactly the "hardware register addresses as literals"
124# construct CLAUDE.md forbids by name. Keeping headers out would also make the
125# clang-tidy de-duplication in scripts/checks/tidy/pass_args.sh dishonest: that
126# switches readability-magic-numbers off on the grounds that THIS checker owns
127# the rule over the same files, which is only true if it reads the same files.
128SOURCE_SUFFIXES = (".c", ".h")
129
130# Per-line opt-out marker.
131OPT_OUT = "MAGIC-OK"
132_OPT_OUT_COMMENT_RE = re.compile(r"(?P<comment>//|/\*+<?)\s*MAGIC-OK\s*:\s*(?P<reason>.*)$")
133
134# clang-tidy check names whose NOLINT suppressions this gate honours. The
135# project uses NOLINTBEGIN/END(readability-magic-numbers, ...) extensively
136# to waive whole files / functions (crypto, JPEG codec, BLE host, ...); a
137# backstop for clang-tidy must respect clang-tidy's own suppression
138# mechanism or it would override author-sanctioned exemptions.
139MAGIC_CHECK_NAMES = (
140 "readability-magic-numbers",
141 "cppcoreguidelines-avoid-magic-numbers",
142)
143
144_NOLINT_BEGIN_RE = re.compile(r"NOLINTBEGIN(?:\‍(([^)]*)\‍))?")
145_NOLINT_END_RE = re.compile(r"NOLINTEND(?:\‍(([^)]*)\‍))?")
146_NOLINT_NEXT_RE = re.compile(r"NOLINTNEXTLINE(?:\‍(([^)]*)\‍))?")
147_NOLINT_INLINE_RE = re.compile(r"NOLINT(?!BEGIN|END|NEXTLINE)(?:\‍(([^)]*)\‍))?")
148
149
150def _nolint_applies(arg: str | None) -> bool:
151 """Whether a NOLINT marker suppresses the magic-number check specifically.
152
153 A bare ``NOLINT`` with no argument list suppresses EVERY check, so it
154 counts here too -- reading it as "suppresses some other check" would let
155 a blanket suppression silently cover this one.
156 """
157 if arg is None or arg.strip() == "":
158 return True
159 return any(name in arg for name in MAGIC_CHECK_NAMES)
160
161
162# A numeric literal: hex, binary, octal, float, or decimal, each with an
163# optional integer/float suffix. Order matters -- hex/binary/float must
164# be tried before the bare-decimal alternative.
165_NUM_RE = re.compile(
166 r"""
167 (?P<hex>0[xX][0-9a-fA-F]+(?:[uUlL]*))
168 | (?P<bin>0[bB][01]+(?:[uUlL]*))
169 | (?P<flt>(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?[fF])
170 | (?P<dec_flt>(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?[lL]?)
171 | (?P<dec>\d+(?:[uUlL]*))
172 """,
173 re.VERBOSE,
174)
175
176# Characters that, immediately before a match, mean the digits are part
177# of an identifier (uint8_t, crc32) or a larger numeric token rather
178# than a standalone literal.
179_IDENT_BEFORE = re.compile(r"[0-9A-Za-z_.]")
180
181# A "data row": a line (after comment/string stripping) that holds only
182# numeric literals, commas, braces, signs and whitespace -- i.e. a row
183# of a const lookup table (font glyphs, JPEG quant tables, crypto
184# S-boxes, MIPI PHY register sequences). These are data, not logic; the
185# magic-number rule is meaningless for them, exactly as clang-tidy never
186# saw them (the data-table TUs are absent from the host compile-db). A
187# real magic number always rides on an identifier or operator
188# (`ra8_delay_ms(500U)`, `buf[0] = 500`, `x << 7`), which breaks the
189# pattern below and is still flagged.
190_DATA_ROW_RE = re.compile(r"^[\s{}\‍[\‍],0-9a-fA-FxXuUlL.+\-]*$")
191
192# A declaration line carries a storage class, qualifier, or type token.
193# clang-tidy ignores an array-*dimension* literal in a declaration
194# (``uint8_t z[64]``) but still flags an array *subscript* (``b[5]``);
195# the dimension only counts as a magic number on a declaration line.
196_DECL_HINT = re.compile(
197 r"\b(static|const|extern|volatile|register|struct|union|enum|"
198 r"unsigned|signed|void|char|short|int|long|float|double|bool|"
199 r"[A-Za-z_][A-Za-z0-9_]*_t)\b" # uint8_t, int32_t, size_t, my_type_t, ...
200)
201
202# A `const` float/double definition is the sanctioned home for a
203# floating-point constant (C enums cannot hold floats -- see CLAUDE.md
204# "Constants and Macros"). A float literal initialising such a
205# definition is the named value itself, analogous to an enum member, and
206# is not a magic number -- exactly as `1.0`/`0.0` are already ignored.
207_CONST_FLOAT_DEF = re.compile(r"\bconst\b")
208_FLOAT_TYPE = re.compile(r"\b(float|double)\b")
209
210
211def _strip_comments_and_strings(text: str) -> str:
212 """Blank comment and literal bytes to spaces, preserving every position.
213
214 Newlines survive, so line AND column numbers reported against the blanked
215 view remain valid for the original source -- this gate reports columns,
216 not just lines, so length preservation is required and not merely tidy.
217 """
218 out: list[str] = []
219 i = 0
220 n = len(text)
221 while i < n:
222 c = text[i]
223 nxt = text[i + 1] if i + 1 < n else ""
224 if c == "/" and nxt == "/":
225 while i < n and text[i] != "\n":
226 i += 1
227 continue
228 if c == "/" and nxt == "*":
229 out.append(" ")
230 i += 2
231 while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"):
232 out.append("\n" if text[i] == "\n" else " ")
233 i += 1
234 if i < n:
235 out.append(" ")
236 i += 2
237 continue
238 if c in {'"', "'"}:
239 quote = c
240 out.append(" ")
241 i += 1
242 while i < n and text[i] != quote:
243 if text[i] == "\\" and i + 1 < n:
244 out.append(" ")
245 i += 2
246 continue
247 out.append("\n" if text[i] == "\n" else " ")
248 i += 1
249 if i < n:
250 out.append(" ")
251 i += 1
252 continue
253 out.append(c)
254 i += 1
255 return "".join(out)
256
257
258def _literal_value(match: re.Match) -> tuple[bool, float]:
259 """Decode a numeric-literal match to ``(is_float, value)``.
260
261 Returns ``(False, NaN)`` for anything un-parseable, and the caller treats
262 NaN as "do not flag" -- an unrecognised literal form is not evidence of a
263 magic number.
264
265 Suffix stripping is type-aware: ``f`` is never stripped from a hex
266 literal, where it is a digit (0xAF) rather than a float suffix.
267 """
268 raw = match.group(0)
269 group = match.lastgroup
270 # Strip only true literal suffixes -- and never strip `f` from a hex
271 # literal, where it is a hex digit (0xAF), not a float suffix.
272 if group == "hex":
273 body = raw.rstrip("uUlL")
274 return False, float(int(body, 16))
275 if group == "bin":
276 body = raw.rstrip("uUlL")
277 return False, float(int(body, 2))
278 if group in ("flt", "dec_flt"):
279 body = raw.rstrip("fFlL")
280 try:
281 return True, float(body)
282 except ValueError:
283 return False, float("nan")
284 body = raw.rstrip("uUlL")
285 # Leading-zero decimals are octal in C (0700 == 448); fall back to
286 # base 8, then base 10, before giving up.
287 for base in (0, 8, 10):
288 try:
289 return False, float(int(body, base))
290 except ValueError: # per-base fallback; loop is only 3 iterations
291 continue
292 return False, float("nan")
293
294
295def _is_array_dimension(code_line: str, start: int, end: int) -> bool:
296 """Whether a literal is an array dimension rather than a magic number.
297
298 True only when it is the SOLE content of a ``[ ... ]`` on a declaration
299 line. Matching clang-tidy's treatment here is deliberate: two gates
300 disagreeing about the same literal is worse than either rule alone.
301 """
302 i = start - 1
303 while i >= 0 and code_line[i].isspace():
304 i -= 1
305 if i < 0 or code_line[i] != "[":
306 return False
307 j = end
308 while j < len(code_line) and code_line[j].isspace():
309 j += 1
310 if j >= len(code_line) or code_line[j] != "]":
311 return False
312 return bool(_DECL_HINT.search(code_line))
313
314
315def _is_ignored(is_float: bool, value: float) -> bool:
316 if math.isnan(value): # could not parse, do not flag
317 return True
318 if is_float:
319 return value in IGNORED_FLOAT
320 return int(value) in IGNORED_INT
321
322
323class _SuppressionState:
324 """Tracks clang-tidy NOLINT scope across the lines of one file.
325
326 Honours the project's own readability-magic-numbers waivers, so a literal
327 this gate would flag but clang-tidy is already told to ignore is not
328 reported twice under two different names.
329 """
330
331 def __init__(self) -> None:
332 self.in_region = False # inside a magic-relevant NOLINTBEGIN/END block
333 self.skip_next = False # previous line was a magic-relevant NOLINTNEXTLINE
334
335 def suppresses(self, raw: str) -> bool:
336 """True when ``raw`` is inside, or is itself, a NOLINT suppression."""
337 m_beg = _NOLINT_BEGIN_RE.search(raw)
338 if m_beg and _nolint_applies(m_beg.group(1)):
339 self.in_region = True
340 m_end = _NOLINT_END_RE.search(raw)
341 if m_end and _nolint_applies(m_end.group(1)):
342 self.in_region = False
343 if self.in_region or m_beg or m_end:
344 return True
345 if self.skip_next:
346 self.skip_next = False
347 return True
348 m_nl = _NOLINT_NEXT_RE.search(raw)
349 if m_nl and _nolint_applies(m_nl.group(1)):
350 self.skip_next = True
351 return True
352 m_inl = _NOLINT_INLINE_RE.search(raw)
353 return bool(m_inl and _nolint_applies(m_inl.group(1)))
354
355
356class _EnumState:
357 """Tracks brace depth inside an enum body.
358
359 Literals inside an enum ARE the named constants this gate exists to
360 require, so the body is skipped rather than reported.
361 """
362
363 def __init__(self) -> None:
364 self.depth = 0
365 self.pending = False # saw `enum`, awaiting its `{`
366
367 def inside(self, code_line: str) -> bool:
368 """Advance the tracker over ``code_line``; True if it is enum body."""
369 if self.depth == 0 and re.search(r"\benum\b", code_line):
370 self.pending = True
371 if not (self.pending or self.depth > 0):
372 return False
373 opens = code_line.count("{")
374 closes = code_line.count("}")
375 if self.pending and opens > 0:
376 self.pending = False
377 self.depth += opens - closes
378 return True # the `... enum ... {` line itself is a definition
379 self.depth = max(self.depth + opens - closes, 0)
380 return self.depth > 0
381
382
383def _line_literals(code_line: str) -> list[tuple[int, str]]:
384 """Return (column, literal) for every magic number on one line of code."""
385 out: list[tuple[int, str]] = []
386 for m in _NUM_RE.finditer(code_line):
387 start = m.start()
388 if start > 0 and _IDENT_BEFORE.match(code_line[start - 1]):
389 continue
390 if _is_array_dimension(code_line, start, m.end()):
391 continue
392 is_float, value = _literal_value(m)
393 if _is_ignored(is_float, value):
394 continue
395 if is_float and _CONST_FLOAT_DEF.search(code_line) and _FLOAT_TYPE.search(code_line):
396 continue # named const float/double definition
397 out.append((start + 1, m.group(0)))
398 return out
399
400
401def _trailing_c_comment(orig_line: str) -> str:
402 """Return the real trailing C comment, ignoring comment-like strings."""
403 quote = ""
404 escaped = False
405 index = 0
406 while index < len(orig_line):
407 char = orig_line[index]
408 if escaped:
409 escaped = False
410 elif quote and char == "\\":
411 escaped = True
412 elif quote and char == quote:
413 quote = ""
414 elif not quote and char in {'"', "'"}:
415 quote = char
416 elif not quote and orig_line.startswith("//", index):
417 return orig_line[index:]
418 elif not quote and orig_line.startswith("/*", index):
419 end = orig_line.find("*/", index + 2)
420 if end < 0:
421 return ""
422 end += 2
423 if not orig_line[end:].strip():
424 return orig_line[index:end]
425 index = end - 1
426 index += 1
427 return ""
428
429
430def _has_reasoned_opt_out(orig_line: str) -> bool:
431 """Accept only a trailing C comment carrying ``MAGIC-OK: <reason>``."""
432 match = _OPT_OUT_COMMENT_RE.fullmatch(_trailing_c_comment(orig_line))
433 if match is None:
434 return False
435 reason = match.group("reason").strip()
436 if match.group("comment").startswith("/*"):
437 if not reason.endswith("*/"):
438 return False
439 reason = reason[:-2].strip()
440 return bool(reason)
441
442
443def _skip_line(code_line: str, orig_line: str, enums: _EnumState) -> bool:
444 """True when this line cannot hold a reportable literal."""
445 if enums.inside(code_line):
446 return True
447 stripped = code_line.lstrip()
448 # Preprocessor directives are governed by other gates.
449 if stripped.startswith("#"):
450 return True
451 # Pure data rows of a const lookup table are not logic.
452 if stripped and _DATA_ROW_RE.match(code_line):
453 return True
454 # Per-line opt-out (checked against the original source line).
455 return _has_reasoned_opt_out(orig_line)
456
457
458def _scan_file(path: Path) -> list[tuple[int, int, str]]:
459 """Return a list of (line, column, literal) for every magic number in `path`."""
460 try:
461 text = path.read_text()
462 except (OSError, UnicodeDecodeError):
463 return []
464
465 code_lines = _strip_comments_and_strings(text).splitlines()
466 orig_lines = text.splitlines()
467
468 violations: list[tuple[int, int, str]] = []
469 nolint = _SuppressionState()
470 enums = _EnumState()
471
472 for idx, code_line in enumerate(code_lines):
473 raw = orig_lines[idx] if idx < len(orig_lines) else ""
474 if nolint.suppresses(raw):
475 continue
476 if _skip_line(code_line, raw, enums):
477 continue
478 violations.extend((idx + 1, col, lit) for col, lit in _line_literals(code_line))
479
480 return violations
481
482
483def _is_excluded(path: Path) -> bool:
484 p = str(path)
485 return is_build_output_path(p) or any(frag in p for frag in EXCLUDE_FRAGMENTS)
486
487
488def _enumerate_targets(arg_paths: Iterable[str]) -> list[Path]:
489 """Resolve the list of files to scan from CLI arguments."""
490 args = [a for a in arg_paths if a not in ("--check", "--all")]
491 if args:
492 out: list[Path] = []
493 for raw in args:
494 p = Path(raw)
495 if not p.is_absolute():
496 p = REPO_ROOT / p
497 if p.is_dir():
498 out.extend(p.rglob("*.c"))
499 out.extend(p.rglob("*.h"))
500 elif p.suffix in SOURCE_SUFFIXES:
501 out.append(p)
502 return [p for p in out if not _is_excluded(p)]
503
504 # No-argument mode scans every GIT-VISIBLE .c and .h so nothing first-party
505 # is silently skipped (new top-level dirs, tools/, port/, etc. are covered
506 # automatically -- there is no allowlist to forget to update).
507 # Enumerating via ``git ls-files`` (tracked plus untracked-but-not-
508 # ignored) instead of a filesystem walk keeps gitignored artifacts out:
509 # a raw rglob scanned stray CMake compiler-probe files under app
510 # build-*/ dirs and the .claude/worktrees checkouts, failing commits on
511 # files CI can never see. The per-app boot boilerplate (vector_table.c,
512 # system_init.c, ...) is copied verbatim into every app and is full of
513 # vector/IRQ-slot indices, so it is dropped BY FILENAME via
514 # BOOT_BOILERPLATE. Vendor trees are dropped via EXCLUDE_FRAGMENTS.
515 #
516 # There is deliberately no longer an "examples/ scans only main.c" rule.
517 # BOOT_BOILERPLATE already names the copied boot files precisely, so that
518 # extra filename test protected nothing -- it silently dropped every OTHER
519 # example TU: each app's src/*.c, plus cpu1_main.c, ns_main.c and ns_usb.c.
520 # 47 real magic numbers were sitting in those files, among them a whole
521 # block of hand-addressed Armv8-M SAU registers, while this gate reported
522 # the tree clean. Scope by what a file IS, never by what it is named
523 # (the #296 / #332 / #358 / #369 defect family).
524 git_tool = shutil.which("git") or "git"
525 proc = subprocess.run( # noqa: S603 -- fixed argv, trusted tool path
526 [git_tool, "ls-files", "--cached", "--others", "--exclude-standard", "--", "*.c", "*.h"],
527 cwd=REPO_ROOT,
528 capture_output=True,
529 text=True,
530 check=False,
531 )
532 if proc.returncode != 0:
533 sys.stderr.write(proc.stderr)
534 sys.stderr.write(f"git ls-files failed (exit {proc.returncode})\n")
535 sys.exit(2)
536 out = []
537 for line in proc.stdout.splitlines():
538 rel = line.strip()
539 if not rel:
540 continue
541 c = REPO_ROOT / rel
542 if c.name in BOOT_BOILERPLATE:
543 continue
544 out.append(c)
545 return [p for p in out if not _is_excluded(p)]
546
547
548def _run_selftest() -> int:
549 """Prove both halves of the reasoned ``MAGIC-OK`` contract."""
550 source = """\
551int bare = 50; /* MAGIC-OK */
552int blank = 51; /* MAGIC-OK: */
553int reasoned = 52; /* MAGIC-OK: fixed fixture protocol value */
554const char *text = "MAGIC-OK: string text"; int string_only = 53;
555int lookalike = 54; /* MAGIC-OKAY: not the marker */
556int line_reason = 55; // MAGIC-OK: documented line-comment exception
557int fake_line_comment = 56; const char *line = "// MAGIC-OK: string bypass";
558int fake_block_comment = 57; const char *block = "/* MAGIC-OK: string bypass */";
559"""
560 failures: list[str] = []
561 with tempfile.TemporaryDirectory(prefix="ra8-magic-selftest-") as temp:
562 fixture = Path(temp) / "fixture.c"
563 fixture.write_text(source, encoding="ascii")
564 findings = {literal for _, _, literal in _scan_file(fixture)}
565 expected = {"50", "51", "53", "54", "56", "57"}
566 if findings != expected:
567 failures.append(
568 "reason contract mismatch: "
569 f"expected findings {sorted(expected)}, got {sorted(findings)}"
570 )
571 if _has_reasoned_opt_out("int x = 56; /* MAGIC-OK: missing terminator"):
572 failures.append("unterminated block comment accepted as a reasoned opt-out")
573 if failures:
574 print("check_magic_numbers.py selftest: FAILED", file=sys.stderr)
575 for failure in failures:
576 print(f" - {failure}", file=sys.stderr)
577 return 1
578 print("check_magic_numbers.py selftest: PASS (reason required; false markers rejected)")
579 return 0
580
581
582def main(argv: list[str]) -> int:
583 """Fail on integer literals that should be named typed enums.
584
585 Reports column as well as line, which is why the whole scan runs over a
586 length-preserving blanked view of the source rather than a stripped one.
587
588 Returns 1 listing each literal, 0 when clean or when argv filtered to
589 nothing.
590 """
591 if argv[1:] == ["--selftest"]:
592 return _run_selftest()
593 if "--selftest" in argv[1:]:
594 print("--selftest cannot be combined with scan paths", file=sys.stderr)
595 return 2
596
597 targets = _enumerate_targets(argv[1:])
598 if not targets:
599 print("check_magic_numbers.py: no files to scan", file=sys.stderr)
600 return 0
601
602 findings: list[tuple[str, int, int, str]] = []
603 for path in targets:
604 for line_no, col, literal in _scan_file(path):
605 rel = path.relative_to(REPO_ROOT) if path.is_relative_to(REPO_ROOT) else path
606 findings.append((str(rel), line_no, col, literal))
607
608 if not findings:
609 print(f"check_magic_numbers.py: {len(targets)} file(s) scanned, no magic numbers found.")
610 return 0
611
612 findings.sort()
613 print(
614 f"check_magic_numbers.py: {len(findings)} magic number(s) found "
615 "-- every integer literal must be a named typed enum (use a const "
616 "only for floating-point; macros are not allowed for constants):\n",
617 file=sys.stderr,
618 )
619 print(" file:line:col literal", file=sys.stderr)
620 for path, line_no, col, literal in findings:
621 print(f" {path}:{line_no}:{col} {literal}", file=sys.stderr)
622 print(
623 "\nReplace each literal with a named value. Genuine exceptions may "
624 f"carry a trailing `{OPT_OUT}: <reason>` comment on the line.",
625 file=sys.stderr,
626 )
627 return 1
628
629
630if __name__ == "__main__":
631 sys.exit(main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298