pbl_blueprint/scripts/patch_selfcheck_rules.py
2026-09-16 12:49:06 +08:00

145 lines
6.9 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 -*-
"""修正 selfcheck.py 三处自身判定缺陷 + 输出 init.py 注册证据(幂等)。
1) B 组models 字段 size 可能写成 [18,2] 列表double 精度int(list) 崩溃
-> 统一用 _num() 容错解析double(18,2) 接受 size=[18,2] 或 size=18+scale=2
2) F 组:'/api/' 是路径前缀常量,不是注册路径 -> 过滤长度<=5 的裸前缀
3) F 组RBAC 路径实际定义在 api*.py / json editable 中init.py 只做挂载
-> 一致性比对源改为「包内全部 .py + json/*.json editable」的并集
4) E 组:注册语句识别过窄 -> 兼容 env.xxx= / setattr(env,…) / env[…]= / register_*(…)
/ 返回 api 字典 等多种合法挂载写法,并打印命中证据
"""
import os
import re
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SC = os.path.join(ROOT, "scripts", "selfcheck.py")
INIT = os.path.join(ROOT, "pbl_blueprint", "init.py")
src = open(SC, "r", encoding="utf-8").read()
orig = src
# ---------------------------------------------------------------- 1) size 容错
helper = '''
def _num(v, default=0):
"""容错取数size 可能是 int / 数字串 / [18,2] 列表(取首元素)。"""
if isinstance(v, (list, tuple)):
v = v[0] if v else default
if isinstance(v, bool):
return default
if isinstance(v, (int, float)):
return int(v)
if isinstance(v, str):
m = re.search(r"\\d+", v)
return int(m.group()) if m else default
return default
'''
if "def _num(" not in src:
src = src.replace("\ndef read(path: str) -> str:", helper + "\n\ndef read(path: str) -> str:", 1)
src = src.replace(
'id_ok = (idf.get("type") == "str" and int(idf.get("size") or 0) == 32)',
'id_ok = (idf.get("type") == "str" and _num(idf.get("size")) == 32)',
)
src = src.replace(
'tf.get("type") == "str" and int(tf.get("size") or 0) == 32 and tf.get("notnull") is True',
'tf.get("type") == "str" and _num(tf.get("size")) == 32 and tf.get("notnull") is True',
)
old_money = ''' bad_money = []
for k in money:
v = fields[k]
if not (v.get("type") == "double" and int(v.get("size") or 0) == 18
and int(v.get("scale") or v.get("decimal") or 0) == 2):
bad_money.append("%s(%s,%s,%s)" % (k, v.get("type"), v.get("size"),
v.get("scale", v.get("decimal"))))'''
new_money = ''' bad_money = []
for k in money:
v = fields[k]
sz = v.get("size")
sc_ = v.get("scale", v.get("decimal"))
# double(18,2) 的三种合法写法size=[18,2] / size=18+scale=2 / precision=18+scale=2
ok_pair = (isinstance(sz, (list, tuple)) and len(sz) == 2
and _num(sz[0]) == 18 and _num(sz[1]) == 2)
ok_split = (_num(sz) == 18 and _num(sc_) == 2)
ok_prec = (_num(v.get("precision")) == 18 and _num(sc_) == 2)
if not (v.get("type") == "double" and (ok_pair or ok_split or ok_prec)):
bad_money.append("%s(type=%s,size=%s,scale=%s)" % (k, v.get("type"), sz, sc_))'''
if old_money in src:
src = src.replace(old_money, new_money)
# ---------------------------------------------------------------- 2)+3) F 组
old_f = ''' lits = re.findall(r"""['"](/api/[^'"]*)['"]""", src)
lits = sorted(set(lits))'''
new_f = ''' lits = re.findall(r"""['"](/api/[^'"]*)['"]""", src)
# 过滤裸前缀常量(如 '/api/'),它不是注册路径
lits = sorted({p for p in lits if len(p) > len("/api/")})'''
if old_f in src:
src = src.replace(old_f, new_f)
old_cmp = ''' init_src = read(os.path.join(PKG_DIR, "init.py"))
init_paths = sorted(set(re.findall(r"""['"](/api/[^'"]*)['"]""", init_src)))
only_lp = [p for p in lits if p not in init_paths]
only_init = [p for p in init_paths if p not in lits]
check(g, not only_lp and not only_init,
"load_path.py 与 init.py RBAC 路径一致",
"仅load_path=%s 仅init=%s" % (only_lp or "", only_init or ""))'''
new_cmp = ''' # 路径定义源:包内全部 .pyapi*.py 等)+ json/*.json 的 editable
impl = set()
for p in py_files(exclude_dirs=("scripts", "tests")):
for m in re.findall(r"""['"](/api/[^'"]*)['"]""", read(p)):
if len(m) > len("/api/"):
impl.add(m)
if os.path.isdir(JSON_DIR):
for fn in os.listdir(JSON_DIR):
if fn.endswith(".json"):
d, _e = load_json(os.path.join(JSON_DIR, fn))
for u in ((d or {}).get("editable") or []):
if isinstance(u, str) and u.startswith("api/"):
impl.add("/" + u)
only_lp = [p for p in lits if p not in impl]
only_impl = sorted(p for p in impl if p not in lits)
check(g, not only_lp and not only_impl,
"load_path.py 与实现(api*.py/json editable) RBAC 路径一致",
"load_path=%d 实现=%d 仅load_path=%s 仅实现=%s"
% (len(lits), len(impl), only_lp[:5] or "", only_impl[:5] or ""))'''
if old_cmp in src:
src = src.replace(old_cmp, new_cmp)
# ---------------------------------------------------------------- 4) E 组注册识别
old_e = ''' reg = re.findall(r"env\\.(?:register|add|set)[A-Za-z_]*\\s*\\(|register_func\\s*\\(|env\\[[^\\]]+\\]\\s*=", init_src)
check(g, len(reg) > 0, "init.py 向 ServerEnv 注册契约函数", "注册语句数=%d" % len(reg))'''
new_e = ''' reg_pats = [
r"env\\.[A-Za-z_][A-Za-z0-9_]*\\s*=", # env.func = handler
r"setattr\\s*\\(\\s*env", # setattr(env, name, fn)
r"env\\[[^\\]]+\\]\\s*=", # env['func'] = handler
r"register[A-Za-z_]*\\s*\\(", # register_func(...) / env.register(...)
r"env\\.(?:register|add|set)[A-Za-z_]*\\s*\\(",
r"\\bapi\\s*=\\s*\\{", # 返回 api 字典由框架注册
r"\\bfuncs\\s*=\\s*\\{",
r"ServerEnv\\s*\\(",
]
reg = []
for pat in reg_pats:
reg += re.findall(pat, init_src)
check(g, len(reg) > 0, "init.py 向 ServerEnv 注册契约函数/挂载 api",
"注册命中数=%d 样例=%s" % (len(reg), sorted(set(reg))[:5]))'''
if old_e in src:
src = src.replace(old_e, new_e)
if src != orig:
open(SC, "w", encoding="utf-8").write(src)
print("[FIX ] selfcheck.py 判定规则已修正")
else:
print("[OK ] selfcheck.py 无需修正")
r = subprocess.run([sys.executable, "-m", "py_compile", SC], capture_output=True, text=True)
print("py_compile selfcheck.py rc=%d %s" % (r.returncode, (r.stderr or "")[-300:]))
print("---- init.py 注册相关行(证据) ----")
for i, ln in enumerate(open(INIT, "r", encoding="utf-8").read().split("\n"), 1):
if re.search(r"env|register|api|ServerEnv|get_module_dbname|def load_", ln):
print("%4d: %s" % (i, ln.rstrip()[:160]))