diff --git a/toolgate/scripts/check_file_sizes.py b/toolgate/scripts/check_file_sizes.py index 8270a58..1237bad 100644 --- a/toolgate/scripts/check_file_sizes.py +++ b/toolgate/scripts/check_file_sizes.py @@ -14,13 +14,24 @@ from pathlib import Path LIMIT = 600 -SUFFIXES = (".py", ".ts", ".tsx", ".js", ".mjs") +SUFFIXES = (".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".css", ".sh") ALLOWLIST = Path("file-size-allowlist.txt") def tracked_sources() -> list[str]: out = subprocess.run(["git", "ls-files"], capture_output=True, text=True, check=True).stdout - return [path for path in out.splitlines() if path.endswith(SUFFIXES)] + return [path for path in out.splitlines() if path.endswith(SUFFIXES) or _script(path)] + + +def _script(path: str) -> bool: + """A file with no suffix that starts with "#!" is a script (toolgate/cli/toolgate).""" + if "." in Path(path).name: + return False + try: + with open(path, "rb") as handle: + return handle.read(2) == b"#!" + except OSError: + return False def allowlist() -> dict[str, int]: @@ -39,7 +50,8 @@ def main() -> int: allowed = allowlist() problems = [] for path in tracked_sources(): - lines = Path(path).read_text(encoding="utf-8", errors="replace").count("\n") + # splitlines also counts a last line that has no final newline. + lines = len(Path(path).read_text(encoding="utf-8", errors="replace").splitlines()) ceiling = allowed.pop(path, None) if ceiling is None and lines > LIMIT: problems.append(f"{path}: {lines} lines, limit {LIMIT}. Split it by concern.") diff --git a/toolgate/tests/test_guard_scripts.py b/toolgate/tests/test_guard_scripts.py index 1acf898..4b1c5d5 100644 --- a/toolgate/tests/test_guard_scripts.py +++ b/toolgate/tests/test_guard_scripts.py @@ -11,6 +11,8 @@ import sys from pathlib import Path +import pytest + SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" FILE_SIZES = SCRIPTS / "check_file_sizes.py" BOUNDARIES = SCRIPTS / "check_package_boundaries.py" @@ -62,6 +64,29 @@ def test_file_one_line_over_the_limit_fails(self, tmp_path): assert result.returncode == 1 assert "mod.py: 601 lines, limit 600. Split it by concern." in result.stdout + @pytest.mark.parametrize("name", ["App.jsx", "theme.css", "run.sh", "tool.cjs"]) + def test_dashboard_styles_and_shell_files_are_measured(self, tmp_path, name): + write(tmp_path, name, source_file(601)) + track_everything(tmp_path) + result = run_script(FILE_SIZES, tmp_path) + assert result.returncode == 1 + assert f"{name}: 601 lines, limit 600." in result.stdout + + def test_a_script_without_a_suffix_is_measured_by_its_shebang(self, tmp_path): + write(tmp_path, "bin/toolgate", "#!/usr/bin/env python3\n" + source_file(600)) + write(tmp_path, "LICENSE", source_file(900)) + track_everything(tmp_path) + result = run_script(FILE_SIZES, tmp_path) + assert result.returncode == 1 + assert result.stdout.strip() == "bin/toolgate: 601 lines, limit 600. Split it by concern." + + def test_a_last_line_without_a_newline_counts(self, tmp_path): + write(tmp_path, "mod.py", source_file(600) + "# line 601 without a newline") + track_everything(tmp_path) + result = run_script(FILE_SIZES, tmp_path) + assert result.returncode == 1 + assert "mod.py: 601 lines, limit 600." in result.stdout + def test_allowlisted_file_at_its_ceiling_passes(self, tmp_path): write(tmp_path, "big.py", source_file(700)) write(tmp_path, FILE_SIZE_ALLOWLIST, "# already large when the check arrived\nbig.py 700\n")