删除 lib/includes/t.py、c.py,类型模型迁入 lib/core/TypeSystem.py;移除 IsTModule/_T_MODULE_NAMES 等名称判据,语法糖与内部标记统一为裸名;builtins 的 sizet/intN/uintN/fast/least/uchar/short/long 等改为编译器内建短名。
153 lines
7.8 KiB
Python
153 lines
7.8 KiB
Python
# -*- 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:]))
|