179 lines
8.2 KiB
Python
179 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""gdstyle_gate.py — S13 第三方质量规则闸件(game/tests/static_scan.sh 调用)
|
||
|
||
跑 gdstyle(github.com/atelico/gdstyle,Rust 单二进制,版本钉 v0.4.0)质量规则扫描,
|
||
与存量基线 tools/gdstyle_baseline.txt 对账。规则口径见 tools/gdstyle.toml 头注(只开
|
||
quality/*+syntax/reserved-identifier,format/naming/order 全关)。扫描面=产品码三目录
|
||
(game/core、game/data、game/presentation,与 S9 类型闸同口径——probe/sim/tests 测量面
|
||
直构惯例不在面)。
|
||
|
||
基线键=稳定身份键(消息里的行数/分支数/变量数等计数漂移不再换键,存量债改小不假红):
|
||
函数级带数条目 键=文件|规则|函数名(函数名取自消息 function 'x' 段);
|
||
类级/文件级带数条目(class variables/public methods/file length)
|
||
键=文件|规则;
|
||
无数字条目 键=文件|规则|消息全文(消息本身稳定,保留第三段避免同文件同规则多条互吞)。
|
||
带数条目行格式「键 = 数值」,数值=消息剥掉 (max N) 段后首个整数;无数字条目裸键无数值。
|
||
判定:①键不在基线=新违规(NEW)红;②键在基线且双方有数值、当前数值 > 基线登记值=
|
||
恶化(WORSENED)红——独立于差集,防稳定键吞掉 104→300 行式恶化;③其余绿(改小/持平不红)。
|
||
逐条违规行带 FAIL 前缀——gate 09 的存量红指纹只取含 FAIL 的行,带前缀才能让「红项清单」
|
||
(哪几条、恶化到多少)进指纹:否则存量红期间的任何新增/进一步恶化都算同款红被放行。
|
||
|
||
阈值口径=gdstyle 默认值(tools/gdstyle.toml 不设数值阈值);基线只是欠账簿,登记值只准因
|
||
还债下降,禁抬高到现状(抬高=把既有代码当尺子,默认恶化)。
|
||
|
||
基线维护=人工手动编辑基线文件(本闸无任何重写基线的入口——防清基线换绿;增删行与改登记值
|
||
都是人工动作,须用户批准;登记值只准因还债下降,禁抬高到现状)。
|
||
|
||
二进制:本机固定路径 D:/code/gdstyle/gdstyle.exe(不入库不入包),GDSTYLE_BIN 环境变量覆盖;
|
||
版本与钉定版不符即红——升级工具须重测语法兼容性并人工修订基线。
|
||
|
||
退出码:0=无基线外新违规且无恶化;1=有新违规/有恶化/二进制缺失或版本不符/扫描面为空/
|
||
输出不可解析;2=用法错。
|
||
用法:python tools/gdstyle_gate.py # 闸判定
|
||
"""
|
||
import json
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
CONFIG = ROOT / "tools" / "gdstyle.toml"
|
||
BASELINE = ROOT / "tools" / "gdstyle_baseline.txt"
|
||
SCAN_DIRS = ["game/core", "game/data", "game/presentation"]
|
||
PINNED_VERSION = "0.4.0"
|
||
# 二进制定位:GDSTYLE_BIN 环境变量优先;Windows 本机固定路径保底,macOS/Linux 走 /usr/local/bin
|
||
# (mac_setup.sh 自举件装钉定版 v0.4.0 darwin 包至此,远端 CI 与本机同判据)。
|
||
DEFAULT_BIN = ("D:/code/gdstyle/gdstyle.exe" if sys.platform == "win32"
|
||
else "/usr/local/bin/gdstyle")
|
||
MAX_REPORT = 50
|
||
|
||
_FUNC_RE = re.compile(r"function '([^']+)'")
|
||
_MAX_RE = re.compile(r"\s*\(max \d+\)")
|
||
_INT_RE = re.compile(r"\d+")
|
||
|
||
|
||
def fail(msg: str) -> int:
|
||
print("[gdstyle_gate] FAIL — %s" % msg)
|
||
return 1
|
||
|
||
|
||
def build_entry(finding: dict):
|
||
"""finding dict -> (key, value|None)。数值=消息剥掉 (max N) 段后首个整数。"""
|
||
file_path = str(finding.get("file", "")).replace("\\", "/")
|
||
rule = str(finding.get("rule", ""))
|
||
msg = str(finding.get("message", ""))
|
||
m = _INT_RE.search(_MAX_RE.sub("", msg))
|
||
value = int(m.group()) if m else None
|
||
fm = _FUNC_RE.search(msg)
|
||
if fm:
|
||
key = "%s|%s|%s" % (file_path, rule, fm.group(1))
|
||
elif value is not None:
|
||
key = "%s|%s" % (file_path, rule)
|
||
else:
|
||
key = "%s|%s|%s" % (file_path, rule, msg)
|
||
return key, value
|
||
|
||
|
||
def collect_entries(findings) -> dict:
|
||
"""扫描结果 -> {键: 数值|None};同键多条取较大数值(恶化侧保守)。"""
|
||
entries = {}
|
||
for f in findings:
|
||
if not isinstance(f, dict) or "rule" not in f:
|
||
continue
|
||
key, value = build_entry(f)
|
||
if key in entries and entries[key] is not None and value is not None:
|
||
value = max(entries[key], value)
|
||
entries[key] = value
|
||
return entries
|
||
|
||
|
||
def parse_baseline() -> dict:
|
||
"""基线文件 -> {键: 数值|None}。行「键 = 数字」解析为带数条目,其余整行为裸键。"""
|
||
entries = {}
|
||
for line in BASELINE.read_text(encoding="utf-8").splitlines():
|
||
s = line.strip()
|
||
if not s or s.startswith("#"):
|
||
continue
|
||
key, value = s, None
|
||
if " = " in s:
|
||
k, _, v = s.rpartition(" = ")
|
||
if v.isdigit():
|
||
key, value = k, int(v)
|
||
entries[key] = value
|
||
return entries
|
||
|
||
|
||
def run_gate() -> int:
|
||
if hasattr(sys.stdout, "reconfigure"):
|
||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||
|
||
args = sys.argv[1:]
|
||
if args:
|
||
print("[gdstyle_gate] 用法: python tools/gdstyle_gate.py(无参数;基线维护=人工手动编辑基线文件)")
|
||
return 2
|
||
|
||
binary = os.environ.get("GDSTYLE_BIN", DEFAULT_BIN)
|
||
try:
|
||
ver = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=30)
|
||
except (FileNotFoundError, OSError):
|
||
return fail("gdstyle 二进制不可用:%s(本机固定路径,GDSTYLE_BIN 可覆盖;"
|
||
"v%s Windows 版见 github.com/atelico/gdstyle releases)" % (binary, PINNED_VERSION))
|
||
if PINNED_VERSION not in (ver.stdout or ""):
|
||
return fail("gdstyle 版本不符:钉定 v%s,实得 %r——升级工具须重测语法兼容性并人工重生成基线"
|
||
% (PINNED_VERSION, (ver.stdout or "").strip()))
|
||
|
||
face = [p for d in SCAN_DIRS for p in Path(ROOT, d).rglob("*.gd")]
|
||
if not face:
|
||
return fail("扫描面为空——%s 下零个 .gd(路径写错导致的静默空转)" % "、".join(SCAN_DIRS))
|
||
|
||
try:
|
||
proc = subprocess.run(
|
||
[binary, "check", "--format", "json", "--no-color", "-c", str(CONFIG)] + SCAN_DIRS,
|
||
capture_output=True, text=True, timeout=300, cwd=str(ROOT), encoding="utf-8",
|
||
)
|
||
except subprocess.TimeoutExpired:
|
||
return fail("gdstyle 扫描超时(300s)")
|
||
try:
|
||
findings = json.loads(proc.stdout or "[]")
|
||
except json.JSONDecodeError:
|
||
return fail("gdstyle 输出不可解析为 JSON(stderr 头部:%r)" % (proc.stderr or "")[:200])
|
||
|
||
current = collect_entries(findings)
|
||
|
||
base = parse_baseline() if BASELINE.exists() else {}
|
||
if not base and not BASELINE.exists():
|
||
return fail("基线文件不存在:%s(首次落位=人工创建基线文件、格式见文件头注,须经用户批准)" % BASELINE.name)
|
||
|
||
new = sorted(k for k in current if k not in base)
|
||
worsened = sorted(
|
||
k for k in current
|
||
if k in base and current[k] is not None and base[k] is not None and current[k] > base[k]
|
||
)
|
||
print("[gdstyle_gate] 扫描面 %d 个 .gd;命中 %d 条,基线内 %d 条,基线外新违规 %d 条,较登记值恶化 %d 条"
|
||
% (len(face), len(current), len(set(current) & set(base)), len(new), len(worsened)))
|
||
red = False
|
||
if new:
|
||
for k in new[:MAX_REPORT]:
|
||
print(" FAIL 新增 " + k)
|
||
if len(new) > MAX_REPORT:
|
||
print(" FAIL ...(截断,共 %d 条)" % len(new))
|
||
red = True
|
||
if worsened:
|
||
for k in worsened[:MAX_REPORT]:
|
||
print(" FAIL 恶化 %s = %d(基线登记 %d)" % (k, current[k], base[k]))
|
||
if len(worsened) > MAX_REPORT:
|
||
print(" FAIL ...(截断,共 %d 条)" % len(worsened))
|
||
red = True
|
||
if red:
|
||
return fail("基线外新违规 %d 条/恶化 %d 条——新码须修到基线口径内,恶化须还债或经用户批准随批人工修订基线"
|
||
% (len(new), len(worsened)))
|
||
print("[gdstyle_gate] ok")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(run_gate())
|