3"""Python function-body measurement for ``check_function_size.py``.
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``.
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.
17Nothing else is subtracted. Blank lines and comments are inside the C span, so
18they stay inside the Python one.
20Unlike the C side this is a real parse (``ast``), so nested functions,
21decorators, and multi-line signatures are exact rather than heuristic.
24from __future__
import annotations
27from pathlib
import Path
30def _docstring_lines(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
31 """Physical line count of `node`'s docstring, or 0 if it has none."""
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
41 return first.end_lineno - first.lineno + 1
45def _signature(source_lines: list[str], node: ast.AST) -> str:
46 """The ``def`` line, for the diagnostic table."""
48 if 0 <= idx < len(source_lines):
49 return source_lines[idx].strip()
53def scan(path: Path, threshold: int) -> list[tuple[int, int, str]]:
54 """Return (start_line, length, signature) for functions over `threshold`.
56 A file that does not parse yields nothing here; ``check_ruff.py`` owns
57 syntax errors and reporting them twice helps nobody.
60 text = path.read_text(encoding=
"utf-8", errors=
"replace")
64 tree = ast.parse(text)
65 except (SyntaxError, ValueError):
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)):
73 if node.end_lineno
is None:
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)))