4"""check_no_gnu_attribute.py -- enforce the C23 [[...]] attribute syntax.
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.
10 void f(void) __attribute__((weak)); # REJECTED
11 [[gnu::weak]] void f(void); # OK
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.
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__):
22 * cmse_nonsecure_entry
24(with or without the __leading_trailing__ underscores).
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).
31Exit 0 when clean, 1 on any violation, 2 when the whole-tree sweep collapses
32below FILE_FLOOR (see the constant).
35from __future__
import annotations
40from pathlib
import Path
42sys.path.insert(0, str(Path(__file__).resolve().parent))
44from lint_targets
import is_build_output_path
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"}
51ATTR_RE = re.compile(
r"__attribute__\s*\(\(")
52WAIVER_RE = re.compile(
r"ATTR-OK:\s*\S")
66def _strip_us(name: str) -> str:
67 """Strip the surrounding double underscores from an attribute name.
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
73 if len(name) > _MIN_DUNDER_LEN
and name.startswith(
"__")
and name.endswith(
"__"):
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)
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((
"*",
"//",
"/*")):
105 return "/*" in before
and "*/" not in before
108def discover() -> list[str]:
109 """Every first-party source file, for the whole-tree sweep."""
113 if not base.is_dir():
116 for f
in base.rglob(f
"*{ext}"):
118 if is_build_output_path(s)
or any(d
in s
for d
in EXEMPT_DIRS):
124def check_file(path: str) -> list[tuple[int, str]]:
125 """Report every legacy ``__attribute__((...))`` in one file.
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.
132 findings: list[tuple[int, str]] = []
134 text = Path(path).read_text(encoding=
"utf-8")
135 except (OSError, UnicodeDecodeError):
137 if "__attribute__" not in text:
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()):
143 if WAIVER_RE.search(line):
145 body = _attr_body(line, m.start())
147 _strip_us(t.strip().split(
"(")[0].strip())
148 for t
in (body.split(
",")
if body
else [])
150 if names
and names <= ALLOWED:
152 findings.append((i, line.strip()[:100]))
156def selftest() -> int:
157 """Prove migratable GNU attributes fire and exact exceptions stay quiet."""
158 with tempfile.TemporaryDirectory(prefix=
"gnu-attribute-selftest-")
as raw:
161 good = root /
"good.c"
162 bad.write_text(
"void f(void) __attribute__((weak));\n", encoding=
"ascii")
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",
170 bad_findings = check_file(str(bad))
171 good_findings = check_file(str(good))
173 (len(bad_findings) == 1,
"migratable GNU attribute fires"),
174 (
not good_findings,
"C23, exact exception, waiver, and prose stay quiet"),
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}")
180 print(f
"check_no_gnu_attribute.py --selftest: {len(failed)} failure(s)", file=sys.stderr)
182 print(
"check_no_gnu_attribute.py --selftest: all cases pass (both directions).")
187 """Fail on GNU ``__attribute__`` syntax where the C23 form is available.
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.
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.
199 Returns 1 listing each occurrence, 0 when clean, 2 when the whole-tree
200 sweep enumerated too few files to trust.
202 raw_args = sys.argv[1:]
203 if raw_args == [
"--selftest"]:
205 if any(arg.startswith(
"-")
for arg
in raw_args):
206 print(
"usage: check_no_gnu_attribute.py [--selftest] [file ...]", file=sys.stderr)
209 whole_tree =
not args
210 files = args
or discover()
211 if whole_tree
and len(files) < FILE_FLOOR:
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.",
220 for path
in sorted(set(files)):
222 not path.endswith(EXTS)
223 or is_build_output_path(path)
224 or any(d
in path
for d
in EXEMPT_DIRS)
227 for ln, snippet
in check_file(path):
229 f
"{path}:{ln}: GNU __attribute__ -- use the C23 [[...]] form "
230 f
"(e.g. [[gnu::weak]]); {snippet}"
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)."
240 print(
"check_no_gnu_attribute: clean -- all attributes use the C23 [[...]] form.")
244if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.