Files
o62lte-customising-plan/s5e-pressure-anim/tools/s5e_lua_static.py
T
2026-10-03 00:01:12 +08:00

146 lines
6.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.
# -*- 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'(?<![\w.:])([A-Za-z_]\w*)\s*\(', code):
head = code[max(0, m.start(1) - 12):m.start(1)]
if re.search(r'\bfunction\s+$', head):
continue # 定义,不是调用
out.append((m.group(1), m.start(1)))
return out
def main():
src = open(LUAC, encoding='utf-8').read()
code = strip(src)
print('=== tools/s5e_lua_static.py %s (%d B) ===' % (LUAC, len(src.encode('utf-8'))))
print('\n[1] 块配平')
def cnt(k):
return len(re.findall(r'(?<![\w.])' + k + r'(?![\w])', code))
do_standalone = cnt('do') - cnt('while') - cnt('for')
openers = cnt('function') + cnt('if') + cnt('while') + cnt('for') + do_standalone
ck(openers == cnt('end') and cnt('repeat') == cnt('until'),
'openers=%d end=%d(function=%d if=%d for=%d while=%d do=%d)repeat=%d until=%d'
% (openers, cnt('end'), cnt('function'), cnt('if'), cnt('for'), cnt('while'),
do_standalone, cnt('repeat'), cnt('until')))
print('\n[2] 先用后声明(真机上会变成 "call a nil value")')
d = decls(src, code)
early, unknown = [], []
for name, pos in calls(code):
if name in KEYWORDS or name in WHITELIST:
continue
if name in d:
if pos < d[name]:
early.append((name, lineno(src, pos), lineno(src, d[name])))
else:
unknown.append((name, lineno(src, pos)))
ck(not early, '没有"先调用后 local":%s'
% ('' if not early else ' / '.join('%s 调用@%d 声明@%d' % e for e in early)))
ck(not unknown, '没有未声明的被调用名:%s'
% ('' if not unknown else ' / '.join('%s@%d' % u for u in unknown)))
print('\n[3] 模板约定(s5e_verify.py 靠这些从 .face 里反解 Lua)')
for ph in REQUIRED_PH:
ck(src.count(ph) == 1, '占位符 %s 出现 %d 次(要 1)' % (ph, src.count(ph)))
ck(src.startswith('--[[--'), '文件以 --[[-- 开头')
ck(src.count('--[[') == 1, '--[[ 只出现 %d 次(要 1,否则 verify 会从中间截 Lua)'
% src.count('--[['))
ck(src.endswith('return ui\n'), '以 return ui\\n 结尾')
ck(src.count('return ui\n') == 1, 'return ui\\n 只出现 %d 次(要 1)' % src.count('return ui\n'))
ck('local UIIMG = {' in src, '有 local UIIMG = {')
print('\n[4] 与 s5e_verify.py 对齐的字面量')
ck(re.search(r"local ROLE = '__ROLE__'", src) is not None, "local ROLE = '__ROLE__'")
ck(re.search(r'local W, H = 480, 480', src) is not None, 'local W, H = 480, 480')
m = re.search(r'local DEVS = \{([^}]*)\}', src)
ck(m is not None and '/dev/health' in m.group(1),
'DEVS 里有 health -> /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())