146 lines
6.2 KiB
Python
146 lines
6.2 KiB
Python
# -*- 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())
|