aboutsummaryrefslogtreecommitdiff
path: root/tools/lint_project.py
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-18 21:10:37 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-18 21:10:37 +0200
commit2292bf700d28ade39cf2d64b45509e6db21034bc (patch)
treeef455320d39ebe51db236219dbbda2b148dcbe18 /tools/lint_project.py
parent85f1ecabd7f58f306b2a042376209ac8cf49ab9b (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.py74
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: