diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh index 58e84f52..d562289a 100644 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -13,6 +13,28 @@ fi fail=0 +# ── 扫描路径单点 + 零命中自检(迭代 29 T6)── +# 静默空转比红更危险:S3 曾把路径写成裸 `core`(仓根无此目录,实际是 game/core), +# 恒零命中恒绿、什么都不扫,直到本批才被发现。故每条规则的扫描路径必须是单点变量, +# 且规则自己先数一遍扫描面文件数——数为零一律判红(这样路径写歪时自检会跟着一起歪、一起红)。 +DIR_CORE="game/core" +DIR_PRES="game/presentation" + +scan_files() { # scan_files <目录> — 该目录下 .gd 文件数 + find "$1" -name '*.gd' 2>/dev/null | wc -l +} + +require_scan_face() { # require_scan_face <规则名> <目录> — 扫描面为空即判红 + local n + n=$(scan_files "$2") + if [ "$n" -eq 0 ]; then + echo "[static_scan] $1 FAIL: 扫描面为空——$2 下零个 .gd(规则路径写错导致的静默空转)" + fail=1 + return 1 + fi + return 0 +} + # S9 — 类型闸(迭代 27 T5——code-conventions §4 机器闸:产品码无类型容器/有值返回缺类型/参数缺类型) if python tools/audit_type_hints.py; then echo "[static_scan] S9 ok" @@ -33,22 +55,40 @@ fi # S1/S2 — 玩法代码禁 randomize()/自建 RandomNumberGenerator/全局随机函数(绕开 RngService 的口子) # 2026-09-24 首次实跑收敛(转正结构批):违规签名=裸 randomize/rand* 调用(实例方法 .randi() 排除) # + RngService 之外的 RandomNumberGenerator.new();接收服务注入流的类型标注不属违规(core 五文件 15 处实证) +if require_scan_face "S1/S2" "$DIR_CORE"; then hits=$(grep -rnE '\brandomize[[:space:]]*\(|(^|[^.[:alnum:]_])randi[[:space:]]*\(|(^|[^.[:alnum:]_])randf[[:space:]]*\(|(^|[^.[:alnum:]_])randf_range[[:space:]]*\(' \ - game/core game/presentation --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -v 'rng-seed-exempt' | wc -l) -hits2=$(grep -rn 'RandomNumberGenerator\.new()' game/core game/presentation --include='*.gd' 2>/dev/null | grep -vc rng_service) -if [ "$hits" -gt 0 ] || [ "$hits2" -gt 0 ]; then echo "[static_scan] S1/S2 FAIL: 全局随机/RNG 自建命中 $hits+$hits2 处"; grep -rnE '\brandomize[[:space:]]*\(|(^|[^.[:alnum:]_])randi[[:space:]]*\(|(^|[^.[:alnum:]_])randf[[:space:]]*\(|(^|[^.[:alnum:]_])randf_range[[:space:]]*\(' game/core game/presentation --include='*.gd' 2>/dev/null; grep -rn 'RandomNumberGenerator\.new()' game/core game/presentation --include='*.gd' 2>/dev/null | grep -v rng_service; fail=1; else echo "[static_scan] S1/S2 ok"; fi + "$DIR_CORE" "$DIR_PRES" --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -v 'rng-seed-exempt' | wc -l) +hits2=$(grep -rn 'RandomNumberGenerator\.new()' "$DIR_CORE" "$DIR_PRES" --include='*.gd' 2>/dev/null | grep -vc rng_service) +if [ "$hits" -gt 0 ] || [ "$hits2" -gt 0 ]; then echo "[static_scan] S1/S2 FAIL: 全局随机/RNG 自建命中 $hits+$hits2 处"; grep -rnE '\brandomize[[:space:]]*\(|(^|[^.[:alnum:]_])randi[[:space:]]*\(|(^|[^.[:alnum:]_])randf[[:space:]]*\(|(^|[^.[:alnum:]_])randf_range[[:space:]]*\(' "$DIR_CORE" "$DIR_PRES" --include='*.gd' 2>/dev/null; grep -rn 'RandomNumberGenerator\.new()' "$DIR_CORE" "$DIR_PRES" --include='*.gd' 2>/dev/null | grep -v rng_service; fail=1; else echo "[static_scan] S1/S2 ok"; fi +fi # S4 — core/ 禁 extends Control/Node2D、禁 .tscn preload(headless 债) -hits=$(grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' game/core --include='*.gd' 2>/dev/null | wc -l) -if [ "$hits" -gt 0 ]; then echo "[static_scan] S4 FAIL: core 引擎/UI 依赖命中 $hits 处"; grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' game/core --include='*.gd' 2>/dev/null; fail=1; else echo "[static_scan] S4 ok"; fi +if require_scan_face "S4" "$DIR_CORE"; then +hits=$(grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' "$DIR_CORE" --include='*.gd' 2>/dev/null | wc -l) +if [ "$hits" -gt 0 ]; then echo "[static_scan] S4 FAIL: core 引擎/UI 依赖命中 $hits 处"; grep -rnE 'extends[[:space:]]+(Control|Node2D)\b|\.tscn' "$DIR_CORE" --include='*.gd' 2>/dev/null; fail=1; else echo "[static_scan] S4 ok"; fi +fi # S3 — core/ 禁 ConfigDB 外 data 表加载(schema class_name 直引豁免) -hits=$(grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' core --include='*.gd' 2>/dev/null \ +# 路径修正(迭代 29 T6):原扫裸 `core`——仓根无此目录,恒零命中恒绿(静默空转);实际为 game/core。 +# 豁免:①注释行 ②game/core/config_db.gd 自身(数值单一事实源的正主,它的单点 load 是本规则的例外)。 +if require_scan_face "S3" "$DIR_CORE"; then +hits=$(grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' "$DIR_CORE" --include='*.gd' 2>/dev/null \ + | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' \ + | grep -v "$DIR_CORE/config_db.gd" \ | grep -v 'res://tests/' | wc -l) -if [ "$hits" -gt 0 ]; then echo "[static_scan] S3 WARNING(人工复核——class_name 直引误报需逐条排除): $hits 处"; grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' core --include='*.gd' 2>/dev/null; fi +if [ "$hits" -gt 0 ]; then echo "[static_scan] S3 WARNING(人工复核——class_name 直引误报需逐条排除): $hits 处"; grep -rnE '\b(load|preload)[[:space:]]*\(|ResourceLoader\.load' "$DIR_CORE" --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -v "$DIR_CORE/config_db.gd"; else echo "[static_scan] S3 ok(扫描面 $DIR_CORE 共 $(scan_files "$DIR_CORE") 个 .gd,非静默空转)"; fi +fi + +# S5 — core/ 禁裸引用 autoload 全局标识符(ConfigDB / RngService 直接写 = --script 模式 parse error) +# 形态:标识符后紧跟 `.`(成员访问)才算裸引用;排除注释行,并排除被引号/斜杠包裹的字符串字面量 +# (如 "[ConfigDB/%s]" 日志前缀、"/root/ConfigDB" 取节点路径)——那是文案与路径,不是标识符引用。 +if require_scan_face "S5" "$DIR_CORE"; then +hits=$(grep -rnE '(^|[^.[:alnum:]_"/])(ConfigDB|RngService)[[:space:]]*\.' "$DIR_CORE" --include='*.gd' 2>/dev/null \ + | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | wc -l) +if [ "$hits" -gt 0 ]; then echo "[static_scan] S5 FAIL: core 裸引用 autoload 全局标识符命中 $hits 处(--script 模式 parse error)"; grep -rnE '(^|[^.[:alnum:]_"/])(ConfigDB|RngService)[[:space:]]*\.' "$DIR_CORE" --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#'; fail=1; else echo "[static_scan] S5 ok(扫描面 $DIR_CORE 共 $(scan_files "$DIR_CORE") 个 .gd)"; fi +fi # TODO(正式工程落地后精化为扫描配置并升格为硬失败): -# S5 core/ 禁裸引用 ConfigDB/RngService 全局标识符(--script 模式 parse error;grep 近似误报多,需 AST 级或精确白名单) # S6 core↔core 信号连接只许方法 Callable(禁 lambda 捕获 self);发射者禁反向引用 # S7 B2/B3/B4 API 符号命中文件 ⊆ B1 文件集(编排唯一性) # S8 结算路径 await 零命中(B1 结算/编排文件集)