3"""Shell function-body measurement for ``check_function_size.py``.
5Shell has two body forms and this tree uses both deliberately:
7 name() { ... } the ordinary function
8 name() ( ... ) a SUBSHELL body
10The subshell form is load-bearing in ``scripts/ci.sh``: every multi-command
11gate body is ``gate_x() ( set -e; ... )`` so that ``set -e`` is scoped to the
12gate and a mid-body failure cannot be swallowed. A parser that only knew
13``{`` would silently measure nothing for the majority of gate bodies -- the
14exact "gate that quietly stopped matching" failure this repository keeps
15finding, so both forms are handled and the selftest asserts both.
17Like the C side this is a textual scan, and for the same reason: there is no
18shell equivalent of a compile database to lean on. Depth tracking ignores
19delimiters inside single and double quotes, inside ``#`` comments, and inside
20heredoc bodies -- a heredoc carrying JSON or a CMake fragment is full of braces
24from __future__
import annotations
27from pathlib
import Path
31_OPEN_RE = re.compile(
r"^\s*(?:function\s+)?([A-Za-z_][A-Za-z0-9_:.-]*)\s*(?:\(\))\s*([({])?\s*$")
33_PAIRS = {
"{":
"}",
"(":
")"}
36def _strip_inert(line: str) -> str:
37 r"""Blank out quoted spans and trailing comments, keeping length irrelevant.
39 Only the delimiters matter downstream, so quoted text is replaced rather
40 than removed. Escapes are honoured so ``\\"`` does not end a string.
48 if ch ==
"\\" and quote ==
'"':
62 if ch ==
"#" and (
not out
or out[-1].isspace()):
69def _heredoc_tag(line: str) -> str |
None:
70 """The delimiter of a heredoc opened on `line`, or None."""
71 match = re.search(
r"<<-?\s*[\"']?([A-Za-z_][A-Za-z0-9_]*)[\"']?", line)
72 return match.group(1)
if match
else None
75def _body_end(lines: list[str], open_idx: int, opener: str) -> int:
76 """Index one past the body whose opening delimiter is on `lines[open_idx]`."""
77 closer = _PAIRS[opener]
79 heredoc: str |
None =
None
81 while idx < len(lines):
83 if heredoc
is not None:
84 if raw.strip() == heredoc:
88 clean = _strip_inert(raw)
89 depth += clean.count(opener) - clean.count(closer)
90 tag = _heredoc_tag(clean)
99def scan(path: Path, threshold: int) -> list[tuple[int, int, str]]:
100 """Return (start_line, length, signature) for functions over `threshold`."""
102 lines = path.read_text(encoding=
"utf-8", errors=
"replace").splitlines()
106 out: list[tuple[int, int, str]] = []
108 while idx < len(lines):
109 match = _OPEN_RE.match(lines[idx])
113 opener = match.group(2)
119 while nxt < len(lines)
and not lines[nxt].strip():
121 if nxt >= len(lines)
or lines[nxt].strip()
not in (
"{",
"("):
124 opener = lines[nxt].strip()
126 end = _body_end(lines, open_idx, opener)
128 if length > threshold:
129 out.append((idx + 1, length, lines[idx].strip()))
130 idx = max(end, idx + 1)