# -*- coding: utf-8 -*- """main.lua 的静态体检 —— 编译前把「真机上才会炸」的几类错抓出来。 S4e 那版的真机事故是:`segs/ddWrite/ddRead` 被放在 `slurp/sh` **之前**, 于是函数体里引用的 `sh` 不是那个 local,而是**全局 nil**,一调用就 `attempt to call a nil value (global 'sh')` —— 而且当时整条流水线全绿 (Compiler.exe 只打包 Lua、不解析它)。所以本文件专门查: ① 块配平(function/if/for/while/do/end/repeat/until) ② **先用后声明**:某个名字在文件里是 `local`,但第一次调用发生在它声明之前 ③ 调用了既不是 local、也不在 Lua 标准库白名单里的名字(拼写错 / 忘了 local) ④ 模板约定:4 个占位符各 1 次、`--[[` 只在文件头、以 `return ui\\n` 结尾 (tools/s5e_verify.py 会从 .face 里反解 Lua,这几条是它定位 Lua 的前提) 用法:python tools/s5e_lua_static.py [main.lua] """ import os, re, sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from paths import PROJ from lua_balance import strip LUAC = os.path.join(PROJ, 'main.lua') # Lua 5.4 标准库 + 本项目用到的宿主全局(都不是拼写错) WHITELIST = set(""" require pcall xpcall type pairs ipairs next select tostring tonumber rawget rawset rawequal rawlen setmetatable getmetatable error assert print unpack load string table math io os coroutine utf8 debug collectgarbage """.split()) KEYWORDS = set(""" and break do else elseif end false for function goto if in local nil not or repeat return then true until while """.split()) REQUIRED_PH = ('__UIIMG__', '__PAY__', '__RB__', '__ROLE__') fail = 0 def bad(msg): global fail fail += 1 print(' FAIL ' + msg) def ck(cond, msg): print((' OK ' if cond else ' FAIL ') + msg) if not cond: global fail fail += 1 def lineno(src, idx): return src.count('\n', 0, idx) + 1 def decls(src, code): """名字 -> 它在 code 里第一次被 local 声明(或作为函数参数出现)的位置。""" out = {} for m in re.finditer(r'\blocal\s+function\s+([A-Za-z_]\w*)', code): out.setdefault(m.group(1), m.start(1)) for m in re.finditer(r'\blocal\s+([A-Za-z_][\w,\s]*?)\s*(?:=|$)', code, re.M): base = m.start(1) for piece in m.group(1).split(','): nm = piece.strip() if re.fullmatch(r'[A-Za-z_]\w*', nm): out.setdefault(nm, base) # 函数参数也是 local(`function(a, b)` / `function f(a)`),否则会误报 for m in re.finditer(r'\bfunction\b\s*[A-Za-z_.:]*\s*\(([^)]*)\)', code): for piece in m.group(1).split(','): nm = piece.strip() if re.fullmatch(r'[A-Za-z_]\w*', nm): out.setdefault(nm, m.start()) return out def calls(code): """所有函数调用点(排除 `function NAME(` 这种定义)。""" out = [] for m in re.finditer(r'(? /dev/health:%s' % (m.group(1).strip() if m else None)) ck("local DEV = DEVS.health" in src, 'DEV 来自 DEVS.health(表里的键名真的被用于取节点,不是摆设)') paytbl = re.search(r'local PAYTBL = \{\n__PAY__\n__RB__\n\}', src) ck(paytbl is not None, 'PAYTBL 里只有 __PAY__ / __RB__ 两处占位符(没有硬编码节点名)') ck('466' not in src, '没有残留的 466(旧 S4e 尺寸)') print('\n=== %s ===' % ('全部通过' if not fail else '%d 项失败' % fail)) return 1 if fail else 0 if __name__ == '__main__': sys.exit(main())