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
6
TEST_ASSERT_EQ widens both arguments to int64_t internally. An outer
7
cast like (int) or (int32_t) is therefore redundant -- and a (int) cast
8
applied to a uint32_t enum silently truncates the value to 32-bit signed
9
before the widening, which can produce a false-passing comparison for
10
values >= 0x80000000.
11
12
Run: check_assert_casts.py <file> [...]
13
Exit 0 if clean, 1 if any violation found.
14
"""
15
16
import
re
17
import
sys
18
import
tempfile
19
from
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
26
def
_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
39
def
_has_leading_cast(text: str) -> bool:
40
return
bool(re.match(
r"^\s*\((?:"
+ _TYPES +
r")\)"
, text))
41
42
43
def
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
102
def
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
133
def
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
169
if
__name__ ==
"__main__"
:
170
sys.exit(
main
())
check
-copyright
main
void main(void)
The application entry point Reset_Handler hands control to.
Definition
main.c:298
scripts
checks
check_assert_casts.py
Generated by
1.16.1