ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
static_decl_scan.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Recognize header-only non-inline static function declarations.
4
5The tidy router must distinguish ordinary standalone headers from textual
6include fragments. This scanner applies line splicing, removes preprocessing
7directives and comments, then tokenizes C-family source so declaration
8whitespace and line wrapping cannot change that ownership decision.
9"""
10
11from __future__ import annotations
12
13import argparse
14import re
15import shutil
16import subprocess
17import sys
18from pathlib import Path
19
20_TOKEN_RE = re.compile(
21 r"""
22 (?P<block_comment>/\*.*?\*/)
23 | (?P<line_comment>//[^\n]*)
24 | (?P<string>"(?:\\.|[^"\\])*")
25 | (?P<char>'(?:\\.|[^'\\])*')
26 | (?P<identifier>[A-Za-z_][A-Za-z0-9_]*)
27 | (?P<punct>[{}()\‍[\‍];=,*])
28 | (?P<space>\s+)
29 | (?P<other>.)
30 """,
31 re.DOTALL | re.VERBOSE,
32)
33_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
34
35
36class ScanError(ValueError):
37 """The token stream is malformed, so ownership cannot be decided safely."""
38
39
40_GROUP_CLOSE = {"(": ")", "[": "]", "{": "}"}
41_GROUP_OPEN = set(_GROUP_CLOSE)
42_GROUP_END = set(_GROUP_CLOSE.values())
43_POINTER_QUALIFIERS = {"const", "restrict", "volatile", "_Atomic"}
44_DECL_SPECIFIER_WORDS = {
45 "_Alignas",
46 "_Atomic",
47 "_Bool",
48 "_Complex",
49 "_Noreturn",
50 "auto",
51 "bool",
52 "char",
53 "char8_t",
54 "char16_t",
55 "char32_t",
56 "class",
57 "const",
58 "consteval",
59 "constexpr",
60 "double",
61 "enum",
62 "extern",
63 "float",
64 "inline",
65 "__inline",
66 "__inline__",
67 "int",
68 "long",
69 "register",
70 "restrict",
71 "short",
72 "signed",
73 "static",
74 "struct",
75 "typedef",
76 "union",
77 "unsigned",
78 "void",
79 "volatile",
80 "wchar_t",
81}
82_INLINE_SPECIFIERS = frozenset({"inline", "__inline", "__inline__", "constexpr", "consteval"})
83_TYPE_BEARING_SPECIFIERS = frozenset(
84 {
85 "_Bool",
86 "auto",
87 "bool",
88 "char",
89 "char8_t",
90 "char16_t",
91 "char32_t",
92 "class",
93 "double",
94 "enum",
95 "float",
96 "int",
97 "long",
98 "short",
99 "signed",
100 "struct",
101 "union",
102 "unsigned",
103 "void",
104 "wchar_t",
105 }
106)
107_ELABORATED_SPECIFIERS = frozenset({"class", "enum", "struct", "union"})
108_PARAMETER_EXPRESSION_LEADS = frozenset({"!", "+", "-", "false", "nullptr", "true", "~"})
109_MIN_USER_TYPE_DECL_TOKENS = 2
110_PREFIX_GROUP_WORDS = {
111 "_Alignas",
112 "_Atomic",
113 "alignas",
114 "decltype",
115 "typeof",
116 "typeof_unqual",
117 "__typeof__",
118}
119_ANNOTATION_WORDS = {
120 "asm",
121 "__asm",
122 "__asm__",
123 "__attribute",
124 "__attribute__",
125 "__declspec",
126}
127
128
129_SELFTEST_CASES = {
130 "single line": ("RA8_INTERNAL static int helper(void);", True),
131 "spliced keyword and name": ("sta\\\ntic int hel\\\nper(void);", True),
132 "spliced block comment": (
133 "/\\\n* static int hidden(void); */ int public_api(void);",
134 False,
135 ),
136 "spliced define": (
137 "#def\\\nine HIDDEN static int hidden(void)\nint public_api(void);",
138 False,
139 ),
140 "return/name split": ("RA8_INTERNAL static int\nhelper(void);", True),
141 "all specifiers split": ("RA8_INTERNAL\nstatic\nint helper(void);", True),
142 "trailing annotation declaration": ("static int helper(void) RA8_UNUSED;", True),
143 "exact RA8_UNUSED definition": (
144 "RA8_INTERNAL static int helper(void) RA8_UNUSED\n{ return 0; }",
145 True,
146 ),
147 "trailing annotation call": (
148 "static int helper(void) RA8_ANNOTATE(owner);",
149 True,
150 ),
151 "compiler attribute": (
152 "static int helper(void) __attribute__((unused));",
153 True,
154 ),
155 "parenthesized function name": ("static int (helper)(void);", True),
156 "returns function pointer": ("static int (*helper(void))(int);", True),
157 "mixed data then function": ("static int value, helper(void);", True),
158 "mixed function then data": ("static int helper(void), value;", True),
159 "pointer return": ("static int *helper(void);", True),
160 "inline then noninline": (
161 "static inline int accessor(void) { return 0; }\nstatic int helper(void);",
162 True,
163 ),
164 "static inline": ("static inline int accessor(void) { return 0; }", False),
165 "inline static": ("inline static int accessor(void) { return 0; }", False),
166 "static data": ("static int value = 1;", False),
167 "static function pointer data": ("static int (*handler)(void);", False),
168 "mixed data only": ("static int first, *second;", False),
169 "annotated data": ("static int value RA8_UNUSED;", False),
170 "function-local static": (
171 "int owner(void) { static int value = 1; return value; }",
172 False,
173 ),
174 "block declaration": (
175 "int owner(void) { int helper(void); static int value = 1; return value; }",
176 False,
177 ),
178 "comment and literal": (
179 '/* static int helper(void); */ const char *s = "static int helper(void);";',
180 False,
181 ),
182 "macro directive": ("#define HELPER static int helper(void)\nint public_api(void);", False),
183 "array initializer": ("static int values[] = {1, 2};", False),
184 "aggregate callback": (
185 "static struct record { int (*callback)(void); } value;",
186 False,
187 ),
188}
189
190_CPP_SELFTEST_CASES = {
191 "extern C declaration": (
192 '#ifdef __cplusplus\nextern "C" {\n#endif\n'
193 "static int helper(void);\n"
194 "#ifdef __cplusplus\n}\n#endif\n",
195 True,
196 ),
197 "extern C definition": (
198 '#ifdef __cplusplus\nextern "C" {\n#endif\n'
199 "static int helper(void) { return 0; }\n"
200 "#ifdef __cplusplus\n}\n#endif\n",
201 True,
202 ),
203 "extern C nested scopes": (
204 '#ifdef __cplusplus\nextern "C" {\n#endif\n'
205 "struct Record { static int member(void); };\n"
206 "union Variant { int value; static int member(void); };\n"
207 "enum Kind { kind_one };\n"
208 "class Holder { static int member(void); };\n"
209 "int owner(void) { static int value = 1; return value; }\n"
210 "#ifdef __cplusplus\n}\n#endif\n",
211 False,
212 ),
213 "named namespace": (
214 "namespace detail { static int helper(void); }",
215 True,
216 ),
217 "inline namespace": (
218 "inline namespace abi { static int helper(void) { return 0; } }",
219 True,
220 ),
221 "anonymous namespace": (
222 "namespace { static int helper(void); }",
223 True,
224 ),
225 "nested namespace": (
226 "namespace outer::inner { static int helper(void); }",
227 True,
228 ),
229 "extern C++ wrapping extern C": (
230 'extern "C++" { extern "C" { static int helper(void); } }',
231 True,
232 ),
233 "direct initialized static object": (
234 "struct Value { explicit Value(int); }; static Value value(7);",
235 False,
236 ),
237 "identifier initialized static object": (
238 "struct Value { explicit Value(int); }; int existing = 7; static Value value(existing);",
239 False,
240 ),
241 "unary initialized static object": (
242 "struct Value { explicit Value(int); }; int existing = 7; static Value value(-existing);",
243 False,
244 ),
245 "call initialized static object": (
246 "struct Value { explicit Value(int); }; int factory(); static Value value(factory());",
247 False,
248 ),
249 "string initialized static object": (
250 'struct Value { explicit Value(const char *); }; static Value value("text");',
251 False,
252 ),
253 "character initialized static object": (
254 "struct Value { explicit Value(char); }; static Value value('x');",
255 False,
256 ),
257 "boolean initialized static object": (
258 "struct Value { explicit Value(bool); }; static Value value(true);",
259 False,
260 ),
261 "defaulted function parameter": (
262 "static int helper(int value = 7);",
263 True,
264 ),
265 "typedef named function parameter": (
266 "using Count = int; static int helper(Count value);",
267 True,
268 ),
269 "qualified unnamed function parameter": (
270 "namespace model { struct Value {}; } static int helper(model::Value);",
271 True,
272 ),
273 "namespace attribute": (
274 'namespace detail [[deprecated("fixture")]] { static int helper(void); }',
275 True,
276 ),
277 "nested inline namespace": (
278 "namespace outer::inline abi { static int helper(void); }",
279 True,
280 ),
281 "noexcept function": (
282 "static int helper(void) noexcept;",
283 True,
284 ),
285 "conditional noexcept function": (
286 "static int helper(void) noexcept(true);",
287 True,
288 ),
289 "trailing return function": (
290 "static auto helper(void) noexcept -> int;",
291 True,
292 ),
293 "templated trailing return": (
294 "#include <array>\nstatic auto helper(void) -> std::array<int, 2>;",
295 True,
296 ),
297 "nested templated trailing return": (
298 "#include <vector>\n"
299 "static auto helper(void) noexcept -> const std::vector<std::vector<int>>&;",
300 True,
301 ),
302 "global qualified templated trailing return": (
303 "#include <array>\nstatic auto helper(void) -> ::std::array<int, 2>;",
304 True,
305 ),
306 "templated direct initialized object": (
307 "#include <vector>\nint existing = 7; static std::vector<int> value(existing);",
308 False,
309 ),
310 "gnu inline spelling": (
311 "static __inline int helper(void) { return 0; }",
312 False,
313 ),
314 "gnu inline suffix spelling": (
315 "static __inline__ int helper(void) { return 0; }",
316 False,
317 ),
318 "constexpr implicit inline": (
319 "static constexpr int helper(void) { return 0; }",
320 False,
321 ),
322 "consteval implicit inline": (
323 "static consteval int helper(void) { return 0; }",
324 False,
325 ),
326 "namespace nested scopes stay opaque": (
327 "namespace detail {\n"
328 "struct Record { static int member(void); };\n"
329 "union Variant { int value; static int member(void); };\n"
330 "enum Kind { kind_one };\n"
331 "class Holder { static int member(void); };\n"
332 "int owner(void) { static int value = 1; return value; }\n"
333 "}",
334 False,
335 ),
336}
337
338_MALFORMED_CASES = {
339 "unterminated declarator": ("static int broken(", "c"),
340 "unterminated extern C wrapper": ('extern "C" { static int helper(void);', "c++"),
341 "malformed extern C declaration": ('extern "C" { static int broken( }', "c++"),
342 "unterminated namespace": ("namespace detail { static int helper(void);", "c++"),
343 "unterminated trailing template": (
344 "#include <vector>\nstatic auto helper(void) -> std::vector<int;",
345 "c++",
346 ),
347}
348
349
350def _strip_preprocessor_directives(text: str) -> str:
351 """Replace directives with newlines while respecting continuations."""
352 kept: list[str] = []
353 continued = False
354 for line in text.splitlines(keepends=True):
355 is_directive = continued or line.lstrip().startswith("#")
356 continued = is_directive and line.rstrip().endswith("\\")
357 if is_directive:
358 kept.append("\n" if line.endswith("\n") else "")
359 else:
360 kept.append(line)
361 return "".join(kept)
362
363
364def _splice_lines(text: str) -> str:
365 """Apply C translation-phase backslash-newline deletion."""
366 output: list[str] = []
367 index = 0
368 while index < len(text):
369 if text.startswith("\\\r\n", index):
370 index += 3
371 elif text.startswith("\\\n", index):
372 index += 2
373 else:
374 output.append(text[index])
375 index += 1
376 return "".join(output)
377
378
379def _tokens(text: str) -> list[str]:
380 """Return semantic identifier and punctuation tokens."""
381 # Translation phase 2 precedes comment recognition and preprocessing.
382 # Splicing later would misread a split comment opener or directive and
383 # could invent declarations that the compiler never sees.
384 source = _strip_preprocessor_directives(_splice_lines(text))
385 # Preserve literals so the declarator parser can distinguish a C++ direct
386 # initializer from an empty function parameter list.
387 ignored = {"block_comment", "line_comment", "space"}
388 return [
389 match.group(0) for match in _TOKEN_RE.finditer(source) if match.lastgroup not in ignored
390 ]
391
392
393def _consume_group(tokens: list[str], start: int, end: int) -> int:
394 """Return the token after one balanced (), [], or {} group."""
395 opening = tokens[start]
396 if opening not in _GROUP_OPEN:
397 message = f"expected group opener, got {opening!r}"
398 raise ScanError(message)
399 stack = [_GROUP_CLOSE[opening]]
400 for index in range(start + 1, end):
401 lexeme = tokens[index]
402 if lexeme in _GROUP_OPEN:
403 stack.append(_GROUP_CLOSE[lexeme])
404 elif lexeme in _GROUP_END and (not stack or lexeme != stack.pop()):
405 message = f"mismatched group terminator {lexeme!r}"
406 raise ScanError(message)
407 if lexeme in _GROUP_END and not stack:
408 return index + 1
409 message = f"unterminated {opening!r} group"
410 raise ScanError(message)
411
412
413def _consume_double_bracket(tokens: list[str], start: int, end: int) -> int:
414 """Return the token after a balanced C23/C++ [[attribute]]."""
415 depth = 1
416 index = start + 2
417 while index < end:
418 pair = tokens[index : index + 2]
419 if pair == ["[", "["]:
420 depth += 1
421 index += 2
422 elif pair == ["]", "]"]:
423 depth -= 1
424 index += 2
425 if depth == 0:
426 return index
427 else:
428 index += 1
429 message = "unterminated '[[ attribute ]]' group"
430 raise ScanError(message)
431
432
433def _is_annotation_word(token: str) -> bool:
434 """Recognize project/compiler annotation macro spellings."""
435 return (
436 token in _ANNOTATION_WORDS
437 or token.startswith(("RA8_", "__"))
438 or (token.isupper() and "_" in token)
439 )
440
441
442def _consume_annotation(tokens: list[str], start: int, end: int) -> int | None:
443 """Consume one leading/trailing annotation, if present."""
444 if tokens[start : start + 2] == ["[", "["]:
445 return _consume_double_bracket(tokens, start, end)
446 token = tokens[start]
447 if not _IDENTIFIER_RE.fullmatch(token) or not _is_annotation_word(token):
448 return None
449 after = start + 1
450 if after < end and tokens[after] == "(":
451 return _consume_group(tokens, after, end)
452 return after
453
454
455def _prefix_is_specifiers(prefix: list[str], *, require_static: bool) -> bool:
456 """Accept a declaration-specifier/attribute prefix, not declarator syntax."""
457 if require_static and "static" not in prefix:
458 return False
459 index = 0
460 user_type_names = 0
461 while index < len(prefix):
462 annotation_end = _consume_annotation(prefix, index, len(prefix))
463 if annotation_end is not None:
464 index = annotation_end
465 continue
466 lexeme = prefix[index]
467 if not _IDENTIFIER_RE.fullmatch(lexeme):
468 if lexeme == "{":
469 index = _consume_group(prefix, index, len(prefix))
470 continue
471 return False
472 if lexeme not in _DECL_SPECIFIER_WORDS and lexeme not in _PREFIX_GROUP_WORDS:
473 user_type_names += 1
474 if user_type_names > 1:
475 return False
476 if index + 1 < len(prefix) and prefix[index + 1] == "(":
477 if lexeme not in _PREFIX_GROUP_WORDS:
478 return False
479 index = _consume_group(prefix, index + 1, len(prefix))
480 else:
481 index += 1
482 return bool(prefix) or not require_static
483
484
485def _parse_declarator(tokens: list[str], start: int, end: int) -> tuple[int, list[str]] | None:
486 """Parse one C declarator and return its derived-type operators."""
487 index = start
488 pointers: list[str] = []
489 while index < end and tokens[index] == "*":
490 pointers.append("pointer")
491 index += 1
492 while index < end:
493 annotation_end = _consume_annotation(tokens, index, end)
494 if annotation_end is not None:
495 index = annotation_end
496 elif tokens[index] in _POINTER_QUALIFIERS:
497 index += 1
498 else:
499 break
500 if (
501 index < end
502 and _IDENTIFIER_RE.fullmatch(tokens[index])
503 and tokens[index] not in _DECL_SPECIFIER_WORDS
504 ):
505 index += 1
506 operators: list[str] = []
507 elif index < end and tokens[index] == "(":
508 after_group = _consume_group(tokens, index, end)
509 inner = _parse_declarator(tokens, index + 1, after_group - 1)
510 if inner is None or inner[0] != after_group - 1:
511 return None
512 index, operators = after_group, inner[1]
513 else:
514 return None
515 while index < end and tokens[index] in {"(", "["}:
516 opening = tokens[index]
517 group_end = _consume_group(tokens, index, end)
518 if opening == "(" and not _is_parameter_declaration_clause(
519 tokens[index + 1 : group_end - 1]
520 ):
521 return None
522 index = group_end
523 operators.append("function" if opening == "(" else "array")
524 operators.extend(pointers)
525 return index, operators
526
527
528def _function_suffix_only(tokens: list[str], start: int, end: int) -> bool:
529 """Recognize conservative C++ function qualifiers and trailing returns."""
530 index = start
531 while index < end:
532 annotation_end = _consume_annotation(tokens, index, end)
533 if annotation_end is not None:
534 index = annotation_end
535 continue
536 if tokens[index] in {"&", "const", "volatile"}:
537 index += 1
538 continue
539 if tokens[index] == "noexcept":
540 index += 1
541 if index < end and tokens[index] == "(":
542 index = _consume_group(tokens, index, end)
543 continue
544 if tokens[index : index + 2] == ["-", ">"]:
545 trailing = tokens[index + 2 : end]
546 return bool(trailing) and _is_parameter_declaration(trailing)
547 return False
548 return True
549
550
551def _top_level_parts(tokens: list[str], separator: str) -> list[list[str]]:
552 """Split at one separator while preserving balanced nested groups."""
553 parts: list[list[str]] = [[]]
554 stack: list[str] = []
555 for index, lexeme in enumerate(tokens):
556 template_prefix = index > 0 and (
557 _IDENTIFIER_RE.fullmatch(tokens[index - 1]) is not None
558 or tokens[index - 1] in {">", "]"}
559 )
560 if lexeme == "<" and template_prefix and _template_argument_end(tokens, index) is not None:
561 stack.append(">")
562 elif lexeme in _GROUP_OPEN:
563 stack.append(_GROUP_CLOSE[lexeme])
564 elif lexeme == ">" and stack[-1:] == [">"]:
565 stack.pop()
566 elif lexeme in _GROUP_END and (not stack or lexeme != stack.pop()):
567 message = f"mismatched group terminator {lexeme!r}"
568 raise ScanError(message)
569 if lexeme == separator and not stack:
570 parts.append([])
571 else:
572 parts[-1].append(lexeme)
573 if stack:
574 message = f"unterminated group, expected {stack[-1]!r}"
575 raise ScanError(message)
576 return parts
577
578
579def _without_annotations(tokens: list[str]) -> list[str]:
580 """Remove leading attributes while preserving the declaration body."""
581 return tokens[_annotations_end(tokens, 0) :]
582
583
584def _annotations_end(tokens: list[str], start: int) -> int:
585 """Return the first token after a consecutive attribute sequence."""
586 index = start
587 while index < len(tokens):
588 annotation_end = _consume_annotation(tokens, index, len(tokens))
589 if annotation_end is None:
590 break
591 index = annotation_end
592 return index
593
594
595def _template_argument_end(tokens: list[str], start: int) -> int | None:
596 """Return the end of a balanced C++ template-argument list, if any."""
597 if tokens[start : start + 1] != ["<"]:
598 return None
599 depth = 1
600 index = start + 1
601 while index < len(tokens):
602 lexeme = tokens[index]
603 if lexeme in _GROUP_OPEN:
604 index = _consume_group(tokens, index, len(tokens))
605 continue
606 if lexeme == "<":
607 depth += 1
608 elif lexeme == ">":
609 depth -= 1
610 if depth == 0:
611 return index + 1
612 index += 1
613 return None
614
615
616def _consume_template_arguments(tokens: list[str], start: int) -> int:
617 """Return the token after one balanced C++ template-argument list."""
618 end = _template_argument_end(tokens, start)
619 if end is not None:
620 return end
621 message = "unterminated '<' template-argument group"
622 raise ScanError(message)
623
624
625def _has_qualified_type_prefix(tokens: list[str]) -> tuple[bool, int]:
626 """Return a positive qualified type-name prefix and its end offset."""
627 if not tokens:
628 return False, 0
629 index = 0
630 qualified = tokens[:2] == [":", ":"]
631 if qualified:
632 index = 2
633 if index >= len(tokens) or not _IDENTIFIER_RE.fullmatch(tokens[index]):
634 return False, 0
635 index += 1
636 if tokens[index : index + 1] == ["<"]:
637 index = _consume_template_arguments(tokens, index)
638 while tokens[index : index + 2] == [":", ":"]:
639 qualified = True
640 index += 2
641 if index < len(tokens) and tokens[index] == "template":
642 index += 1
643 if index >= len(tokens) or not _IDENTIFIER_RE.fullmatch(tokens[index]):
644 return False, 0
645 index += 1
646 if tokens[index : index + 1] == ["<"]:
647 index = _consume_template_arguments(tokens, index)
648 return qualified, index
649
650
651def _parameter_type_end(item: list[str]) -> int | None:
652 """Return the end of a positive parameter type, or no type."""
653 index = 0
654 type_is_positive = False
655 while index < len(item):
656 annotation_end = _consume_annotation(item, index, len(item))
657 if annotation_end is not None:
658 index = annotation_end
659 continue
660 token = item[index]
661 if token in _DECL_SPECIFIER_WORDS:
662 type_is_positive = type_is_positive or token in _TYPE_BEARING_SPECIFIERS
663 index += 1
664 if token in _ELABORATED_SPECIFIERS:
665 if index >= len(item) or not _IDENTIFIER_RE.fullmatch(item[index]):
666 return None
667 index += 1
668 type_is_positive = True
669 continue
670 if token in _PREFIX_GROUP_WORDS and item[index + 1 : index + 2] == ["("]:
671 index = _consume_group(item, index + 1, len(item))
672 type_is_positive = True
673 continue
674 break
675 if type_is_positive:
676 return index
677
678 remainder = item[index:]
679 qualified, qualified_end = _has_qualified_type_prefix(remainder)
680 if qualified:
681 return index + qualified_end
682 user_type_has_declarator = len(remainder) >= _MIN_USER_TYPE_DECL_TOKENS and (
683 remainder[1] in {"&", "*"} or _IDENTIFIER_RE.fullmatch(remainder[1])
684 )
685 qualified_by_specifier = index > 0 and len(remainder) == 1
686 if (
687 remainder
688 and _IDENTIFIER_RE.fullmatch(remainder[0])
689 and (user_type_has_declarator or qualified_by_specifier)
690 ):
691 return index + 1
692 return None
693
694
695def _parameter_declarator_is_valid(declarator: list[str]) -> bool:
696 """Return whether tokens form a conservative parameter declarator."""
697 if not declarator:
698 return True
699 if declarator[:1] in (["&"], ["*"]):
700 return all(token in {"&", "*"} or _IDENTIFIER_RE.fullmatch(token) for token in declarator)
701 if _IDENTIFIER_RE.fullmatch(declarator[0]):
702 return all(
703 _IDENTIFIER_RE.fullmatch(token) or token in {"&", "*", "[", "]", "(", ")"}
704 for token in declarator
705 )
706 if declarator[0] == "(":
707 try:
708 return _consume_group(declarator, 0, len(declarator)) <= len(declarator)
709 except ScanError:
710 return False
711 return False
712
713
714def _is_parameter_declaration(tokens: list[str]) -> bool:
715 """Recognize positive parameter-declaration grammar, never expressions."""
716 item = _without_annotations(_before_initializer(tokens))
717 if not item:
718 return False
719 if item == [".", ".", "."]:
720 return True
721 lead = item[0]
722 if lead.startswith(('"', "'")) or lead[:1].isdigit() or lead in _PARAMETER_EXPRESSION_LEADS:
723 return False
724 type_end = _parameter_type_end(item)
725 return type_end is not None and _parameter_declarator_is_valid(item[type_end:])
726
727
728def _is_parameter_declaration_clause(tokens: list[str]) -> bool:
729 """Require every top-level clause item to be a parameter declaration."""
730 if not tokens:
731 return True
732 parts = _top_level_parts(tokens, ",")
733 return bool(parts) and all(_is_parameter_declaration(part) for part in parts)
734
735
736def _before_initializer(tokens: list[str]) -> list[str]:
737 """Discard a top-level initializer from one init-declarator."""
738 return _top_level_parts(tokens, "=")[0]
739
740
741def _item_declares_function(tokens: list[str], *, require_static: bool) -> bool:
742 """Recognize a function declarator in one comma-separated item."""
743 item = _before_initializer(tokens)
744 stack: list[str] = []
745 for start in range(len(item)):
746 at_top_level = not stack
747 lexeme = item[start]
748 if lexeme in _GROUP_OPEN:
749 stack.append(_GROUP_CLOSE[lexeme])
750 elif lexeme in _GROUP_END and (not stack or lexeme != stack.pop()):
751 message = f"mismatched group terminator {lexeme!r}"
752 raise ScanError(message)
753 if not at_top_level or not _prefix_is_specifiers(
754 item[:start], require_static=require_static
755 ):
756 continue
757 parsed = _parse_declarator(item, start, len(item))
758 if parsed is None:
759 continue
760 after, operators = parsed
761 if (
762 operators
763 and operators[0] == "function"
764 and _function_suffix_only(item, after, len(item))
765 ):
766 return True
767 return False
768
769
770def _statement_declares_function(
771 tokens: list[str], *, require_static: bool, exclude_inline: bool
772) -> bool:
773 """Recognize a function in a complete file-scope declaration header."""
774 if not tokens or (require_static and "static" not in tokens):
775 return False
776 if exclude_inline and _INLINE_SPECIFIERS.intersection(tokens):
777 return False
778 for index, item in enumerate(_top_level_parts(tokens, ",")):
779 if _item_declares_function(item, require_static=(require_static and index == 0)):
780 return True
781 return False
782
783
784def _validate_groups(tokens: list[str]) -> None:
785 """Reject malformed input rather than silently routing it as direct."""
786 stack: list[str] = []
787 for lexeme in tokens:
788 if lexeme in _GROUP_OPEN:
789 stack.append(_GROUP_CLOSE[lexeme])
790 elif lexeme in _GROUP_END and (not stack or lexeme != stack.pop()):
791 message = f"mismatched group terminator {lexeme!r}"
792 raise ScanError(message)
793 if stack:
794 message = f"unterminated group, expected {stack[-1]!r}"
795 raise ScanError(message)
796
797
798def _is_language_linkage_wrapper(tokens: list[str]) -> bool:
799 """Recognize canonical C and C++ language-linkage block prefixes."""
800 return tuple(tokens) in {("extern", '"C"'), ("extern", '"C++"')}
801
802
803def _namespace_name_end(tokens: list[str], start: int) -> int | None:
804 """Consume one optionally-inline namespace name and its attributes."""
805 index = start
806 if tokens[index : index + 1] == ["inline"]:
807 index += 1
808 if index >= len(tokens) or not _IDENTIFIER_RE.fullmatch(tokens[index]):
809 return None
810 return _annotations_end(tokens, index + 1)
811
812
813def _is_namespace_wrapper(tokens: list[str]) -> bool:
814 """Recognize named, anonymous, inline, and nested namespace prefixes."""
815 index = 0
816 if tokens[:1] == ["inline"]:
817 index = 1
818 if tokens[index : index + 1] != ["namespace"]:
819 return False
820 index = _annotations_end(tokens, index + 1)
821 if index == len(tokens):
822 return True
823 while index < len(tokens):
824 name_end = _namespace_name_end(tokens, index)
825 if name_end is None:
826 return False
827 index = name_end
828 if index == len(tokens):
829 return True
830 if tokens[index : index + 2] != [":", ":"]:
831 return False
832 index += 2
833 return False
834
835
836def _is_transparent_cpp_wrapper(tokens: list[str]) -> bool:
837 """Return whether a C++ wrapper preserves namespace/file-scope linkage."""
838 return _is_language_linkage_wrapper(tokens) or _is_namespace_wrapper(tokens)
839
840
841def _scope_has_noninline_static(tokens: list[str], start: int, end: int) -> bool:
842 """Scan one real or transparent file scope for a matching declaration."""
843 statement: list[str] = []
844 index = start
845 while index < end:
846 lexeme = tokens[index]
847 if lexeme == "{":
848 after_group = _consume_group(tokens, index, end)
849 if _is_transparent_cpp_wrapper(statement):
850 if _scope_has_noninline_static(tokens, index + 1, after_group - 1):
851 return True
852 statement = []
853 index = after_group
854 continue
855 if _statement_declares_function(statement, require_static=True, exclude_inline=True):
856 return True
857 if _statement_declares_function(statement, require_static=False, exclude_inline=False):
858 statement = []
859 index = after_group
860 continue
861 statement.extend(tokens[index:after_group])
862 index = after_group
863 continue
864 if lexeme == ";":
865 if _statement_declares_function(statement, require_static=True, exclude_inline=True):
866 return True
867 statement = []
868 else:
869 statement.append(lexeme)
870 index += 1
871 return False
872
873
874def has_noninline_static_decl(text: str) -> bool:
875 """Return whether text has a file-scope non-inline static function."""
876 tokens = _tokens(text)
877 _validate_groups(tokens)
878 return _scope_has_noninline_static(tokens, 0, len(tokens))
879
880
881def _clang_accepts(name: str, source: str, language: str) -> bool:
882 """Prove the fixture is accepted C/C++, not parser-only invented syntax."""
883 compiler_name = "clang++-18" if language == "c++" else "clang-18"
884 clang = shutil.which(compiler_name)
885 if clang is None:
886 print(f"static_decl_scan.py selftest: {compiler_name} not found", file=sys.stderr)
887 return False
888 preamble = (
889 "#define RA8_INTERNAL\n"
890 "#define RA8_UNUSED __attribute__((unused))\n"
891 "#define RA8_ANNOTATE(x) __attribute__((annotate(#x)))\n"
892 )
893 # clang is the resolved, fixed-name compiler; no source token reaches argv.
894 result = subprocess.run( # noqa: S603 -- executable is the fixed clang-18 probe
895 [
896 clang,
897 "-std=c++20" if language == "c++" else "-std=c17",
898 "-Wno-gcc-compat",
899 "-fsyntax-only",
900 "-x",
901 language,
902 "-",
903 ],
904 input=preamble + source,
905 text=True,
906 capture_output=True,
907 check=False,
908 )
909 if result.returncode != 0:
910 print(
911 f"static_decl_scan.py selftest: clang rejected {name}:\n{result.stderr}",
912 file=sys.stderr,
913 )
914 return False
915 return True
916
917
918def _run_selftest_case(name: str, source: str, expected: bool, language: str) -> int:
919 """Run one detection direction and its real-compiler syntax proof."""
920 try:
921 actual = has_noninline_static_decl(source)
922 except ScanError as exc:
923 print(f"static_decl_scan.py selftest: {name}: {exc}", file=sys.stderr)
924 return 1
925 mismatch = actual != expected
926 if mismatch:
927 print(
928 f"static_decl_scan.py selftest: {name}: got {actual}, expected {expected}",
929 file=sys.stderr,
930 )
931 return int(mismatch) + int(not _clang_accepts(name, source, language))
932
933
934def _run_selftest() -> int:
935 failures = sum(
936 _run_selftest_case(name, source, expected, language)
937 for language, cases in (("c", _SELFTEST_CASES), ("c++", _CPP_SELFTEST_CASES))
938 for name, (source, expected) in cases.items()
939 )
940 for name, (source, _language) in _MALFORMED_CASES.items():
941 try:
942 has_noninline_static_decl(source)
943 except ScanError:
944 continue
945 print(f"static_decl_scan.py selftest: {name} did not fail closed", file=sys.stderr)
946 failures += 1
947 if failures == 0:
948 print(
949 "static_decl_scan.py --selftest: "
950 f"PASS ({len(_SELFTEST_CASES)} clang-18 + "
951 f"{len(_CPP_SELFTEST_CASES)} clang++-18 cases)"
952 )
953 return failures
954
955
956def _parse_args() -> argparse.Namespace:
957 parser = argparse.ArgumentParser(description=__doc__)
958 parser.add_argument("--selftest", action="store_true", help="run the token scanner selftest")
959 parser.add_argument("path", nargs="?", type=Path, help="header to inspect")
960 return parser.parse_args()
961
962
963def main() -> int:
964 """Run the file scanner or its selftest."""
965 args = _parse_args()
966 if args.selftest:
967 return _run_selftest()
968 if args.path is None:
969 print("static_decl_scan.py: a path is required", file=sys.stderr)
970 return 2
971 try:
972 text = args.path.read_text(encoding="utf-8")
973 matched = has_noninline_static_decl(text)
974 except (OSError, UnicodeError, ScanError) as exc:
975 print(f"static_decl_scan.py: {exc}", file=sys.stderr)
976 return 2
977 return 0 if matched else 1
978
979
980if __name__ == "__main__":
981 raise SystemExit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298