86 lines
3.5 KiB
Python
86 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""audit_file_headers.py — 文件头机器闸(code-conventions §1.4.1)。
|
||
|
||
文件头 = .gd 文件顶部连续注释区:连续的空行或 # 注释行,首个非注释代码行止。
|
||
本脚本只实现机械可判子集(两类白名单的人判面见规范正文):
|
||
R0 载体:头注非空时首行必须以 `##` 文档注释开头(清偿批落地起生效);
|
||
R1 行数上限:头注注释行(非空)> 8 行;
|
||
R2 日期:头注出现日期模式(2026-09-12 / 2026年9月 一类)——日期只属于 git log 与迭代报告;
|
||
R3 变更叙事词:「迭代N / 本迭代」、批次叙事复合词(本批/拆件批/改造批……)、「迁移到/改名为」
|
||
一类变更经过。清单从紧防误伤:「结算批」等玩法机制词不在黑名单;
|
||
新失败模式先补本脚本规则再修码(static_scan 纪律)。
|
||
扫描域 = game/core + game/data + game/presentation(tests/smoke 豁免,同 audit_type_hints 口径)。
|
||
退出码:0=归零,1=有存量(逐条列 file:line)。用法:python tools/audit_file_headers.py
|
||
"""
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
SCAN_DIRS = ["game/core", "game/data", "game/presentation"]
|
||
|
||
MAX_HEADER_LINES = 8
|
||
|
||
DATE_PAT = re.compile(r"\d{4}\s*[-/.年]\s*\d{1,2}")
|
||
NARRATIVE_PATS = [
|
||
(re.compile(r"迭代\s*\d|本迭代"), "变更叙事「迭代N」"),
|
||
(re.compile(
|
||
r"本批|拆件批|改造批|重构批|实装批|清偿批|落地批|收口批|收尾批|转正批|对齐批|拆分批|换代批|升级批|随批"),
|
||
"变更叙事「XX批」"),
|
||
(re.compile(r"迁移到|迁至|改名为|更名为|原名为|曾用名"), "变更经过叙述"),
|
||
]
|
||
|
||
|
||
def header_lines(text):
|
||
"""返回文件头注释区的注释行 list[(lineno, line)],首个非注释非空行止;空行不计数。"""
|
||
rows = []
|
||
for lineno, line in enumerate(text.splitlines(), start=1):
|
||
stripped = line.lstrip("\ufeff").strip()
|
||
if stripped == "":
|
||
continue
|
||
if stripped.startswith("#"):
|
||
rows.append((lineno, line))
|
||
else:
|
||
break
|
||
return rows
|
||
|
||
|
||
def audit_path(path):
|
||
hits = []
|
||
text = path.read_text(encoding="utf-8", errors="replace")
|
||
rows = header_lines(text)
|
||
if rows and not rows[0][1].lstrip().startswith("##"):
|
||
hits.append((rows[0][0], "R0 头注必须以 ## 文档注释开头"))
|
||
if len(rows) > MAX_HEADER_LINES:
|
||
hits.append((rows[0][0], "R1 头注 %d 行 > 上限 %d 行(区间 %d-%d 行)"
|
||
% (len(rows), MAX_HEADER_LINES, rows[0][0], rows[-1][0])))
|
||
for lineno, line in rows:
|
||
if DATE_PAT.search(line):
|
||
hits.append((lineno, "R2 头注日期"))
|
||
for pat, label in NARRATIVE_PATS:
|
||
if pat.search(line):
|
||
hits.append((lineno, "R3 " + label))
|
||
return hits
|
||
|
||
|
||
def main():
|
||
violations = 0
|
||
for d in SCAN_DIRS:
|
||
base = ROOT / d
|
||
if not base.is_dir():
|
||
continue
|
||
for path in sorted(base.rglob("*.gd")):
|
||
for lineno, msg in audit_path(path):
|
||
print("%s:%d: %s" % (path.relative_to(ROOT), lineno, msg))
|
||
violations += 1
|
||
if violations:
|
||
print("[audit_file_headers] FAIL: 文件头存量 %d 处(存量按 code-conventions §1.4.1 白名单清偿)" % violations)
|
||
return 1
|
||
print("[audit_file_headers] ok: 头注 ≤8 行且零黑名单命中")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|