diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-18 21:10:37 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-18 21:10:37 +0200 |
| commit | 2292bf700d28ade39cf2d64b45509e6db21034bc (patch) | |
| tree | ef455320d39ebe51db236219dbbda2b148dcbe18 /tools/lint_project.py | |
| parent | 85f1ecabd7f58f306b2a042376209ac8cf49ab9b (diff) | |
test(tools): add screenshot, budget and product checks; enforce file headers
- sim_screenshot.py finds the device drawing in the simulator window,
crops the screen and compares it with approved golden images.
- check_budgets.py reads key=value DIAG/HARNESS lines (the simulator
console escapes quotes, so JSON was dropped).
- ciq_test_report.py accepts SDK 9.2.0's real summary line
`PASSED (passed=N, failed=N, errors=N)`; a captured log is a fixture.
- gen_products.py checks all manifest products are watches with
API 5.0+ and at least 512 KB for apps.
- lint_project.py rule HDR001: every file carries the Author/Summary
header (JSON keys, PNG text chunks; generated package-lock exempt).
- make_icon.py --all draws one icon per launcher size in use.
- Golden screenshots for fr255s, venusq2, fr70 and fr965.
- 44 tests, warnings as errors, ruff and mypy --strict clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/lint_project.py')
| -rw-r--r-- | tools/lint_project.py | 74 |
1 files changed, 71 insertions, 3 deletions
diff --git a/tools/lint_project.py b/tools/lint_project.py index f11d35e..e02215b 100644 --- a/tools/lint_project.py +++ b/tools/lint_project.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -"""Project-specific lint rules for the Monkey C sources and the manifest. +# Author: cirakg & claude ai +# Summary: Project-specific lint rules for the Monkey C sources, the manifest and the file headers. +"""Project-specific lint rules. The Monkey C compiler (strict type checking, `make lint`) catches language errors. These rules catch project policy violations the compiler can't see: @@ -14,6 +16,11 @@ errors. These rules catch project policy violations the compiler can't see: can be allowed with the comment `// lint: allow-literal` on its line. MC005 no network: manifest.xml declares no permissions and no source file imports or uses Toybox.Communications + HDR001 every project file names its author and summarises itself: + "Author: cirakg & claude ai" and "Summary: ..." in its first lines; + JSON files use "author"/"_author" and "description"/"_summary" keys; + PNG images use Author and Description text chunks. Files that a tool + regenerates (GENERATED below) are exempt. Usage: lint_project.py [ROOT] Exit: 0 clean, 1 problems found (printed as file:line: CODE message). @@ -21,13 +28,24 @@ Exit: 0 clean, 1 problems found (printed as file:line: CODE message). from __future__ import annotations +import json +import os import re import sys from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path +from PIL import Image + +AUTHOR = "cirakg & claude ai" +HEADER_LINES = 15 DIAG_FILE = Path("source/util/Diag.mc") +# Folders that hold local tool state or generated output, never project files. +SKIP_DIRS = {".git", ".cache", "build", "node_modules", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"} +# Written by a tool that would drop a hand-added header on the next run. +GENERATED = {Path("docker/docs/package-lock.json")} + BASE32_SECRET_RE = re.compile(r'"[^"\n]*[A-Z2-7]{16,}[^"\n]*"') HEX_SECRET_RE = re.compile(r'"[^"\n]*[0-9a-fA-F]{32,}[^"\n]*"') MARKER_RE = re.compile(r"\b(TODO|FIXME|XXX)\b") @@ -35,6 +53,8 @@ PRINTLN_RE = re.compile(r"\bSystem\.println\s*\(") COMMUNICATIONS_RE = re.compile(r"\bToybox\.Communications\b|\bCommunications\.") PERMISSION_RE = re.compile(r"<iq:uses-permission\b") ALLOW_LITERAL = "lint: allow-literal" +AUTHOR_RE = re.compile(r"Author:\s+" + re.escape(AUTHOR)) +SUMMARY_RE = re.compile(r"Summary:\s+\S") @dataclass(frozen=True, order=True) @@ -86,7 +106,8 @@ def check_manifest(path: Path, rel: Path) -> Iterable[Problem]: yield Problem(rel, number, "MC005", "manifest must not request permissions") -def lint(root: Path) -> list[Problem]: +def lint_code(root: Path) -> list[Problem]: + """The MC rules: Monkey C sources, tests and the manifest.""" problems: list[Problem] = [] for folder in ("source", "test"): for path in sorted((root / folder).rglob("*.mc")): @@ -96,7 +117,54 @@ def lint(root: Path) -> list[Problem]: problems += check_manifest(manifest, manifest.relative_to(root)) else: problems.append(Problem(Path("manifest.xml"), 1, "MC005", "manifest.xml is missing")) - return sorted(problems) + return problems + + +def project_files(root: Path) -> list[Path]: + """Every file of the project, skipping tool state and build output.""" + files = [] + for folder, dirs, names in os.walk(root): + dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS) + files += [Path(folder) / name for name in sorted(names)] + return files + + +def check_header(path: Path, rel: Path) -> Iterable[Problem]: + """HDR001 for one file, by file type.""" + if rel in GENERATED: + return + suffix = path.suffix.lower() + if suffix == ".png": + with Image.open(path) as img: + info = img.info + if info.get("Author") != AUTHOR or not info.get("Description"): + yield Problem(rel, 1, "HDR001", f"PNG needs Author '{AUTHOR}' and Description text chunks") + return + if suffix == ".json": + data = json.loads(path.read_text(encoding="utf-8")) + meta = data if isinstance(data, dict) else {} + if meta.get("author", meta.get("_author")) != AUTHOR or not meta.get("description", meta.get("_summary")): + yield Problem(rel, 1, "HDR001", 'JSON needs "author"/"_author" and "description"/"_summary" keys') + return + try: + head = "\n".join(path.read_text(encoding="utf-8").splitlines()[:HEADER_LINES]) + except UnicodeDecodeError: + yield Problem(rel, 1, "HDR001", "binary file type without a header convention") + return + if not AUTHOR_RE.search(head) or not SUMMARY_RE.search(head): + message = f"needs 'Author: {AUTHOR}' and 'Summary: ...' in its first {HEADER_LINES} lines" + yield Problem(rel, 1, "HDR001", message) + + +def lint_headers(root: Path) -> list[Problem]: + problems: list[Problem] = [] + for path in project_files(root): + problems += check_header(path, path.relative_to(root)) + return problems + + +def lint(root: Path) -> list[Problem]: + return sorted(lint_code(root) + lint_headers(root)) def main(argv: list[str]) -> int: |
