ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
strip_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"""Remove redundant integer casts from TEST_ASSERT_EQ arguments.
5
6TEST_ASSERT_EQ internally widens both arguments to int64_t, so any outer
7cast like (int), (int32_t), (uint32_t), etc. is redundant and misleading:
8a (int) cast applied to a uint32_t enum truncates the value to 32-bit
9signed before the macro widens it again.
10
11This script strips those casts from the two argument positions only -- it
12does not touch casts inside other function calls.
13
14Limitation: a cast that TRUNCATES an out-of-range value is load-bearing, not
15redundant -- e.g. ``(uint16_t)~x``, where the bare ``~x`` promotes to a negative
16int. This script cannot tell those apart, so verify the full host suite after a
17run and hoist any load-bearing cast into a typed local before the assertion.
18"""
19
20import re
21import sys
22from pathlib import Path
23
24_TYPES = r"u?int(?:8|16|32|64)?_t|int|size_t|ssize_t"
25_CAST_RE = re.compile(r"^(\s*)\‍((?:" + _TYPES + r")\‍)(.*)", re.DOTALL)
26
27MACRO = "TEST_ASSERT_EQ("
28
29
30def _strip_leading_cast(text: str) -> str:
31 """Remove one leading integer cast from an argument, if present.
32
33 Only the outermost, leading cast: a cast deeper in the expression is
34 usually load-bearing and is left alone.
35 """
36 m = _CAST_RE.match(text)
37 return (m.group(1) + m.group(2)) if m else text
38
39
40def _skip_token(text: str, i: int) -> int:
41 """Index just past a literal or comment at ``i``; ``i`` itself for plain code.
42
43 Escapes are handled, so an embedded quote does not end the literal early.
44
45 Bracket/comma scanning must never count a ``(``, ``)`` or ``,`` that lives
46 inside a ``"..."`` / ``'...'`` literal or a ``/* */`` / ``//`` comment, or the
47 depth accounting drifts and the scan runs off the end of the file.
48 """
49 c = text[i]
50 nxt = text[i + 1] if i + 1 < len(text) else ""
51 if c in "\"'":
52 j = i + 1
53 while j < len(text):
54 if text[j] == "\\":
55 j += 2
56 continue
57 if text[j] == c:
58 return j + 1
59 j += 1
60 return j
61 if c == "/" and nxt == "/":
62 j = text.find("\n", i)
63 return len(text) if j == -1 else j
64 if c == "/" and nxt == "*":
65 j = text.find("*/", i + 2)
66 return len(text) if j == -1 else j + 2
67 return i
68
69
70def _find_close_paren(text: str, start: int) -> int:
71 """Offset of the ``)`` closing the paren already opened before ``start``.
72
73 Skips literals and comments via ``_skip_token``, so a parenthesis inside a
74 string cannot unbalance the depth count.
75 """
76 depth = 1
77 i = start
78 while i < len(text) and depth:
79 j = _skip_token(text, i)
80 if j != i:
81 i = j
82 continue
83 c = text[i]
84 if c == "(":
85 depth += 1
86 elif c == ")":
87 depth -= 1
88 i += 1
89 return i - 1
90
91
92def process(content: str) -> str:
93 """Strip redundant leading casts from every TEST_ASSERT_EQ in one pass.
94
95 A SINGLE pass: removing a cast can expose another one beneath it, so this
96 is not guaranteed to reach a fixed point on its own -- callers should use
97 ``process_to_convergence``.
98 """
99 out = []
100 pos = 0
101 macro_len = len(MACRO)
102 while True:
103 idx = content.find(MACRO, pos)
104 if idx == -1:
105 out.append(content[pos:])
106 break
107
108 out.append(content[pos : idx + macro_len])
109 inner_start = idx + macro_len
110 close = _find_close_paren(content, inner_start)
111 inner = content[inner_start:close]
112
113 depth = 0
114 split = None
115 k = 0
116 while k < len(inner):
117 j = _skip_token(inner, k)
118 if j != k:
119 k = j
120 continue
121 ch = inner[k]
122 if ch in "([{":
123 depth += 1
124 elif ch in ")]}":
125 depth -= 1
126 elif ch == "," and depth == 0:
127 split = k
128 break
129 k += 1
130
131 if split is None:
132 out.append(inner)
133 out.append(")")
134 else:
135 arg1 = _strip_leading_cast(inner[:split])
136 arg2 = _strip_leading_cast(inner[split + 1 :])
137 out.append(arg1)
138 out.append(",")
139 out.append(arg2)
140 out.append(")")
141
142 pos = close + 1
143
144 return "".join(out)
145
146
147def process_to_convergence(content: str) -> str:
148 """Re-run ``process`` until the text stops changing.
149
150 Bounded to ten iterations rather than looping until stable: a bug that
151 made the transform oscillate between two forms would otherwise hang the
152 pre-commit hook. In practice one pass fixes everything and a second
153 confirms it.
154 """
155 for _ in range(10):
156 next_pass = process(content)
157 if next_pass == content:
158 break
159 content = next_pass
160 return content
161
162
163def main() -> int:
164 """Rewrite the files named on argv in place, reporting how many changed.
165
166 Always writes -- there is no dry-run mode here, because check_assert_casts
167 is the read-only half of this pair and CI runs that one.
168
169 Returns 1 on the empty-argv usage error, 0 otherwise.
170 """
171 paths = [Path(p) for p in sys.argv[1:]]
172 if not paths:
173 print("usage: strip_assert_casts.py <file> [...]", file=sys.stderr)
174 return 1
175 changed = 0
176 for path in paths:
177 original = path.read_text(encoding="ascii")
178 fixed = process_to_convergence(original)
179 if fixed != original:
180 path.write_text(fixed, encoding="ascii")
181 changed += 1
182 print(f"fixed: {path}")
183 print(f"{changed}/{len(paths)} file(s) modified")
184 return 0
185
186
187if __name__ == "__main__":
188 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298