From ca85343f170ad5b9379cfbe8e38ace8b664def4f Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 8 Nov 2026 19:48:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E5=B1=82=E5=BA=8F=E8=90=BD?= =?UTF-8?q?=E4=BD=8D=E5=8D=95=E5=8F=A3=E6=89=AB=E6=8F=8F=E9=9D=A2=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=C2=B7=E7=94=A8=E6=88=B7=E4=BB=A4=E7=9B=B4=E6=94=B9):?= =?UTF-8?q?=20source=5Fguard=5Ftest.gd=20=E7=9A=84=20V1d=20=E7=94=B1?= =?UTF-8?q?=E5=B9=B3=E9=93=BA=E5=88=97=E7=9B=AE=E5=BD=95=E6=94=B9=E9=80=92?= =?UTF-8?q?=E5=BD=92=EF=BC=8Cpresentation=20=E6=89=AB=E6=8F=8F=E9=9D=A2=20?= =?UTF-8?q?31=E2=86=9281=20=E4=B8=AA=20.gd=EF=BC=8Cviews/(47)=20=E4=B8=8E?= =?UTF-8?q?=20scenes/(3)=20=E5=85=A8=E6=95=B0=E7=BA=B3=E5=85=A5=EF=BC=9B?= =?UTF-8?q?=E5=88=A0=E3=80=8Cpresentation=20=E6=97=A0=E5=AD=90=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E3=80=8D=E6=97=A7=E6=B3=A8=E9=87=8A=EF=BC=9B=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20LAYER=5FREORDER=5FEXEMPT=20=E7=B2=BE=E7=A1=AE?= =?UTF-8?q?=E8=B1=81=E5=85=8D=E8=A1=A8(=E4=BB=85=E7=99=BB=E8=AE=B0=20views?= =?UTF-8?q?/ui=5Fkit.gd=20=E7=9A=84=20BCard.add=5Ffooter=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=86=85=E9=87=8D=E6=8E=92)=EF=BC=8C=E4=B8=A5?= =?UTF-8?q?=E6=A0=BC=E5=88=A4=E6=8D=AE=E4=BF=9D=E6=8C=81=E4=B8=8D=E5=8F=98?= =?UTF-8?q?=E3=80=81=E6=9C=AA=E7=99=BB=E8=AE=B0=E6=96=87=E4=BB=B6=E5=86=85?= =?UTF-8?q?=E5=87=BA=E7=8E=B0=20move=5Fchild=20=E4=B8=80=E5=BE=8B=E5=88=A4?= =?UTF-8?q?=E7=BA=A2=EF=BC=9B=E8=AE=A1=E6=95=B0=E5=89=8D=E5=85=88=E5=89=94?= =?UTF-8?q?=E9=99=A4=20remove=5Fchild(=20=E7=9A=84=E5=AD=97=E9=9D=A2?= =?UTF-8?q?=E9=87=8D=E5=90=88=EF=BC=8C=E9=81=BF=E5=85=8D=208=20=E5=A4=84?= =?UTF-8?q?=E5=88=A0=E5=AD=90=E8=8A=82=E7=82=B9=E8=B0=83=E7=94=A8=E8=AF=AF?= =?UTF-8?q?=E6=8A=A5=E3=80=82=E5=87=AD=E6=8D=AE:=20=E5=AD=A4=E7=AB=8B?= =?UTF-8?q?=E5=8D=95=E4=BB=B6=E7=9B=B4=E8=B7=91=20source=5Fguard=5Ftest=20?= =?UTF-8?q?8/8=20=E5=85=A8=E7=BB=BF(V1d:=20presentation=2081=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E3=80=81=E8=90=BD=E4=BD=8D=E5=8D=95=E5=8F=A3=E4=B8=8E?= =?UTF-8?q?=E7=99=BB=E8=AE=B0=E8=B1=81=E5=85=8D=E4=B9=8B=E5=A4=96=20move?= =?UTF-8?q?=5Fchild=3D0)=EF=BC=9B=E4=BB=A3=E7=A0=81=E5=9F=9F=E9=97=B8?= =?UTF-8?q?=E9=A6=96=E8=B7=91:=20=E5=86=92=E7=83=9F=E6=B5=8B=E8=AF=95/?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95/=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=B5=8B=E8=AF=95/=E5=9C=BA=E6=99=AF=E6=8E=A2=E9=92=88/?= =?UTF-8?q?=E9=9D=99=E6=80=81=E6=89=AB=E6=8F=8F/=E7=86=B5=E5=A2=9E?= =?UTF-8?q?=E9=97=B8=E5=85=A8=E7=BB=BF=EF=BC=8C=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=85=A8=E5=A5=97=E4=BB=B6=E7=9C=9F=E8=B7=91=E8=BF=87?= =?UTF-8?q?=E6=96=B0=E6=96=AD=E8=A8=80(run=5Fall=20PASS=20source=5Fguard?= =?UTF-8?q?=5Ftest=20exit=3D0)=EF=BC=9B=E6=B8=B2=E6=9F=93=E9=97=B8?= =?UTF-8?q?=E5=9B=A0=E6=9C=AC=E6=9C=BA=20python3=20=E7=BC=BA=20PIL=20?= =?UTF-8?q?=E5=81=87=E7=BA=A2(=E4=B8=8E=E6=94=B9=E5=8A=A8=E9=9D=A2?= =?UTF-8?q?=E6=97=A0=E5=85=B3=E2=80=94=E2=80=94=E6=9C=AC=E6=89=B9=20game/?= =?UTF-8?q?=20=E6=94=B9=E5=8A=A8=E5=85=A8=E5=9C=A8=E6=B5=8B=E9=87=8F?= =?UTF-8?q?=E9=9D=A2=EF=BC=8C=E6=8F=90=E4=BA=A4=E6=80=81=E6=8C=89=E5=BD=B1?= =?UTF-8?q?=E5=93=8D=E9=9D=A2=E9=80=89=E9=97=B8=E5=BA=94=20SKIP)=EF=BC=8C?= =?UTF-8?q?=E9=A6=96=E8=B7=91=E5=9B=9E=E6=89=A7=E5=9B=A0=E6=9C=AA=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=A0=87=20dirty=5Fgame=EF=BC=8C=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E5=90=8E=E9=87=8D=E8=B7=91=E5=8F=96=E5=B9=B2=E5=87=80=E5=9B=9E?= =?UTF-8?q?=E6=89=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/unit/contracts/source_guard_test.gd | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/game/tests/unit/contracts/source_guard_test.gd b/game/tests/unit/contracts/source_guard_test.gd index 394ab682..276cc2fc 100644 --- a/game/tests/unit/contracts/source_guard_test.gd +++ b/game/tests/unit/contracts/source_guard_test.gd @@ -10,6 +10,12 @@ extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") +# V1d 层序单口豁免(组件内部自身布局重排,非根容器层序):文件 → 允许 move_child 的函数名。 +# 登记面封闭——未登记的函数内出现 move_child 一律判红,新增文件默认受管。 +const LAYER_REORDER_EXEMPT: Dictionary = { + "res://presentation/views/ui_kit.gd": "add_footer", +} + func _init() -> void: # 套执行序沿源编排序(_split_tail 尾段调用序) @@ -68,23 +74,39 @@ func _smoke_hygiene_gates() -> void: var mv: String = FileAccess.get_file_as_string("res://presentation/main.gd") Kit.check("V8 GP 影子账残留零命中(main 源码面——文件已删+委托缝已拆)〔迭代27 T3〕", not mv.contains("gp_shadow_ledger") and not mv.contains("_gp_ledger")) - # V1d 层序落位单口:注册面建件后扫全 presentation——move_child 只允许出现在 ui_registry.gd 的 - # apply_layer_contract 函数体内(合同装配单口——出现第二处落位=硬编码层序,须改合同表而非加调用) + # V1d 层序落位单口:递归扫全 presentation(含 views/、scenes/)——move_child 只允许出现在 + # ui_registry.gd 的 apply_layer_contract 函数体内,或 LAYER_REORDER_EXEMPT 登记的组件内重排函数内 + # (合同装配单口——出现第二处落位=硬编码层序,须改合同表而非加调用) var scanned: int = 0 var outside_calls: int = 0 for fn_path: String in _list_scripts("res://presentation"): var src: String = FileAccess.get_file_as_string(fn_path) scanned += 1 - var fn_start: int = src.find("func apply_layer_contract") if fn_path.contains("ui_registry.gd") else -1 - var fn_end: int = src.find("\nfunc ", fn_start + 10) if fn_start >= 0 else -1 - if fn_end < 0: - fn_end = src.length() - outside_calls += (src.substr(0, fn_start) + src.substr(fn_end)).count("move_child(") - Kit.check("V1d 层序落位单口(presentation %d 文件——apply_layer_contract 之外 move_child 调用点=%d 应=0)〔注册面 V1〕" % [scanned, outside_calls], + outside_calls += _count_outside_allowed(src, fn_path) + Kit.check("V1d 层序落位单口(presentation %d 文件——落位单口与登记豁免之外 move_child 调用点=%d 应=0)〔注册面 V1〕" % [scanned, outside_calls], scanned > 0 and outside_calls == 0) -# 目录 .gd 清单(DirAccess 平铺——presentation 无子目录;扫描面>0 前置由调用侧断言承载) +# 允许段之外逐个文件数 move_child:遇任意函数声明行重判「当前是否在允许函数内」, +# 内部类方法与缩进形态都不影响定位;remove_child( 尾部与 move_child( 字面重合,先剔除再计数。 +static func _count_outside_allowed(src: String, fn_path: String) -> int: + var allowed: PackedStringArray = PackedStringArray() + if fn_path.ends_with("ui_registry.gd"): + allowed.append("apply_layer_contract") + if LAYER_REORDER_EXEMPT.has(fn_path): + allowed.append(String(LAYER_REORDER_EXEMPT[fn_path])) + var in_allowed: bool = false + var calls: int = 0 + for line: String in src.split("\n"): + var t: String = line.strip_edges() + if t.begins_with("func "): + in_allowed = allowed.has(t.substr(5).split("(")[0].strip_edges()) + if not in_allowed: + calls += line.replace("remove_child(", "").count("move_child(") + return calls + + +# 目录 .gd 清单(DirAccess 递归——presentation 下 views/、scenes/ 等子目录同样在册;扫描面>0 前置由调用侧断言承载) static func _list_scripts(dir_path: String) -> PackedStringArray: var out: PackedStringArray = PackedStringArray() var dir := DirAccess.open(dir_path) @@ -93,7 +115,9 @@ static func _list_scripts(dir_path: String) -> PackedStringArray: dir.list_dir_begin() var fname := dir.get_next() while fname != "": - if fname.ends_with(".gd"): + if dir.current_is_dir() and not fname.begins_with("."): + out.append_array(_list_scripts(dir_path + "/" + fname)) + elif fname.ends_with(".gd"): out.append(dir_path + "/" + fname) fname = dir.get_next() dir.list_dir_end()