#!/usr/bin/env python3 """Comments stay on one line and under eleven words.""" import re import sys from pathlib import Path root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[2] PATTERNS = [ "crates/**/*.rs", "apps/**/*.rs", "scripts/*.sh", "tests/**/*.sh", "tests/**/*.py", "packaging/**/*.sh", "packaging/**/*.nsi", "packaging/**/*.py", "packaging/**/*.swift", "tests/engines/*.c", "crates/**/*.c", "crates/**/*.h", "patches/**/*.patch", "tests/**/*.swift", "apps/*/web/src/**/*.js", "site/src/**/*.ts", "site/src/**/*.css", "site/test/**/*.ts", "site/video/**/*.py", "site/video/**/*.sh", ] LINE_COMMENT = { ".rs": re.compile(r"^\s*//[!/]?\s?(.*)$"), ".c": re.compile(r"^\s*(?://[!/]?\s?(.*)|/\*\s?(.*?)\s*\*/)$"), ".h": re.compile(r"^\s*(?://[!/]?\s?(.*)|/\*\s?(.*?)\s*\*/)$"), ".nsi": re.compile(r"^\s*;\s?(.*)$"), ".swift": re.compile(r"^\s*//\s?(.*)$"), } # Scanned by tokens: strings skipped, trailing comments found. SCANNED = {".js": True, ".ts": True, ".css": False} def scan(text, slashes): """Comments as (first line, last line, text); strings skipped.""" found, i, line, n = [], 0, 1, len(text) while i < n: c = text[i] if c in "\"'`": i += 1 while i < n and text[i] != c: if text[i] == "\\": i += 1 elif text[i] == "\n": line += 1 i += 1 elif text.startswith("/*", i): end = text.find("*/", i + 2) end = n if end < 0 else end body = text[i + 2:end] found.append((line, line + body.count("\n"), body.strip("* \n"))) line += body.count("\n") i = end + 1 elif slashes and text.startswith("//", i): end = text.find("\n", i) end = n if end < 0 else end found.append((line, line, text[i + 2:end].lstrip("/!").strip())) i = end - 1 elif c == "\n": line += 1 i += 1 return found def comment_re(path): if path.suffix == ".patch": return re.compile(r"^\+\s*(?://[!/]?\s?(.*)|/\*\s?(.*?)\s*\*/)$") return LINE_COMMENT.get(path.suffix, re.compile(r"^\s*#\s?(.*)$")) too_long = [] repeated = {} for pattern in PATTERNS: for path in sorted(root.glob(pattern)): if path.suffix in SCANNED: last = None for first, final, said in scan(path.read_text(), SCANNED[path.suffix]): if len(said.split()) > 10: too_long.append(f"{path.relative_to(root)}:{first}: {said[:60]}") if final > first or last == first - 1: repeated.setdefault(path, first) last = final continue comment = comment_re(path) previous = None lines = path.read_text().split("\n") if path.suffix == ".py": start = 1 if lines and lines[0].startswith("#!") else 0 if start < len(lines) and lines[start].startswith('"""'): doc = [] for line in lines[start:]: doc.append(line.strip('" ')) if line.count('"""') >= 2 or (len(doc) > 1 and '"""' in line): break lines = lines[:start] + [f"# {text}" for text in doc] + lines[start + len(doc):] for number, line in enumerate(lines, 1): if line.startswith("#!"): continue # A block comment left open spans lines. if path.suffix in (".c", ".h") and re.search(r"/\*(?!.*\*/)", line): repeated.setdefault(path, number) match = comment.match(line) said = match and next(group for group in match.groups() if group is not None) if match and len(said.split()) > 10: too_long.append(f"{path.relative_to(root)}:{number}: {said[:60]}") if match and previous == number - 1: repeated.setdefault(path, number) previous = number if match else None problems = too_long + [ f"{path.relative_to(root)}:{number}: comment runs past one line" for path, number in sorted(repeated.items()) ] if problems: sys.exit("FAIL: comments break the one-line ten-word rule:\n" + "\n".join(problems)) print("comments: every comment is one line and ten words or fewer")