3"""C / C++ function-body measurement for ``check_function_size.py``.
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
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.
18``scan(path, threshold)`` returns ``(start_line, length, signature)`` for every
19function over `threshold` lines.
22from __future__
import annotations
24from pathlib
import Path
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.
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.
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.
67 Returns ``(opens, closes, in_block_comment_after)``.
79 if c ==
"*" and i + 1 < n
and line[i + 1] ==
"/":
80 in_block_comment =
False
94 if c ==
"/" and i + 1 < n
and line[i + 1] ==
"/":
96 if c ==
"/" and i + 1 < n
and line[i + 1] ==
"*":
97 in_block_comment =
True
107 return opens, closes, in_block_comment
110def _looks_like_function_body_open(prev: str) -> bool:
111 """Return True if `prev` is the last line of a function signature.
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
118 stripped = prev.rstrip()
119 if not stripped.endswith(
")"):
122 return all(
not head.startswith(prefix)
for prefix
in _CONTROL_PREFIXES)
125def _function_signature_start(lines: list[str], brace_idx: int) -> int:
126 """Find the line where the function signature began.
128 Walks backward from the line carrying the opening ``{``.
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.
137 prev = lines[i - 1].rstrip()
138 if (
not prev)
or prev.endswith((
";",
"}",
"*/")):
142 if prev.lstrip().startswith(
"#"):
148def _measure_body(lines: list[str], brace_idx: int, n: int) -> int:
149 """Line index one past the end of the function body.
151 The body is the one whose opening ``{`` sits on ``lines[brace_idx]``.
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.
168 opens, closes, in_block_comment_seed = _brace_delta(lines[brace_idx], in_block_comment=
False)
169 depth = opens - closes
173 cpp_arms: list[bool] = []
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"):
185 elif directive.startswith((
"else",
"elif")):
188 elif directive.startswith(
"if"):
189 cpp_arms.append(
False)
192 opens, closes, in_block_comment = _brace_delta(lines[j], in_block_comment)
193 if not any(cpp_arms):
200def scan(path: Path, threshold: int) -> list[tuple[int, int, str]]:
201 """Every function in ``path`` whose body exceeds ``threshold`` lines.
203 Returns ``(function_start_line, length, signature)`` per offender; an
204 unreadable file yields an empty list rather than raising.
207 text = path.read_text()
208 except (OSError, UnicodeDecodeError):
211 lines = text.splitlines()
212 violations: list[tuple[int, int, str]] = []
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)
221 if length > threshold:
222 signature = lines[sig_start].strip()
224 violations.append((sig_start + 1, length, signature))