1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
#!/usr/bin/env python3
"""Project-specific lint rules for the Monkey C sources and the manifest.
The Monkey C compiler (strict type checking, `make lint`) catches language
errors. These rules catch project policy violations the compiler can't see:
MC001 every .mc file starts with a comment header explaining the file
MC002 System.println only in source/util/Diag.mc (no logging, so no secret
can ever end up in a log) and in test code
MC003 no TODO / FIXME / XXX markers in source/ (finished code only)
MC004 no secret-looking string literals in source/ (long Base32 or hex
runs); real secrets never belong in code, test vectors live in test/.
A literal that is not a secret (for example the Base32 alphabet)
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
Usage: lint_project.py [ROOT]
Exit: 0 clean, 1 problems found (printed as file:line: CODE message).
"""
from __future__ import annotations
import re
import sys
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
DIAG_FILE = Path("source/util/Diag.mc")
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")
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"
@dataclass(frozen=True, order=True)
class Problem:
path: Path
line: int
code: str
message: str
def __str__(self) -> str:
return f"{self.path}:{self.line}: {self.code} {self.message}"
def strip_line_comment(line: str) -> str:
"""Drop a trailing // comment (good enough: the rules only look at code)."""
in_string = False
for i, ch in enumerate(line):
if ch == '"' and (i == 0 or line[i - 1] != "\\"):
in_string = not in_string
elif not in_string and line.startswith("//", i):
return line[:i]
return line
def check_mc(path: Path, rel: Path) -> Iterable[Problem]:
"""All Monkey C rules for one source or test file."""
lines = path.read_text(encoding="utf-8").splitlines()
first = next((line for line in lines if line.strip()), "")
if not first.lstrip().startswith("//"):
yield Problem(rel, 1, "MC001", "file must start with a // comment header that explains it")
in_source = rel.parts[0] == "source"
for number, line in enumerate(lines, start=1):
code = strip_line_comment(line)
if PRINTLN_RE.search(code) and in_source and rel != DIAG_FILE:
yield Problem(rel, number, "MC002", "System.println outside source/util/Diag.mc")
if in_source and MARKER_RE.search(line):
yield Problem(rel, number, "MC003", "unfinished-work marker in source/")
secret_like = BASE32_SECRET_RE.search(code) or HEX_SECRET_RE.search(code)
if in_source and secret_like and ALLOW_LITERAL not in line:
yield Problem(rel, number, "MC004", "secret-looking string literal in source/")
if COMMUNICATIONS_RE.search(code):
yield Problem(rel, number, "MC005", "network API (Toybox.Communications) is not allowed")
def check_manifest(path: Path, rel: Path) -> Iterable[Problem]:
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
if PERMISSION_RE.search(line):
yield Problem(rel, number, "MC005", "manifest must not request permissions")
def lint(root: Path) -> list[Problem]:
problems: list[Problem] = []
for folder in ("source", "test"):
for path in sorted((root / folder).rglob("*.mc")):
problems += check_mc(path, path.relative_to(root))
manifest = root / "manifest.xml"
if manifest.exists():
problems += check_manifest(manifest, manifest.relative_to(root))
else:
problems.append(Problem(Path("manifest.xml"), 1, "MC005", "manifest.xml is missing"))
return sorted(problems)
def main(argv: list[str]) -> int:
root = Path(argv[1] if len(argv) > 1 else ".").resolve()
problems = lint(root)
for problem in problems:
print(problem)
print(f"lint_project: {len(problems)} problems")
return 1 if problems else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
|