ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
funcsize_py.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Python function-body measurement for ``check_function_size.py``.
4
5Measures the same span the C rule measures, which needs one deliberate
6adjustment rather than a different number -- see the "what is counted"
7discussion in ``check_function_size.py``.
8
9In C, a function's Doxygen contract sits *above* the signature and is therefore
10outside the measured span. In Python the docstring is the first statement of
11the body and would be inside it, so a heavily documented function would count as
12oversized purely for being documented -- in a repository whose stated policy is
13maximum documentation. The docstring span is therefore subtracted, which makes
14the two languages measure the same thing: the code a reader has to hold in
15their head, with the contract excluded in both.
16
17Nothing else is subtracted. Blank lines and comments are inside the C span, so
18they stay inside the Python one.
19
20Unlike the C side this is a real parse (``ast``), so nested functions,
21decorators, and multi-line signatures are exact rather than heuristic.
22"""
23
24from __future__ import annotations
25
26import ast
27from pathlib import Path
28
29
30def _docstring_lines(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
31 """Physical line count of `node`'s docstring, or 0 if it has none."""
32 if not node.body:
33 return 0
34 first = node.body[0]
35 if (
36 isinstance(first, ast.Expr)
37 and isinstance(first.value, ast.Constant)
38 and isinstance(first.value.value, str)
39 and first.end_lineno is not None
40 ):
41 return first.end_lineno - first.lineno + 1
42 return 0
43
44
45def _signature(source_lines: list[str], node: ast.AST) -> str:
46 """The ``def`` line, for the diagnostic table."""
47 idx = node.lineno - 1
48 if 0 <= idx < len(source_lines):
49 return source_lines[idx].strip()
50 return "<unknown>"
51
52
53def scan(path: Path, threshold: int) -> list[tuple[int, int, str]]:
54 """Return (start_line, length, signature) for functions over `threshold`.
55
56 A file that does not parse yields nothing here; ``check_ruff.py`` owns
57 syntax errors and reporting them twice helps nobody.
58 """
59 try:
60 text = path.read_text(encoding="utf-8", errors="replace")
61 except OSError:
62 return []
63 try:
64 tree = ast.parse(text)
65 except (SyntaxError, ValueError):
66 return []
67
68 source_lines = text.splitlines()
69 out: list[tuple[int, int, str]] = []
70 for node in ast.walk(tree):
71 if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
72 continue
73 if node.end_lineno is None:
74 continue
75 length = node.end_lineno - node.lineno + 1 - _docstring_lines(node)
76 if length > threshold:
77 out.append((node.lineno, length, _signature(source_lines, node)))
78 out.sort()
79 return out