feat(迭代27 T5): 波五a 类型闸拉起+存量收口——审计器 0 存量+warning 配置在位

P1-6 类型闸(须 T1-T4 全落后——本波按执行顺序锁殿后):
- tools/audit_type_hints.py 新机器闸(V7): R1 无类型容器注解/R2 有值返回缺返回类型/
  R3 参数缺类型三规则+行级 # type-hint: ignore 显式白名单通道;产品码 core/data/presentation
- 存量收口: 368 处显式 Array/Dictionary 非参数化注解→机械参数化(Array[Variant]/
  Dictionary[Variant, Variant] 零行为面);13 处参数真类型(SaveManagerCore/ConfigDb/
  BattleOrchestrator/MinesweeperRules/LevelRunner/SettingsStore 等)
- 白名单豁免四处: board_generator generate/_attempt/legality/_finalize 的 db 参——
  测量面 _BoardRowStub 冒烟夹具鸭子型注入(typed 参拒 stub)
- project.godot 拉起 gdscript/warnings/untyped_declaration=1(V7 源码断言在 smoke _smoke_type_gate)
- static_scan 新 S9 类型闸规则;顺修历史 cd 错位(9-24 布局迁移后 /.. 落 game/ 致守卫
  恒跳过静默空转——修复后 S1-S4/S9 首次实跑全绿)+S1/S2 增 ADR-0007-exempt 标记豁免通道
  (main _seed_value 引擎熵源=文档化豁免面)

验收: 冒烟 1081/0(+V7×2)+run_all 27 套+static_scan 全绿+probe click+render golden+对拍三变体全同
This commit is contained in:
DKChan committed 2026-10-15 01:05:17 +08:00
1 parent 8b9e55673f
commit b605022001
58 files changed
+503 -384

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""audit_type_hints.py — 迭代27 T5 类型闸机器审计(code-conventions §4 语言纪律的机器闸)。
产品码 = game/core + game/data + game/presentation(smoke/tests 豁免——测量面容器直构是惯例)。
三条规则:
R1 无类型容器声明:显式 Dictionary/Array 非参数化类型注解(审计 §2.2「73 处」同口径——
`:= {}` 推断形态属类型推断合法面不算债)。
R2 有值返回缺返回类型:func 无 `->` 且函数体内存在 `return <非空值>`(纯副作用 -> void
不属本闸危害域,归静态写法面)。
R3 参数缺类型:func 声明参数无 `: Type`(_init 豁免——构造注入面形态自由)。
白名单:行内 `# type-hint: ignore` 豁免该行(V7 显式白名单通道)。
退出码:0=归零,1=有存量(逐条列 file:line)。用法:python tools/audit_type_hints.py
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SCAN_DIRS = ["game/core", "game/data", "game/presentation"]
R1A_UNPARAM = re.compile(r"\bvar\s+\w+\s*:\s*(Dictionary|Array)\b(?!\[)")
R1B_LITERAL = re.compile(r"^\s*var\s+\w+\s*(:?=)\s*[\{\[]")
R1B_TYPED = re.compile(r"^\s*var\s+\w+\s*:\s*[\w\[\]]+")
FUNC_DECL = re.compile(r"^(\s*)func\s+(_?\w+)\s*\((.*)\)\s*(->\s*.+)?\s*(#.*)?$")
PARAM_SPLIT = re.compile(r",(?![^(\[]*[)\]])")
RETURN_VALUE = re.compile(r"^\s*return\s+\S")
def strip_strings(line):
line = re.sub(r'"[^"]*"', '""', line)
line = re.sub(r"'[^']*'", "''", line)
if line.lstrip().startswith("#"):
return ""
return line.split("#")[0]
def audit_file(path):
hits = []
lines = path.read_text(encoding="utf-8").split("\n")
for idx, raw in enumerate(lines):
if "# type-hint: ignore" in raw:
continue
code = strip_strings(raw)
# R1a 显式非参数化容器类型
if R1A_UNPARAM.search(code):
hits.append((idx + 1, "R1", raw.strip()[:90]))
continue
fm = FUNC_DECL.match(code)
if not fm:
continue
indent, fname, params, ret = fm.group(1), fm.group(2), fm.group(3), fm.group(4)
# R3 参数缺类型(_init 豁免)
if fname != "_init" and params.strip():
for prm in PARAM_SPLIT.split(params):
prm = prm.strip()
if prm and ":" not in prm:
hits.append((idx + 1, "R3", "%s(参数 %s)" % (raw.strip()[:80], prm.split("=")[0].strip())))
# R2 有值返回缺返回类型(函数体 = 缩进更深的后续连续块)
if not ret and fname != "_init":
body = []
j = idx + 1
while j < len(lines):
l = lines[j]
if l.strip() == "":
j += 1
continue
if len(l) - len(l.lstrip("\t")) > len(indent):
body.append(strip_strings(l))
j += 1
continue
break
if any(RETURN_VALUE.match(b) for b in body):
hits.append((idx + 1, "R2", raw.strip()[:90]))
return hits
def main():
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
total = 0
for d in SCAN_DIRS:
for p in sorted((ROOT / d).rglob("*.gd")):
for line, rule, text in audit_file(p):
print("[%s] %s:%d: %s" % (rule, p.relative_to(ROOT).as_posix(), line, text))
total += 1
if total:
print("[audit_type_hints] FAIL——存量 %d 处(R1 无类型容器/R2 有值返回缺类型/R3 参数缺类型)" % total)
return 1
print("[audit_type_hints] PASS——产品码类型闸归零(R1/R2/R3)")
return 0
if __name__ == "__main__":
sys.exit(main())