Files
TransPyC/Tools/AuditViperParseEntries.py
T
TermiNexus f19d25782f refactor(tpc): 退役 t/c 模块并将定宽类型升级为内建
删除 lib/includes/t.py、c.py,类型模型迁入 lib/core/TypeSystem.py;移除 IsTModule/_T_MODULE_NAMES 等名称判据,语法糖与内部标记统一为裸名;builtins 的 sizet/intN/uintN/fast/least/uchar/short/long 等改为编译器内建短名。
2026-10-18 13:00:58 +08:00

153 lines
7.8 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 -*-
"""只读审计:全仓"把源码交给 Python 解析器"的调用点是否都过了 Viper 软关键字层。
背景(为什么需要这个门禁)
Viper 前端 = TPC 自研 vast(`lib/vast`)+ **唯一展开点**
`lib/core/SymbolUtils.ExpandSoftKeywords`(`fn/fun/func/function` → `def`;
`enum X:` 块 → `class X(CEnum):`,两者均同行替换、行号不变)。
凡把 **Viper 源码文本**交给 `ast.parse` / `vast.parse` 的入口都必须先过这一层;
漏过者的失败形态是**静默降级**,不是报错:`includes/ast/base.vp:168` 的
`enum ASTFlag:` 曾让整个 base.vp 的声明(含顶层 `_init_ast`)一个都不注册,
下游只报 `Undefined function: '_init_ast'`(浪费一轮排查,见
`Debugger/enum-semantics.md` §13)。判据 = `Grep "ast\\.parse\\("` 逐个确认
上游是否已有展开层,或该输入**必然无糖**。
分类(逐调用点,见 `Classify`)
PASS 首实参文本含 `ExpandSoftKeywords`(同一表达式内已展开)
EVAL `mode='eval'`:注解 / 表达式的**片段**解析,不是整份源码 ⇒ 无需
EXEMPT 命中下方冻结豁免表:输入必然是 `.vpi`/`.pyi` 无糖产物,或同一函数
上游(紧邻几行)已展开 ⇒ 已人工审计确认
SUSPECT 其余 ⇒ **需人工判定**;`--strict` 下 exit 1(门禁失败)
覆盖范围:仓库内全部 `*.py`(含根目录脚本、`MCP/**`、`TransPyC/Tools/**`),
跳过 `.git`/`__pycache__`/`output`/`temp`/`__vpcbuild__`/`dlls` 与解析器自身
(`lib/vast`、`lib/vast_c` 是前端**实现**,不是前端**消费者**)。
VPC 侧源码(`ViperC/**` 的 `.vp`)跑自己的前端,不在本脚本范围内。
用法(**只读,不写任何文件**)
python TransPyC/Tools/AuditViperParseEntries.py # 打印审计表
python TransPyC/Tools/AuditViperParseEntries.py --strict # 有 SUSPECT 则 exit 1
"""
import ast as pyast
import os
import sys
ScriptDir = os.path.dirname(os.path.abspath(__file__))
RepoRoot = os.path.dirname(os.path.dirname(ScriptDir))
SkipDirs = {'__pycache__', '.git', '__vpcbuild__', 'dlls', 'output', 'temp', 'node_modules'}
# 前端实现(vast 解析器本体;它自己就是"唯一许可入口"的实现层)
SkipRelPrefixes = ('TransPyC/lib/vast/', 'TransPyC/lib/vast_c/')
# ---- 冻结豁免表:(仓库相对路径, 首实参文本归一化) -> 豁免理由 ------------------
# 新增合法调用点时必须在此登记原因;否则 --strict 会把新调用点当作 SUSPECT 拦下。
# 表来源:本轮全仓穷举审计(见最终报告 B 节审计表)。
Exempt = {
('TransPyC/lib/Projectrans/Phase2Translator.py', 'f.read()'):
'.vpi/.pyi 无糖产物(_ScanPyiFiles / _PyiCacheWorkerInitializer 仅扫 .vpi/.pyi)',
('TransPyC/lib/Projectrans/Phase2Translator.py', 'sig_content'):
'sig_files 全部来自 temp/*.vpi|*.pyi(Phase2Translator._LoadSha1Map:759)',
('TransPyC/lib/Projectrans/DeclarationGenerator.py', 'pyi_content'):
'generate(pyi_content, ...) 的入参来自 Phase1Generator 读 temp/{sha1}.vpi',
('TransPyC/lib/StubGen/Converter.py', 'PyContent'):
'convert() 首行 PyContent = ExpandSoftKeywords(PyContent)(Converter.py:68)',
('TransPyC/lib/Shell/TransPyC.py', 'content'):
'同函数 :100 content = ExpandSoftKeywords(content)',
('TransPyC/lib/Shell/TransPyC.py', 'Content'):
'同函数 :445 Content = ExpandSoftKeywords(Content)',
('TransPyC/lib/Shell/TransPyC.py', 'self.code'):
'Convert() :522 self.code = ExpandSoftKeywords(self.code)',
('TransPyC/lib/core/Translator/PythonParser.py', 'content'):
'ParsePythonFile() :76 content = ExpandSoftKeywords(content)',
('TransPyC/lib/core/Translator/AnnotationLoader.py', 'ann_content'):
'三处调用点各自紧邻上一行 ann_content = ExpandSoftKeywords(ann_content)',
('TransPyC/lib/core/SymbolExtractor.py', 'code'):
'extract(FilePath) 的 FilePath 全部来自 LoadModuleSymbols → .vpi/.pyi',
('TransPyC/lib/core/Handles/HandlesImports.py', '_fh0.read()'):
'.vpi(_TryLoadClassMembersFromPyi guard 分支补解析 class_parents)',
('TransPyC/lib/core/Handles/HandlesImports.py', 'f.read()'):
'.vpi(_TryLoadClassMembersFromPyi 主路径 _pyi_cache 填充)',
('TransPyC/lib/core/Handles/HandlesExprCall.py', 'pyi_code'):
'.vpi(_vtable_pyi_cache 填充,路径固定 temp/{sha1}.vpi)',
}
def Classify(Rel, Arg, Mode):
"""→ (类别, 理由)。"""
if 'ExpandSoftKeywords' in Arg:
return 'PASS', '首实参已过 ExpandSoftKeywords'
if Mode.strip('\'"') == 'eval':
return 'EVAL', "mode='eval'(注解/表达式片段,非整份源码)"
Hit = Exempt.get((Rel, Arg))
if Hit:
return 'EXEMPT', Hit
return 'SUSPECT', '未过展开层且不在冻结豁免表中(需人工判定该输入是否必然无糖)'
def IterCallSites():
"""遍历仓库 .py,产出 (rel, line, arg, mode);无法按 CPython 解析的文件记入 Unparsed。"""
for Root, Dirs, Files in os.walk(RepoRoot):
Dirs[:] = sorted(d for d in Dirs if d not in SkipDirs)
for Name in sorted(Files):
if not Name.endswith('.py'):
continue
Path = os.path.join(Root, Name)
Rel = os.path.relpath(Path, RepoRoot).replace(os.sep, '/')
if Rel.startswith(SkipRelPrefixes):
continue
try:
with open(Path, encoding='utf-8') as Fh:
Src = Fh.read()
Tree = pyast.parse(Src, Rel)
except Exception as Err:
yield ('<unparsed>', 0, Rel, type(Err).__name__)
continue
for Node in pyast.walk(Tree):
if not isinstance(Node, pyast.Call):
continue
Func = Node.func
if not isinstance(Func, pyast.Attribute) or Func.attr != 'parse':
continue
if not (isinstance(Func.value, pyast.Name) and Func.value.id in ('ast', 'vast')):
continue
Arg = pyast.get_source_segment(Src, Node.args[0]) if Node.args else ''
Mode = ''
for Kw in Node.keywords:
if Kw.arg == 'mode':
Mode = pyast.get_source_segment(Src, Kw.value) or ''
yield ('site', Node.lineno, Rel, ' '.join((Arg or '').split()), ' '.join(Mode.split()))
def Main(Strict):
Rows = []
Unparsed = []
for Item in IterCallSites():
if Item[0] == '<unparsed>':
Unparsed.append((Item[2], Item[3]))
continue
_, Line, Rel, Arg, Mode = Item
Kind, Why = Classify(Rel, Arg, Mode)
Rows.append((Kind, Rel, Line, Arg, Why))
Order = {'SUSPECT': 0, 'PASS': 1, 'EVAL': 2, 'EXEMPT': 3}
Rows.sort(key=lambda R: (Order[R[0]], R[1], R[2]))
Count = {}
for Kind, _, _, _, _ in Rows:
Count[Kind] = Count.get(Kind, 0) + 1
for Kind, Rel, Line, Arg, Why in Rows:
print('%-8s %s:%d arg=%s — %s' % (Kind, Rel, Line, (Arg or '')[:60], Why))
print('-' * 100)
print('sites=%d PASS=%d EVAL=%d EXEMPT=%d SUSPECT=%d unparsed=%d' % (
len(Rows), Count.get('PASS', 0), Count.get('EVAL', 0),
Count.get('EXEMPT', 0), Count.get('SUSPECT', 0), len(Unparsed)))
if Unparsed:
print('无法按 CPython 解析(未审计): %s' % Unparsed)
if Strict and Count.get('SUSPECT'):
sys.stderr.write('[strict] 门禁失败: 有 %d 个未过展开层且未登记的解析入口\n'
% Count['SUSPECT'])
return 1
return 0
if __name__ == '__main__':
sys.exit(Main('--strict' in sys.argv[1:]))