ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
funcsize_c.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""C / C++ function-body measurement for ``check_function_size.py``.
4
5Split out of the checker when the gate grew Python and shell parsers (#359):
6three languages in one file put it well over the 1000-line file cap it is
7itself half of, and a size gate that cannot pass its own rule has no standing
8to enforce it.
9
10The measurement is textual, not a real parse. That is deliberate: the point of
11this backstop is to cover translation units that never reach
12``compile_commands.json`` -- the ARM-cross-compiled ones clang-tidy's
13``readability-function-size`` never sees -- so it cannot depend on a compile
14database existing. The heuristics below (brace-depth tracking that ignores
15literals and comments, a preprocessor-arm stack) are what make that textual
16scan agree with a real parse on this codebase.
17
18``scan(path, threshold)`` returns ``(start_line, length, signature)`` for every
19function over `threshold` lines.
20"""
21
22from __future__ import annotations
23
24from pathlib import Path
25
26# Heuristic: a top-level function body opens with ``{`` on its own line
27# (or, less commonly, at the end of the signature line). The signature
28# spans the lines immediately above ``{`` whose first non-whitespace
29# character is *not* a control-flow keyword.
30_CONTROL_PREFIXES = (
31 "if ",
32 "if(",
33 "for ",
34 "for(",
35 "while ",
36 "while(",
37 "switch ",
38 "switch(",
39 "else",
40 "do ",
41 "do{",
42 "do\t",
43 "//",
44 "/*",
45 "*",
46 "}",
47)
48
49
50def _brace_delta(line: str, in_block_comment: bool) -> tuple[int, int, bool]:
51 r"""Net brace delta for one line, ignoring braces inside literals and comments.
52
53 A naive ``line.count("{")`` miscounts every brace that appears in a
54 textual constant -- a JSON/CSS/JS blob written as a C string literal
55 (``"{\\"$schema\\":..."``) or a ``case '{':`` character literal in a
56 tokenizer. Those braces do not open a real scope, so counting them
57 runs the depth tracker off the true closing ``}`` and reports a short
58 function (e.g. ``prof_write_speedscope``, 49 lines) as hundreds of
59 lines. This scanner walks the line character by character and ignores
60 any brace that is not live C punctuation.
61
62 Only ``in_block_comment`` persists across lines: C string and character
63 literals do not span source lines in this codebase (no backslash-newline
64 line-continued literals -- adjacent string concatenation is used
65 instead), so they are always resolved within the one line.
66
67 Returns ``(opens, closes, in_block_comment_after)``.
68 """
69 opens = 0
70 closes = 0
71 i = 0
72 n = len(line)
73 # "" while outside a literal; otherwise the opening quote (`"` or `'`).
74 # Unifying string and character literals keeps the branch count down.
75 in_quote = ""
76 while i < n:
77 c = line[i]
78 if in_block_comment:
79 if c == "*" and i + 1 < n and line[i + 1] == "/":
80 in_block_comment = False
81 i += 2
82 continue
83 i += 1
84 continue
85 if in_quote:
86 if c == "\\": # skip the escaped character (e.g. \" or \\‍)
87 i += 2
88 continue
89 if c == in_quote:
90 in_quote = ""
91 i += 1
92 continue
93 # Outside any literal or comment: interpret the punctuation.
94 if c == "/" and i + 1 < n and line[i + 1] == "/":
95 break # line comment: the remainder of the line is inert
96 if c == "/" and i + 1 < n and line[i + 1] == "*":
97 in_block_comment = True
98 i += 2
99 continue
100 if c in ('"', "'"):
101 in_quote = c
102 elif c == "{":
103 opens += 1
104 elif c == "}":
105 closes += 1
106 i += 1
107 return opens, closes, in_block_comment
108
109
110def _looks_like_function_body_open(prev: str) -> bool:
111 """Return True if `prev` is the last line of a function signature.
112
113 Function signatures end in ``)`` (possibly followed by a trailing
114 space). Macros and control statements that open a brace also end
115 in ``)`` -- those are filtered out by the caller via the keyword
116 check.
117 """
118 stripped = prev.rstrip()
119 if not stripped.endswith(")"):
120 return False
121 head = prev.lstrip()
122 return all(not head.startswith(prefix) for prefix in _CONTROL_PREFIXES)
123
124
125def _function_signature_start(lines: list[str], brace_idx: int) -> int:
126 """Find the line where the function signature began.
127
128 Walks backward from the line carrying the opening ``{``.
129
130 Heuristic: keep walking while previous lines look like continuation
131 of the same declaration (no terminator like ``;``, ``}``, ``*/``
132 on the previous line). Stop once we hit a blank line, a
133 terminator, or the start of the file.
134 """
135 i = brace_idx - 1
136 while i > 0:
137 prev = lines[i - 1].rstrip()
138 if (not prev) or prev.endswith((";", "}", "*/")):
139 break
140 # Stop on preprocessor directives -- `#pragma` and `#if` blocks
141 # sit between functions and are not part of any signature.
142 if prev.lstrip().startswith("#"):
143 break
144 i -= 1
145 return i
146
147
148def _measure_body(lines: list[str], brace_idx: int, n: int) -> int:
149 """Line index one past the end of the function body.
150
151 The body is the one whose opening ``{`` sits on ``lines[brace_idx]``.
152
153 Tracks brace depth from the opening ``{``, ignoring braces that sit inside
154 string / character literals or comments (via `_brace_delta`). A
155 preprocessor-conditional arm stack keeps a brace opened in a *later*
156 ``#elif``/``#else`` arm from double-counting a brace the first arm already
157 opened -- only one arm is ever compiled, so counting both would run the
158 depth off the true closing ``}`` (e.g. the poll-vs-direct
159 ``#if RA8_OFF_TARGET { ... #else { ... #endif`` idiom). Each stack
160 entry is False in the first arm and True once an ``#elif``/``#else`` is
161 seen; brace deltas are applied only while every entry is False.
162 """
163 # Seed the depth from the opening line itself rather than assuming it
164 # contributes exactly one. An empty body written `{}` on one line closes
165 # immediately; assuming depth 1 ran the tracker past it and swallowed the
166 # whole enclosing scope -- a C++ constructor with an initialiser list and a
167 # `{}` body was reported as its entire 124-line class.
168 opens, closes, in_block_comment_seed = _brace_delta(lines[brace_idx], in_block_comment=False)
169 depth = opens - closes
170 if depth <= 0:
171 return brace_idx + 1
172 j = brace_idx + 1
173 cpp_arms: list[bool] = []
174 # Block comments straddle lines, so their state persists across the body
175 # scan; string / character literals are always resolved within one line.
176 in_block_comment = in_block_comment_seed
177 while j < n and depth > 0:
178 if not in_block_comment:
179 stripped = lines[j].lstrip()
180 if stripped.startswith("#"):
181 directive = stripped[1:].lstrip()
182 if directive.startswith("endif"):
183 if cpp_arms:
184 cpp_arms.pop()
185 elif directive.startswith(("else", "elif")):
186 if cpp_arms:
187 cpp_arms[-1] = True
188 elif directive.startswith("if"): # if / ifdef / ifndef
189 cpp_arms.append(False)
190 j += 1
191 continue
192 opens, closes, in_block_comment = _brace_delta(lines[j], in_block_comment)
193 if not any(cpp_arms):
194 depth += opens
195 depth -= closes
196 j += 1
197 return j
198
199
200def scan(path: Path, threshold: int) -> list[tuple[int, int, str]]:
201 """Every function in ``path`` whose body exceeds ``threshold`` lines.
202
203 Returns ``(function_start_line, length, signature)`` per offender; an
204 unreadable file yields an empty list rather than raising.
205 """
206 try:
207 text = path.read_text()
208 except (OSError, UnicodeDecodeError):
209 return []
210
211 lines = text.splitlines()
212 violations: list[tuple[int, int, str]] = []
213 i = 0
214 n = len(lines)
215 while i < n:
216 line = lines[i].lstrip()
217 if line.startswith("{") and i > 0 and _looks_like_function_body_open(lines[i - 1]):
218 sig_start = _function_signature_start(lines, i)
219 j = _measure_body(lines, i, n)
220 length = j - i
221 if length > threshold:
222 signature = lines[sig_start].strip()
223 # 1-based line for editor friendliness.
224 violations.append((sig_start + 1, length, signature))
225 i = j
226 continue
227 i += 1
228 return violations