{
"event": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "\/var\/www\/dev.campus.systemische-tools.de\/crawler\/scripts\/max_loc_linter.py",
"content": "\"\"\"MaxLOC linter — enforces 80-LOC hard limit for methods and classes.\n\nCount excludes:\n- blank lines\n- comment-only lines (# ...)\n- docstring lines (first string expression of a function\/class body)\n\nUsage:\n python scripts\/max_loc_linter.py <path> [<path> ...]\n\nExit codes:\n 0 OK\n 1 violation(s) detected\n 2 parse error\n\"\"\"\nfrom __future__ import annotations\n\nimport ast\nimport pathlib\nimport sys\nfrom dataclasses import dataclass\nfrom typing import Iterable\n\nHARD_LIMIT_METHOD = 80\nHARD_LIMIT_CLASS = 80\nSOFT_LIMIT = 50\n\n\n@dataclass(frozen=True)\nclass Violation:\n file: pathlib.Path\n kind: str\n name: str\n lineno: int\n loc: int\n limit: int\n\n\ndef _iter_py_files(paths: Iterable[str]) -> Iterable[pathlib.Path]:\n for raw in paths:\n base = pathlib.Path(raw)\n if base.is_file() and base.suffix == \".py\":\n yield base\n elif base.is_dir():\n yield from (\n p for p in base.rglob(\"*.py\") if \"venv\" not in p.parts and \".venv\" not in p.parts\n )\n\n\ndef _source_lines(src: str) -> list[str]:\n return src.splitlines()\n\n\ndef _count_loc(node: ast.AST, lines: list[str]) -> int:\n start = node.lineno\n end = getattr(node, \"end_lineno\", start)\n docstring_span = _docstring_span(node)\n counted = 0\n for lineno in range(start, end + 1):\n if docstring_span and docstring_span[0] <= lineno <= docstring_span[1]:\n continue\n text = lines[lineno - 1].strip()\n if not text or text.startswith(\"#\"):\n continue\n counted += 1\n return counted\n\n\ndef _docstring_span(node: ast.AST) -> tuple[int, int] | None:\n body = getattr(node, \"body\", None)\n if not body:\n return None\n first = body[0]\n if not isinstance(first, ast.Expr) or not isinstance(first.value, ast.Constant):\n return None\n if not isinstance(first.value.value, str):\n return None\n return (first.lineno, getattr(first, \"end_lineno\", first.lineno))\n\n\ndef _check_tree(path: pathlib.Path, tree: ast.AST, lines: list[str]) -> list[Violation]:\n violations: list[Violation] = []\n for node in ast.walk(tree):\n if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n loc = _count_loc(node, lines)\n if loc > HARD_LIMIT_METHOD:\n violations.append(\n Violation(path, \"method\", node.name, node.lineno, loc, HARD_LIMIT_METHOD),\n )\n elif isinstance(node, ast.ClassDef):\n loc = _count_loc(node, lines)\n if loc > HARD_LIMIT_CLASS:\n violations.append(\n Violation(path, \"class\", node.name, node.lineno, loc, HARD_LIMIT_CLASS),\n )\n return violations\n\n\ndef check_file(path: pathlib.Path) -> list[Violation]:\n src = path.read_text(encoding=\"utf-8\")\n tree = ast.parse(src, filename=str(path))\n return _check_tree(path, tree, _source_lines(src))\n\n\ndef main(argv: list[str]) -> int:\n if not argv:\n print(\"usage: max_loc_linter.py <path> [...]\", file=sys.stderr)\n return 2\n all_violations: list[Violation] = []\n for path in _iter_py_files(argv):\n try:\n all_violations.extend(check_file(path))\n except SyntaxError as exc:\n print(f\"PARSE_ERROR {path}: {exc}\", file=sys.stderr)\n return 2\n for v in all_violations:\n print(\n f\"{v.file}:{v.lineno}: {v.kind} '{v.name}' has {v.loc} LOC (hard limit {v.limit})\",\n file=sys.stderr,\n )\n return 1 if all_violations else 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv[1:]))\n"
}
}
{
"tool_response": {
"type": "create",
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/crawler\/scripts\/max_loc_linter.py",
"content": "\"\"\"MaxLOC linter — enforces 80-LOC hard limit for methods and classes.\n\nCount excludes:\n- blank lines\n- comment-only lines (# ...)\n- docstring lines (first string expression of a function\/class body)\n\nUsage:\n python scripts\/max_loc_linter.py <path> [<path> ...]\n\nExit codes:\n 0 OK\n 1 violation(s) detected\n 2 parse error\n\"\"\"\nfrom __future__ import annotations\n\nimport ast\nimport pathlib\nimport sys\nfrom dataclasses import dataclass\nfrom typing import Iterable\n\nHARD_LIMIT_METHOD = 80\nHARD_LIMIT_CLASS = 80\nSOFT_LIMIT = 50\n\n\n@dataclass(frozen=True)\nclass Violation:\n file: pathlib.Path\n kind: str\n name: str\n lineno: int\n loc: int\n limit: int\n\n\ndef _iter_py_files(paths: Iterable[str]) -> Iterable[pathlib.Path]:\n for raw in paths:\n base = pathlib.Path(raw)\n if base.is_file() and base.suffix == \".py\":\n yield base\n elif base.is_dir():\n yield from (\n p for p in base.rglob(\"*.py\") if \"venv\" not in p.parts and \".venv\" not in p.parts\n )\n\n\ndef _source_lines(src: str) -> list[str]:\n return src.splitlines()\n\n\ndef _count_loc(node: ast.AST, lines: list[str]) -> int:\n start = node.lineno\n end = getattr(node, \"end_lineno\", start)\n docstring_span = _docstring_span(node)\n counted = 0\n for lineno in range(start, end + 1):\n if docstring_span and docstring_span[0] <= lineno <= docstring_span[1]:\n continue\n text = lines[lineno - 1].strip()\n if not text or text.startswith(\"#\"):\n continue\n counted += 1\n return counted\n\n\ndef _docstring_span(node: ast.AST) -> tuple[int, int] | None:\n body = getattr(node, \"body\", None)\n if not body:\n return None\n first = body[0]\n if not isinstance(first, ast.Expr) or not isinstance(first.value, ast.Constant):\n return None\n if not isinstance(first.value.value, str):\n return None\n return (first.lineno, getattr(first, \"end_lineno\", first.lineno))\n\n\ndef _check_tree(path: pathlib.Path, tree: ast.AST, lines: list[str]) -> list[Violation]:\n violations: list[Violation] = []\n for node in ast.walk(tree):\n if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n loc = _count_loc(node, lines)\n if loc > HARD_LIMIT_METHOD:\n violations.append(\n Violation(path, \"method\", node.name, node.lineno, loc, HARD_LIMIT_METHOD),\n )\n elif isinstance(node, ast.ClassDef):\n loc = _count_loc(node, lines)\n if loc > HARD_LIMIT_CLASS:\n violations.append(\n Violation(path, \"class\", node.name, node.lineno, loc, HARD_LIMIT_CLASS),\n )\n return violations\n\n\ndef check_file(path: pathlib.Path) -> list[Violation]:\n src = path.read_text(encoding=\"utf-8\")\n tree = ast.parse(src, filename=str(path))\n return _check_tree(path, tree, _source_lines(src))\n\n\ndef main(argv: list[str]) -> int:\n if not argv:\n print(\"usage: max_loc_linter.py <path> [...]\", file=sys.stderr)\n return 2\n all_violations: list[Violation] = []\n for path in _iter_py_files(argv):\n try:\n all_violations.extend(check_file(path))\n except SyntaxError as exc:\n print(f\"PARSE_ERROR {path}: {exc}\", file=sys.stderr)\n return 2\n for v in all_violations:\n print(\n f\"{v.file}:{v.lineno}: {v.kind} '{v.name}' has {v.loc} LOC (hard limit {v.limit})\",\n file=sys.stderr,\n )\n return 1 if all_violations else 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv[1:]))\n",
"structuredPatch": [],
"originalFile": null,
"userModified": false
}
}