ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_assert_casts.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: reject explicit integer casts inside TEST_ASSERT_EQ arguments.
5
6TEST_ASSERT_EQ widens both arguments to int64_t internally. An outer
7cast like (int) or (int32_t) is therefore redundant -- and a (int) cast
8applied to a uint32_t enum silently truncates the value to 32-bit signed
9before the widening, which can produce a false-passing comparison for
10values >= 0x80000000.
11
12Run: check_assert_casts.py <file> [...]
13Exit 0 if clean, 1 if any violation found.
14"""
15
16import re
17import sys
18import tempfile
19from pathlib import Path
20
21_TYPES = r"u?int(?:8|16|32|64)?_t|int|size_t|ssize_t"
22_CAST_RE = re.compile(r"\‍((?:" + _TYPES + r")\‍)")
23_MACRO = "TEST_ASSERT_EQ("
24
25
26def _find_close_paren(text: str, start: int) -> int:
27 depth = 1
28 i = start
29 while i < len(text) and depth:
30 c = text[i]
31 if c == "(":
32 depth += 1
33 elif c == ")":
34 depth -= 1
35 i += 1
36 return i - 1
37
38
39def _has_leading_cast(text: str) -> bool:
40 return bool(re.match(r"^\s*\‍((?:" + _TYPES + r")\‍)", text))
41
42
43def check(path: Path) -> list[str]:
44 """Report TEST_ASSERT_EQ arguments that OPEN with a redundant integer cast.
45
46 Only a cast in leading position counts, on either argument. A cast deeper
47 in the expression is left alone deliberately -- there it is usually load
48 bearing (narrowing a wider intermediate), whereas an outer one is applied
49 to a value the macro is about to widen to int64_t anyway.
50
51 The macro's two arguments are split at the first comma seen at paren depth
52 zero, so a comma inside a nested call or a compound literal does not shift
53 the second argument. An invocation with no such comma is skipped rather
54 than reported: that is a syntax error, and the compiler names it better.
55
56 Returns one preformatted ``path:line: message`` per violation, in file
57 order; an empty list means clean.
58 """
59 content = path.read_text(encoding="ascii", errors="replace")
60 violations = []
61 macro_len = len(_MACRO)
62 pos = 0
63 while True:
64 idx = content.find(_MACRO, pos)
65 if idx == -1:
66 break
67 inner_start = idx + macro_len
68 close = _find_close_paren(content, inner_start)
69 inner = content[inner_start:close]
70
71 depth = 0
72 split = None
73 for i, ch in enumerate(inner):
74 if ch in "([{":
75 depth += 1
76 elif ch in ")]}":
77 depth -= 1
78 elif ch == "," and depth == 0:
79 split = i
80 break
81
82 line_no = content[:idx].count("\n") + 1
83 if split is None:
84 pos = close + 1
85 continue
86
87 arg1 = inner[:split]
88 arg2 = inner[split + 1 :]
89 if _has_leading_cast(arg1):
90 violations.append(
91 f"{path}:{line_no}: cast in first arg of TEST_ASSERT_EQ: "
92 f"{_MACRO}{arg1.strip()[:60]}..."
93 )
94 if _has_leading_cast(arg2):
95 violations.append(
96 f"{path}:{line_no}: cast in second arg of TEST_ASSERT_EQ: ...{arg2.strip()[:60]}"
97 )
98 pos = close + 1
99 return violations
100
101
102def selftest() -> int:
103 """Prove leading casts fire while clean and nested casts stay quiet."""
104 with tempfile.TemporaryDirectory(prefix="assert-casts-selftest-") as raw:
105 root = Path(raw)
106 bad = root / "bad.c"
107 good = root / "good.c"
108 bad.write_text(
109 "TEST_ASSERT_EQ((int)value, (uint32_t)expected);\n",
110 encoding="ascii",
111 )
112 good.write_text(
113 "TEST_ASSERT_EQ(value, expected);\nTEST_ASSERT_EQ(load((int)value), expected);\n",
114 encoding="ascii",
115 )
116 bad_findings = check(bad)
117 good_findings = check(good)
118 expected_bad_findings = 2
119 cases = (
120 (len(bad_findings) == expected_bad_findings, "leading casts on both arguments fire"),
121 (not good_findings, "clean and nested casts stay quiet"),
122 )
123 failed = [label for passed, label in cases if not passed]
124 for passed, label in cases:
125 print(f" [{'ok' if passed else 'FAIL'}] {label}")
126 if failed:
127 print(f"check_assert_casts.py --selftest: {len(failed)} failure(s)", file=sys.stderr)
128 return 1
129 print("check_assert_casts.py --selftest: all cases pass (both directions).")
130 return 0
131
132
133def main() -> int:
134 """Scan the files named on argv and print every finding to stdout."""
135 args = sys.argv[1:]
136 if args == ["--selftest"]:
137 return selftest()
138 if any(arg.startswith("-") and arg != "--all" for arg in args) or (
139 "--all" in args and args != ["--all"]
140 ):
141 print("check_assert_casts.py: unknown or incompatible arguments", file=sys.stderr)
142 return 2
143 if args == ["--all"]:
144 repo_root = Path(__file__).resolve().parents[2]
145 paths = sorted((repo_root / "tests").rglob("*.c"))
146 else:
147 paths = [Path(p) for p in args]
148 if not paths:
149 print(
150 "usage: check_assert_casts.py <file> [...] or check_assert_casts.py --all",
151 file=sys.stderr,
152 )
153 return 1
154 all_violations: list[str] = []
155 for p in paths:
156 all_violations.extend(check(p))
157 for v in all_violations:
158 print(v)
159 if all_violations:
160 print(
161 f"\n{len(all_violations)} redundant cast(s) in TEST_ASSERT_EQ.\n"
162 "Run scripts/fix/strip_assert_casts.py to fix automatically.",
163 file=sys.stderr,
164 )
165 return 1
166 return 0
167
168
169if __name__ == "__main__":
170 sys.exit(main())
-copyright
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298