Skip to content

Commit 56733d4

Browse files
authored
Merge pull request #149 from Miyamura80/ci/file-length-check
Add file length CI check (500-line limit)
2 parents f14e8e4 + 450568c commit 56733d4

5 files changed

Lines changed: 99 additions & 1 deletion

File tree

.cursor/rules/file_length.mdc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
description: File length limits for Python files
3+
globs: *.py
4+
alwaysApply: false
5+
---
6+
## File Length Limit
7+
8+
Python files must not exceed 500 lines. This is enforced by `make file_len_check` (part of `make ci`) and in the linter GitHub Actions workflow.
9+
10+
This limit exists to prevent AI-generated code from becoming monolithic and forces refactoring into smaller, more modular components.
11+
12+
If a file genuinely needs to exceed 500 lines, add it to the `exclude` list in `pyproject.toml` under `[tool.file_length]`:
13+
14+
```toml
15+
[tool.file_length]
16+
max_lines = 500
17+
exclude = [
18+
"onboard.py",
19+
]
20+
```
21+
22+
Do not add files to the exclude list without a clear justification.

.github/workflows/linter_require_ruff.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ jobs:
3838
run: uv run python scripts/check_ai_writing.py
3939
- name: Run import-linter
4040
run: make import_lint
41+
- name: Run file length check
42+
run: uv run python scripts/check_file_length.py

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,12 @@ check_deps: install_tools ## Check for unused dependencies
248248
@uv run deptry .
249249
@echo "$(GREEN)✅Dependency check completed.$(RESET)"
250250

251-
ci: ruff vulture import_lint ty docs_lint lint_links check_deps ## Run all CI checks (ruff, vulture, import_lint, ty, docs_lint, lint_links)
251+
file_len_check: check_uv ## Check Python files don't exceed max line count
252+
@echo "$(YELLOW)🔍Checking file lengths...$(RESET)"
253+
@uv run python scripts/check_file_length.py
254+
@echo "$(GREEN)✅File length check completed.$(RESET)"
255+
256+
ci: ruff vulture import_lint ty docs_lint lint_links check_deps file_len_check ## Run all CI checks (ruff, vulture, import_lint, ty, docs_lint, lint_links, file_len_check)
252257
@echo "$(GREEN)✅CI checks completed.$(RESET)"
253258

254259
########################################################

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ exclude = [
9595
"onboard.py"
9696
]
9797

98+
[tool.file_length]
99+
max_lines = 500
100+
exclude = [
101+
"onboard.py",
102+
]
103+
98104
[tool.coverage.run]
99105
branch = true
100106
source = ["src", "common", "utils"]

scripts/check_file_length.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from __future__ import annotations
2+
3+
import pathlib
4+
import tomllib
5+
6+
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
7+
ROOT_SKIP_DIRS = {
8+
".git",
9+
".venv",
10+
".uv_cache",
11+
".uv-cache",
12+
".uv_tools",
13+
".uv-tools",
14+
".cache",
15+
"node_modules",
16+
".next",
17+
}
18+
RECURSIVE_SKIP_DIRS = {"__pycache__", ".pytest_cache"}
19+
20+
21+
def load_config() -> tuple[int, set[str]]:
22+
pyproject = REPO_ROOT / "pyproject.toml"
23+
with open(pyproject, "rb") as f:
24+
data = tomllib.load(f)
25+
cfg = data.get("tool", {}).get("file_length", {})
26+
max_lines = cfg.get("max_lines", 500)
27+
exclude = set(cfg.get("exclude", []))
28+
return max_lines, exclude
29+
30+
31+
def main() -> int:
32+
max_lines, exclude = load_config()
33+
violations: list[tuple[pathlib.Path, int]] = []
34+
35+
for path in REPO_ROOT.rglob("*.py"):
36+
rel = path.relative_to(REPO_ROOT)
37+
parts = rel.parts
38+
if parts[0] in ROOT_SKIP_DIRS:
39+
continue
40+
if any(part in RECURSIVE_SKIP_DIRS for part in parts[:-1]):
41+
continue
42+
if rel.as_posix() in exclude:
43+
continue
44+
line_count = len(path.read_text(encoding="utf-8", errors="ignore").splitlines())
45+
if line_count > max_lines:
46+
violations.append((rel, line_count))
47+
48+
if violations:
49+
print(f"File length check failed: {len(violations)} file(s) exceed {max_lines} lines")
50+
for rel_path, count in sorted(violations):
51+
print(f" {rel_path}: {count} lines")
52+
print(
53+
"Refactor large files into smaller modules, "
54+
"or add to [tool.file_length] exclude in pyproject.toml."
55+
)
56+
return 1
57+
58+
print(f"File length check passed (all files <= {max_lines} lines).")
59+
return 0
60+
61+
62+
if __name__ == "__main__":
63+
raise SystemExit(main())

0 commit comments

Comments
 (0)