# -*- coding: utf-8 -*- r'''检查 .java / .py 文件的编码和换行符约定。 项目约定: .java / .py UTF-8 **无 BOM**、**LF** 换行 .ps1 UTF-8 **带 BOM**(PowerShell 5 读中文需要) .bat GBK + CRLF 用法: python _tests/check_encoding.py [文件或目录 ...] # 不给参数就查全仓库 ''' from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent BOM = b"\xef\xbb\xbf" # 默认检查范围:局域网这条线上**我们负责的文件**。 # # 为什么不全仓库默认: # 仓库里有十几处历史 CRLF 欠账(_build/ 的一次性脚本、 # _tests/java/*.java、几个早期测试)。它们和局域网无关, # 一次性归一会产生"整文件改写"的巨型 diff,反而淹没真正的改动。 # 所以默认只盯自己这条线,**新引入**的 CRLF 一律算失败; # 想看全仓库用 --all(历史欠账只提醒,不判失败)。 SCOPE = ( "windows/lanremote/discovery.py", "windows/lanremote/instance.py", "windows/lanremote/server.py", "android/src/com/lanremote/watch", "_tests/test_lan_detect.py", "_tests/test_lan_path.py", "_tests/test_lan_optimize.py", "_tests/test_watch_lan_switch.py", "_tests/test_watch_relayonly.py", "_tests/test_watch_skeleton.py", "_tests/bench_lan_vs_relay.py", "_tests/check_encoding.py", "_tests/verify_all.ps1", ) # 期望的编码/行尾规则(见文件头说明) RULES = { ".java": (False, False, "utf-8"), ".py": (False, False, "utf-8"), ".ps1": (True, False, "utf-8"), ".bat": (False, True, "gbk"), } SKIP_DIRS = {".git", "dist", "dist_exe", "build", "toolchain", "__pycache__", "site_shots", "downloads", "_apk_build", "out_java", "out_java_bench"} # 已经存在的行尾欠账(**不是**这次改出来的)。 # 这些文件早就是 CRLF 了,而且不在局域网这条线上。 # 一次性把它们归一会产生"整文件改写"的巨型 diff,有害无益; # 但也不能假装看不见 —— 所以单独列出来报成提醒, # 而**新引入**的 CRLF 一律算失败。 PREEXISTING_CRLF = { "windows/lanremote/actions.py", "windows/lanremote/diagnostics.py", "windows/lanremote/headless.py", "windows/lanremote/protocol.py", "_tests/test_features.py", "_tests/test_indicator_visible.py", "_tests/test_selfcheck.py", "_tests/test_undo.py", } def check_file(path: Path) -> tuple: """返回 (致命问题列表, 提醒列表)。""" rule = RULES.get(path.suffix.lower()) if rule is None: return [], [] want_bom, want_crlf, encoding = rule try: raw = path.read_bytes() except OSError as exc: return ["读不到: %s" % exc], [] fatal = [] note = [] try: rel = path.relative_to(ROOT).as_posix() except ValueError: rel = path.as_posix() known = rel in PREEXISTING_CRLF has_bom = raw.startswith(BOM) if has_bom != want_bom: message = ("BOM 应为%s,实际%s" % ("有" if want_bom else "无", "有" if has_bom else "无")) (note if known else fatal).append(message) body = raw[len(BOM):] if has_bom else raw crlf = body.count(b"\r\n") lone_cr = body.count(b"\r") - crlf if want_crlf and crlf == 0: (note if known else fatal).append("应为 CRLF 换行,实际是 LF") if not want_crlf and crlf: message = "有 %d 处 CRLF(应为 LF)" % crlf (note if known else fatal).append(message) if lone_cr: fatal.append("有 %d 处单独的 CR" % lone_cr) # 编码必须是声明的那种 try: body.decode(encoding) except (UnicodeDecodeError, LookupError) as exc: fatal.append("不是合法 %s: %s" % (encoding.upper(), exc)) return fatal, note def iter_targets(args: list) -> list: if not args: # 默认:只查 SCOPE 里的文件 return [p for p in _resolve(SCOPE) if p.is_file()] if args == ["--all"]: found = [] for path in ROOT.rglob("*"): if not path.is_file(): continue if any(part in SKIP_DIRS for part in path.parts): continue if path.suffix.lower() in RULES: found.append(path) return found return _resolve(args) def _resolve(items: list) -> list: out = [] for arg in items: path = Path(arg) if not path.is_absolute(): path = ROOT / path if path.is_dir(): out.extend(p for p in path.rglob("*") if p.is_file() and p.suffix.lower() in RULES) elif path.is_file(): out.append(path) return out def main() -> int: targets = iter_targets(sys.argv[1:]) bad = 0 noted = 0 for path in sorted(targets): fatal, note = check_file(path) try: shown = path.relative_to(ROOT) except ValueError: shown = path if fatal: bad += 1 print(" [FAIL] %s %s" % (shown, "; ".join(fatal))) if note: noted += 1 print(" [提醒] %s %s(早就这样,不属于本次改动)" % (shown, "; ".join(note))) print() if noted: print(" (%d 个文件有历史行尾欠账,已单独列出,不算失败)" % noted) if bad: print(" 检查 %d 个文件,%d 个不符合约定" % (len(targets), bad)) return 1 print(" 检查 %d 个文件,编码/换行全部符合约定" % len(targets)) return 0 if __name__ == "__main__": raise SystemExit(main())