增添 S5 支持
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# -*- 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())
|
||||
Reference in New Issue
Block a user