Files
gplan-2/tools/gdstyle_gate.py
T

179 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())