ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_no_gnu_attribute.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"""check_no_gnu_attribute.py -- enforce the C23 [[...]] attribute syntax.
5
6Per CLAUDE.md, first-party code uses the standard C23 attribute-specifier
7form [[...]] (e.g. [[gnu::weak]], [[noreturn]], [[maybe_unused]],
8[[nodiscard]]) instead of the GNU __attribute__((...)) form.
9
10 void f(void) __attribute__((weak)); # REJECTED
11 [[gnu::weak]] void f(void); # OK
12
13Scope:
14 C / C++ sources (.c .h .cpp .hpp) under libs/, src/, tests/, examples/,
15 port/, tools/. Vendored SOUP (libs/third_party/*) and generated font
16 data (libs/ra8_fonts/*) are exempt, as are build trees.
17
18Allowed __attribute__ uses (no portable [[...]] spelling -- clang errors on
19the [[gnu::]] form as an unknown attribute while it silently ignores the GNU
20form, so these MUST stay __attribute__):
21 * interrupt
22 * cmse_nonsecure_entry
23 * cmse_nonsecure_call
24(with or without the __leading_trailing__ underscores).
25
26Other exemptions:
27 * Comment lines / prose mentioning __attribute__ (lines whose attribute
28 token sits inside a // or /* comment).
29 * Any line carrying `ATTR-OK: <reason>` (reason text required).
30
31Exit 0 when clean, 1 on any violation, 2 when the whole-tree sweep collapses
32below FILE_FLOOR (see the constant).
33"""
34
35from __future__ import annotations
36
37import re
38import sys
39import tempfile
40from pathlib import Path
41
42sys.path.insert(0, str(Path(__file__).resolve().parent))
43
44from lint_targets import is_build_output_path
45
46ROOTS = ("libs", "tests", "examples", "port", "tools", "apps")
47EXTS = (".c", ".h", ".cpp", ".hpp")
48EXEMPT_DIRS = ("/third_party/", "/ra8_fonts/")
49ALLOWED = {"interrupt", "cmse_nonsecure_entry", "cmse_nonsecure_call"}
50
51ATTR_RE = re.compile(r"__attribute__\s*\‍(\‍(")
52WAIVER_RE = re.compile(r"ATTR-OK:\s*\S")
53
54# Length of a bare `____` wrapper (two leading + two trailing underscores); a
55# dunder name must exceed it to carry a body worth stripping.
56_MIN_DUNDER_LEN = 4
57
58# A tree this size cannot legitimately collapse to a handful of files. If the
59# whole-tree sweep returns less than this, something broke (a bad cwd -- ROOTS
60# are relative, so this walks nothing when run from elsewhere -- or a renamed
61# root) and reporting "clean" would be a lie. Measured 2026-07-28: 2125
62# first-party C/C++ files. Same trip-wire as check_ruff.py.
63FILE_FLOOR = 1700
64
65
66def _strip_us(name: str) -> str:
67 """Strip the surrounding double underscores from an attribute name.
68
69 ``__packed__`` and ``packed`` are the same attribute to GCC, so both
70 spellings must normalise to one before comparison or half of them would
71 slip past.
72 """
73 if len(name) > _MIN_DUNDER_LEN and name.startswith("__") and name.endswith("__"):
74 return name[2:-2]
75 return name
76
77
78def _attr_body(line: str, pos: int) -> str | None:
79 """Balanced-paren body of the __attribute__ starting at/after pos."""
80 p = line.find("((", pos)
81 if p < 0:
82 return None
83 p += 2
84 depth, q = 1, p
85 while q < len(line):
86 if line[q] == "(":
87 depth += 1
88 elif line[q] == ")":
89 depth -= 1
90 if depth == 0:
91 return line[p:q]
92 q += 1
93 return None
94
95
96def _is_comment_pos(line: str, pos: int) -> bool:
97 """True if column `pos` sits inside a // or /* comment on this line."""
98 stripped = line.lstrip()
99 if stripped.startswith(("*", "//", "/*")):
100 return True
101 before = line[:pos]
102 if "//" in before:
103 return True
104 # crude single-line /* ... */ check
105 return "/*" in before and "*/" not in before
106
107
108def discover() -> list[str]:
109 """Every first-party source file, for the whole-tree sweep."""
110 out: list[str] = []
111 for root in ROOTS:
112 base = Path(root)
113 if not base.is_dir():
114 continue
115 for ext in EXTS:
116 for f in base.rglob(f"*{ext}"):
117 s = str(f)
118 if is_build_output_path(s) or any(d in s for d in EXEMPT_DIRS):
119 continue
120 out.append(s)
121 return out
122
123
124def check_file(path: str) -> list[tuple[int, str]]:
125 """Report every legacy ``__attribute__((...))`` in one file.
126
127 The tree uses the C23 ``[[...]]`` form. Note the two spellings that CANNOT
128 migrate and are excluded rather than reported -- ``interrupt`` and the
129 CMSE ``cmse_nonsecure_entry`` -- neither of which has a standard-attribute
130 equivalent the toolchain accepts.
131 """
132 findings: list[tuple[int, str]] = []
133 try:
134 text = Path(path).read_text(encoding="utf-8")
135 except (OSError, UnicodeDecodeError):
136 return findings
137 if "__attribute__" not in text:
138 return findings
139 for i, line in enumerate(text.splitlines(), 1):
140 for m in ATTR_RE.finditer(line):
141 if _is_comment_pos(line, m.start()):
142 continue
143 if WAIVER_RE.search(line):
144 continue
145 body = _attr_body(line, m.start())
146 names = {
147 _strip_us(t.strip().split("(")[0].strip())
148 for t in (body.split(",") if body else [])
149 }
150 if names and names <= ALLOWED:
151 continue
152 findings.append((i, line.strip()[:100]))
153 return findings
154
155
156def selftest() -> int:
157 """Prove migratable GNU attributes fire and exact exceptions stay quiet."""
158 with tempfile.TemporaryDirectory(prefix="gnu-attribute-selftest-") as raw:
159 root = Path(raw)
160 bad = root / "bad.c"
161 good = root / "good.c"
162 bad.write_text("void f(void) __attribute__((weak));\n", encoding="ascii")
163 good.write_text(
164 "[[gnu::weak]] void f(void);\n"
165 "void irq(void) __attribute__((interrupt));\n"
166 "void g(void) __attribute__((packed)); /* ATTR-OK: wire ABI */\n"
167 "// void prose(void) __attribute__((weak));\n",
168 encoding="ascii",
169 )
170 bad_findings = check_file(str(bad))
171 good_findings = check_file(str(good))
172 cases = (
173 (len(bad_findings) == 1, "migratable GNU attribute fires"),
174 (not good_findings, "C23, exact exception, waiver, and prose stay quiet"),
175 )
176 failed = [label for passed, label in cases if not passed]
177 for passed, label in cases:
178 print(f" [{'ok' if passed else 'FAIL'}] {label}")
179 if failed:
180 print(f"check_no_gnu_attribute.py --selftest: {len(failed)} failure(s)", file=sys.stderr)
181 return 1
182 print("check_no_gnu_attribute.py --selftest: all cases pass (both directions).")
183 return 0
184
185
186def main() -> int:
187 """Fail on GNU ``__attribute__`` syntax where the C23 form is available.
188
189 Position matters for the C23 form -- it must precede the declaration
190 specifiers rather than follow them -- which is why this is a migration
191 worth finishing rather than a style preference: the two spellings do not
192 sit in the same place, so mixed usage reads inconsistently.
193
194 FILE_FLOOR is enforced on the whole-tree sweep only, and exits 2 below it.
195 An explicit argv file list is a deliberately narrowed scope -- not a
196 collapsed one -- so it is exempt; the sweep is where a broken enumeration
197 would silently report the tree clean.
198
199 Returns 1 listing each occurrence, 0 when clean, 2 when the whole-tree
200 sweep enumerated too few files to trust.
201 """
202 raw_args = sys.argv[1:]
203 if raw_args == ["--selftest"]:
204 return selftest()
205 if any(arg.startswith("-") for arg in raw_args):
206 print("usage: check_no_gnu_attribute.py [--selftest] [file ...]", file=sys.stderr)
207 return 2
208 args = raw_args
209 whole_tree = not args
210 files = args or discover()
211 if whole_tree and len(files) < FILE_FLOOR:
212 print(
213 f"check_no_gnu_attribute.py: FATAL -- only {len(files)} first-party source "
214 f"file(s) in scope, floor is {FILE_FLOOR}. A collapsed sweep reports a "
215 "clean tree because it scanned nothing.",
216 file=sys.stderr,
217 )
218 return 2
219 total = 0
220 for path in sorted(set(files)):
221 if (
222 not path.endswith(EXTS)
223 or is_build_output_path(path)
224 or any(d in path for d in EXEMPT_DIRS)
225 ):
226 continue
227 for ln, snippet in check_file(path):
228 print(
229 f"{path}:{ln}: GNU __attribute__ -- use the C23 [[...]] form "
230 f"(e.g. [[gnu::weak]]); {snippet}"
231 )
232 total += 1
233 if total:
234 print(
235 f"\ncheck_no_gnu_attribute: {total} violation(s). Migrate to [[...]] "
236 f"(only interrupt / cmse_nonsecure_entry / cmse_nonsecure_call may stay "
237 f"__attribute__; add `ATTR-OK: <reason>` for a justified exception)."
238 )
239 return 1
240 print("check_no_gnu_attribute: clean -- all attributes use the C23 [[...]] form.")
241 return 0
242
243
244if __name__ == "__main__":
245 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298