fix(sizeof,parse): 尺寸链去静默兜底,解析报错可定位,补 C 解析器缺口

StructGen: _get_struct_size/_get_align 去 opaque→8/ptr_size、未知→4 兜底并补 VectorType;LiteralStructType.fields→.elements(llvmlite 无 fields,必然 AttributeError)

HandlesClassDef: union 载荷尺寸经不存在的 ir.DataLayout 静默退化成 8 字节;HandlesExprCall/ExprBuiltin: sizeof 各静默 0 分支改 fail-fast,数组元素 sizeof 统一走 _get_struct_size

HandlesFor: nonlocal struct/array 堆分配不再退化成 malloc(8)

vast/vast_c: 补一元 * / & 解析,装饰器后 extern/export 分派修复,C 侧报错带 token 与行号;StubGen.Converter: 宏展开丢尾随空行修 .pyi 缩进非法

其余为 rawptr 迁移 / fail-fast 口径等历史改动一并入库
This commit is contained in:
TermiNexus committed 2026-09-26 19:27:22 +08:00
1 parent 53a345a59a
commit c83fc752c6
53 files changed
+6238 -887

No files matched your search

+840
View File
@@ -0,0 +1,840 @@
# -*- coding: utf-8 -*-
"""作用域感知静态扫描: `rawptr[<族>.<X>]` 变量上访问 <X> 不存在的字段(fast-fail 潜在点)。
族(Family)= 一个"类名限定前缀 + 类定义来源"。默认族表见 BuildFamilies;
`rawptr[ast.X]` / `rawptr[llvmlite.X]` / `rawptr[GenTypes.X]` / `rawptr[HT.X]` /
`rawptr[memhub.X]` 均可解析。另加载 linkedlist.vp 作为**支撑族**(空前缀 ''),
供 `class ParamNode(GSListNode[ParamNode])` 这类**裸基类名**解析(否则 `.Next` 会误报)。
相对旧版的关键修正(旧版按“变量名全文件聚合 + 正则硬匹配”导致大量误报):
1) 作用域建模: 为 模块 / 函数(方法/嵌套函数) / lambda 建立独立作用域,
类体作用域按 Python 语义对方法“穿透”; 属性访问按“该访问点所在作用域链”找
**最近一次、且位置在访问点之前** 的声明(不是“全文件第一条”)。
2) 形参(含 self 与 rawptr[X] 形参)计入声明; 无注解形参视为“遮蔽”外层同名变量。
3) 重新赋值/重声明: AnnAssign / Assign(cast 形态) / 赋值继承(Name RHS) 都参与;
非指针赋值记录为“已解析但非指针”=> 不再继承更早的类。
4) 跳过非代码: 解析源码后, 注释/字符串/docstring 内容天然不产生 Attribute 节点
(如 "%s.__iter__" 之类的字符串与三引号注释块不再命中)。
5) 类成员表按族隔离: 基类先在**本族**找, 再找**支撑族**(''), 最后全局唯一名;
同一基类名跨族重复时报 AMBIG(宁可 UNRESOLVED 也不猜)。
6) `t.REnum`(tagged union)派生类: 变体字段合并进父类成员集
(TPV 允许在父类指针上访问变体字段, 不合并会产生大量误报)。
前端 = **TPC 自研 vast**(`lib/vast`,与 StubGen/Converter.py 用的 `import vast as ast`
是同一源文件; 不可用 CPython `ast`, 否则含 `list[X*]` 等 vast 专有写法的文件会整体
SyntaxError 被跳过)。vast 与 CPython ast 的差异及本脚本的适配:
A) `X*` / `X**` 被 vast 脱糖成 `X | t.CPtr`(`**` 加两层), 其中合成的
`t.CPtr` Attribute 节点**连 lineno 属性都没有**(编译器内部脱糖产物, 不是源码里的
访问; 全树实测 555 个, 全部是 ('t','CPtr'))=> 计入 Uses 时用
`getattr(Node,'lineno',None) is not None` 排除。
B) vast 的 SyntaxError 可能不带 lineno(C 路径)=> 从报文 `at line N` 兜底提取。
C) vast 的装饰器 `@t.NoVTable` 的 col_offset 是 `@` 的位置(CPython 是其后 1 列);
该 Attribute 的接收者 `t` 不是族指针, 判据不受影响。
D) 其余节点类名/属性名/walk/iter_child_nodes 与 CPython ast 同构(已逐节点对照)。
输出: 文件:行号 / 变量 / 推导出的类 / 字段 / 声明来源行 / 判定, 并统计
NOWHERE / OTHER / RESOLVED / UNRESOLVED, 并按族(ast/llvmlite/其它)分桶。
路径: 全部按**脚本自身位置**推导(ScriptDir = <RepoRoot>/TransPyC/Tools),
不依赖 cwd;可用 --app-root / --includes / --out 覆盖。
用法:
python TransPyC/Tools/ScanAstFieldMiss.py [--app-root DIR] [--includes DIR]
[--out FILE] [--verbose] [--strict]
python TransPyC/Tools/ScanAstFieldMiss.py --selftest [--includes DIR] [--out FILE]
默认值:
--app-root <RepoRoot>/TransPyV/App
--includes <RepoRoot>/includes
--out <RepoRoot>/Debugger/ScanAstFieldMiss.txt
退出码:
0 非 --strict 的正常扫描; 或 --strict 下 NOWHERE / OTHER / parsefail 三者全 0
1 --strict 下 有 NOWHERE/OTHER 命中, **或 parsefail > 0**
(parsefail 也算门禁失败: 文件被前端静默跳过 = 假绿, 见下)
2 参数错误
--selftest: 0 = 断言全通过; 1 = 有断言失败
--selftest 为什么必须有(历史教训):
上一版门禁的前端是 CPython `ast`, 对含 `list[X*]` / `rawptr[X]*` 等 vast 专有写法的
文件整体 SyntaxError 跳过 ⇒ 曾出现 "OTHER=0 全绿" 而工作树里实际有 **18 处**
`rawptr[llvmlite.Function].Ty` 真错(整批文件根本没被检查)。
故 --selftest 内置合成用例(生成到临时目录、跑完即删), 断言"**报出集合 == 预期集合**",
覆盖 4 类正例(含 vast 专有 `rawptr[X]*` / `rawptr[X]**`)与 4 类负例
(合法字段 / 跨函数同名各自声明 / docstring 与注释 / 返回值推断生效),
用来证明扫描器不是空转。
"""
import glob
import os
import re
import shutil
import sys
import tempfile
# ============================================================
# 0. 路径推导(全部按脚本自身位置, 不依赖 cwd)
# ScriptDir = <RepoRoot>/TransPyC/Tools
# TpcRoot = <RepoRoot>/TransPyC (TransPyC 仓库根)
# RepoRoot = <RepoRoot> (工作区根)
# ============================================================
ScriptDir = os.path.dirname(os.path.abspath(__file__))
TpcRoot = os.path.dirname(ScriptDir)
RepoRoot = os.path.dirname(TpcRoot)
DefaultAppRoot = os.path.join(RepoRoot, 'TransPyV', 'App')
DefaultIncludesDir = os.path.join(RepoRoot, 'includes')
DefaultOutFile = os.path.join(RepoRoot, 'Debugger', 'ScanAstFieldMiss.txt')
# ---- 前端: TPC 自研 vast(非 CPython ast)。需要 TransPyC 包根在 sys.path,
# lib._bootstrap 会再补齐 lib/ 与 lib/includes(使 `import vast` 可用)。
if TpcRoot not in sys.path:
sys.path.insert(0, TpcRoot)
from lib import vast as ast # noqa: E402
from lib.core.SymbolUtils import ExpandSoftKeywords # noqa: E402
SupportPrefix = ''
# ============================================================
# 1. 类成员表: 字段 / 方法 / 基类(按族隔离;每次扫描重建)
# ============================================================
FamilyTable = []
SupportFiles = []
FamilyNames = set()
ClassFields = {}
ClassMethods = {}
ClassBase = {}
ClassLoc = {} # 裸类名 -> [族, ...](用于跨族基类解析)
EnumClass = {} # (族, 类名) -> 1 表示 t.REnum/t.CEnum 派生(变体字段合并)
REnumClass = {} # (族, 类名) -> 1 表示 t.REnum 派生(额外有编译器内建 .kind())
MemberCache = {}
FamilyFields = {} # 族 -> 该族所有字段并集(OTHER/NOWHERE 判定用)
AmbiguousBases = set()
Uses = [] # [(AttrNode, Scope)];Walk 现场填充, 每个文件扫描前 clear
# ---- 返回注解表(同文件内 def f(...) -> rawptr[族.X]):供 x = f() 形态的类型推导。
# 仅补充覆盖(收紧),不改变任何既有判据:名字在文件内返回注解不一致时报歧义并放弃。
FuncRet = {}
def ResetTables():
"""清空全部类表/缓存(供同一进程内多轮扫描复用, 如 --selftest)。"""
global FamilyTable, SupportFiles, FamilyNames
global ClassFields, ClassMethods, ClassBase, ClassLoc, EnumClass, REnumClass
global MemberCache, FamilyFields, AmbiguousBases
FamilyTable = []
SupportFiles = []
FamilyNames = set()
ClassFields = {}
ClassMethods = {}
ClassBase = {}
ClassLoc = {}
EnumClass = {}
REnumClass = {}
MemberCache = {}
FamilyFields = {}
AmbiguousBases = set()
FuncRet.clear()
def AddClass(Fam, Name, Bases):
ClassFields.setdefault((Fam, Name), set())
ClassMethods.setdefault((Fam, Name), set())
ClassLoc.setdefault(Name, [])
if Fam not in ClassLoc[Name]:
ClassLoc[Name].append(Fam)
if Bases:
ClassBase.setdefault((Fam, Name), set()).update(Bases)
def BaseName(Node):
if isinstance(Node, ast.Name):
return Node.id
if isinstance(Node, ast.Attribute):
return Node.attr
if isinstance(Node, ast.Subscript): # GSListNode[ParamNode] -> GSListNode
return BaseName(Node.value)
return None
def CollectBody(Fam, Name, Body, Enum):
"""收集类体成员;Enum=True 时把嵌套变体类的字段并入本类(REnum 语义)。"""
for Item in Body:
if isinstance(Item, ast.AnnAssign) and isinstance(Item.target, ast.Name):
ClassFields[(Fam, Name)].add(Item.target.id)
elif isinstance(Item, (ast.FunctionDef, ast.AsyncFunctionDef)):
ClassMethods[(Fam, Name)].add(Item.name)
elif Enum and isinstance(Item, ast.ClassDef):
CollectBody(Fam, Name, Item.body, Enum) # 变体字段并入父类
def RegisterClass(Fam, Node):
Bases = set()
for B in Node.bases:
Nm = BaseName(B)
if Nm:
Bases.add(Nm)
AddClass(Fam, Node.name, Bases)
Enum = 1 if (Bases & {'REnum', 'CEnum'}) else 0
if Enum:
EnumClass[(Fam, Node.name)] = 1
if 'REnum' in Bases:
# REnum.kind() 是编译器内建(HandlesExprCall.py:3126 'kind' → __tag 读取)
REnumClass[(Fam, Node.name)] = 1
CollectBody(Fam, Node.name, Node.body, Enum)
def ParseSrc(Src, Path):
"""vast 前端解析入口(全脚本单一前端; 失败抛异常, 由调用方记账)。
`.vp` 源码须先过 Viper 软关键字层(`lib/core/SymbolUtils.ExpandSoftKeywords`:
`fn/fun/func/function` → `def`、`enum X:` → `class X(t.CEnum):`),与
`StubGen/Converter.py` / `Shell/TransPyC.py` 等既有前端入口同口径。
否则含 `enum` 的 `.vp`(如 `includes/ast/base.vp:168`)会在此整体 SyntaxError,
被 `LoadInclude` 的正则兜底或 `RunScan` 的 `parsefail` 记成"解析失败"——两者
都会**静默漏掉该文件的类/字段**(本脚本的假绿来源)。`.py` 无糖,展开为空操作
(`'enum' in Code` 门禁 + 行首锚定正则)。
"""
return ast.parse(ExpandSoftKeywords(Src), Path)
def ErrLine(E):
"""vast 的 SyntaxError 可能无 lineno(C 路径)=> 从报文 `at line N` 兜底提取。"""
L = getattr(E, 'lineno', None)
if L:
return L
M = re.search(r'at line (\d+)', str(E))
return int(M.group(1)) if M else -1
def LoadIncludeRegex(Src, Fam):
"""ast.parse 失败(docstring 含 \\xNN 等)时的回退:缩进感知的类/字段/方法收集。"""
Indent = {}
Enum = {}
for Ln in Src.splitlines():
M = re.match(r'(\s*)class (\w+)(?:\[[^\]]*\])?(?:\(([^)]*)\))?\s*:', Ln)
if M:
Depth = len(M.group(1))
Name = M.group(2)
Bases = set()
if M.group(3):
for B in M.group(3).split(','):
B = B.strip().split('.')[-1].split('[')[0].strip()
if B:
Bases.add(B)
AddClass(Fam, Name, Bases)
Indent[Name] = Depth
Enum[Name] = 1 if (Bases & {'REnum', 'CEnum'}) else 0
if Enum[Name]:
EnumClass[(Fam, Name)] = 1
continue
M2 = re.match(r'(\s+)(\w+)\s*:\s', Ln)
if M2 and Indent:
Depth = len(M2.group(1))
Owner = [N for N, D in Indent.items() if D < Depth]
if Owner:
P = max(Owner, key=lambda N: Indent[N])
ClassFields[(Fam, P)].add(M2.group(2))
continue
M3 = re.match(r'(\s+)def (\w+)\(', Ln)
if M3 and Indent:
Depth = len(M3.group(1))
Owner = [N for N, D in Indent.items() if D < Depth]
if Owner:
P = max(Owner, key=lambda N: Indent[N])
ClassMethods[(Fam, P)].add(M3.group(2))
def LoadInclude(Path, Fam):
try:
Src = open(Path, encoding='utf-8').read()
except Exception:
return
try:
Tree = ParseSrc(Src, Path)
except Exception: # noqa: BLE001 (vast 失败可能是 SyntaxError/ValueError)
LoadIncludeRegex(Src, Fam)
return
for Node in Tree.body:
if isinstance(Node, ast.ClassDef):
RegisterClass(Fam, Node)
def BuildFamilies(AppRoot, IncludesDir):
"""按 AppRoot/IncludesDir 建族表并加载类定义(含支撑族)。"""
global FamilyTable, SupportFiles, FamilyNames
FamilyTable = [
('ast', [os.path.join(IncludesDir, 'ast')]),
('llvmlite', [os.path.join(IncludesDir, 'llvmlite')]),
('GenTypes', [os.path.join(AppRoot, 'lib', 'core', 'Gen', 'GenTypes.vp')]),
('HT', [os.path.join(AppRoot, 'lib', 'core', 'Gen', 'Translator.vp')]),
('memhub', [os.path.join(IncludesDir, 'memhub.vp')]),
]
SupportFiles = [os.path.join(IncludesDir, 'linkedlist.vp')]
FamilyNames = set(P for P, _ in FamilyTable) | {''}
for Prefix, Paths in FamilyTable:
for P in Paths:
if os.path.isdir(P):
for F in sorted(glob.glob(os.path.join(P, '*.vp'))):
LoadInclude(F, Prefix)
else:
LoadInclude(P, Prefix)
for F in SupportFiles:
LoadInclude(F, SupportPrefix)
def ComputeFamilyFields():
global FamilyFields
FamilyFields = {}
for (_F, _N) in ClassFields:
FamilyFields.setdefault(_F, set()).update(ClassFields[(_F, _N)])
def FindClass(Name, Prefer):
"""裸类名 -> (族, 类名)。优先本族, 再支撑族, 最后全局唯一名。"""
if (Prefer, Name) in ClassFields:
return (Prefer, Name)
if (SupportPrefix, Name) in ClassFields:
return (SupportPrefix, Name)
Locs = ClassLoc.get(Name, [])
if len(Locs) == 1:
return (Locs[0], Name)
if len(Locs) > 1:
AmbiguousBases.add(Name)
return None
def Members(Fam, Name, Seen=None):
if Seen is None:
Seen = set()
Key = (Fam, Name)
if Key in Seen:
return set()
Seen.add(Key)
Out = set(ClassFields.get(Key, ())) | set(ClassMethods.get(Key, ()))
if Key in REnumClass:
Out.add('kind')
for B in ClassBase.get(Key, ()):
Loc = FindClass(B, Fam)
if Loc is not None:
if Loc in REnumClass:
Out.add('kind')
Out |= Members(Loc[0], Loc[1], Seen)
return Out
def Member(Fam, Name):
Key = (Fam, Name)
if Key not in MemberCache:
MemberCache[Key] = Members(Fam, Name)
return MemberCache[Key]
# ============================================================
# 2. 类型推导: 从注解 / cast 调用里取 (族, 类名)
# ============================================================
def AnnClass(Ann):
"""注解里若为 rawptr[<族>.<X>](可被 `| t.CPtr` 包裹)返回 (族, X), 否则 None。"""
if Ann is None:
return None
if isinstance(Ann, ast.BinOp) and isinstance(Ann.op, ast.BitOr):
return AnnClass(Ann.left) or AnnClass(Ann.right)
if isinstance(Ann, ast.Subscript):
if isinstance(Ann.value, ast.Name) and Ann.value.id == 'rawptr':
Sl = Ann.slice
if isinstance(Sl, ast.Attribute) and isinstance(Sl.value, ast.Name) \
and Sl.value.id in FamilyNames and Sl.value.id != '':
return (Sl.value.id, Sl.attr)
return None
def CastClass(Val):
"""`rawptr[<族>.<X>](...)` 形态的 cast 调用返回 (族, X), 否则 None。"""
if isinstance(Val, ast.Call) and isinstance(Val.func, ast.Subscript):
return AnnClass(Val.func)
return None
def CollectFuncRets(Tree):
FuncRet.clear()
Ambig = set()
for Node in ast.walk(Tree):
if isinstance(Node, (ast.FunctionDef, ast.AsyncFunctionDef)):
C = AnnClass(Node.returns)
if Node.name in FuncRet and FuncRet[Node.name] != C:
Ambig.add(Node.name)
else:
FuncRet[Node.name] = C
for N in Ambig:
FuncRet.pop(N, None)
# ============================================================
# 3. 作用域模型
# ============================================================
class Scope:
__slots__ = ('Kind', 'Parent', 'Decls')
def __init__(self, Kind, Parent):
self.Kind = Kind # 'module' / 'func' / 'class'
self.Parent = Parent
self.Decls = {} # name -> [(Lineno, Col, Cls, DeclLine)]
def Add(self, Name, Lineno, Col, Cls):
self.Decls.setdefault(Name, []).append((Lineno, Col, Cls, Lineno))
def Resolve(Sc, Name, Lineno, Col):
"""沿作用域链找 Name 在 (Lineno, Col) 之前最近一次的声明。
返回 (Cls, DeclLine); Cls 为 None 表示“已解析但非族指针”或“未找到”。
类体作用域对嵌套函数穿透(Python 语义)。
"""
Cur = Sc
First = True
while Cur is not None:
if Cur.Kind != 'class' or First:
if Name in Cur.Decls:
Best = None
for D in Cur.Decls[Name]:
if (D[0], D[1]) <= (Lineno, Col):
if Best is None or (D[0], D[1]) > (Best[0], Best[1]):
Best = D
if Best is not None:
return Best[2], Best[3]
First = False
Cur = Cur.Parent
return None, None
def Walk(Node, Sc):
if Node is None:
return
if isinstance(Node, (ast.FunctionDef, ast.AsyncFunctionDef)):
for D in Node.decorator_list:
Walk(D, Sc)
A = Node.args
Formals = list(A.posonlyargs) + list(A.args) + list(A.kwonlyargs)
for P in Formals:
Walk(P.annotation, Sc)
for D in A.defaults:
Walk(D, Sc)
for D in A.kw_defaults:
Walk(D, Sc)
if A.vararg is not None:
Walk(A.vararg.annotation, Sc)
if A.kwarg is not None:
Walk(A.kwarg.annotation, Sc)
Walk(Node.returns, Sc)
FS = Scope('func', Sc)
for P in Formals:
FS.Add(P.arg, Node.lineno, 0, AnnClass(P.annotation))
if A.vararg is not None:
FS.Add(A.vararg.arg, Node.lineno, 0, AnnClass(A.vararg.annotation))
if A.kwarg is not None:
FS.Add(A.kwarg.arg, Node.lineno, 0, AnnClass(A.kwarg.annotation))
for S in Node.body:
Walk(S, FS)
return
if isinstance(Node, ast.Lambda):
A = Node.args
Formals = list(A.posonlyargs) + list(A.args) + list(A.kwonlyargs)
for P in Formals:
Walk(P.annotation, Sc)
for D in A.defaults:
Walk(D, Sc)
for D in A.kw_defaults:
Walk(D, Sc)
LS = Scope('func', Sc)
for P in Formals:
LS.Add(P.arg, Node.lineno, 0, AnnClass(P.annotation))
Walk(Node.body, LS)
return
if isinstance(Node, ast.ClassDef):
for D in Node.decorator_list:
Walk(D, Sc)
for B in Node.bases:
Walk(B, Sc)
for K in Node.keywords:
Walk(K.value, Sc)
CS = Scope('class', Sc)
for S in Node.body:
Walk(S, CS)
return
if isinstance(Node, ast.Attribute):
# 只收源码里真实存在的属性访问: vast 脱糖 `X*` 会合成 `t.CPtr` Attribute,
# 该节点**连 lineno 属性都没有**(全树 555 个, 全为 ('t','CPtr')), 不是源码
# 访问、也不能参与位置比较 => 用 getattr 显式排除。
if isinstance(Node.value, ast.Name) and getattr(Node, 'lineno', None) is not None:
Uses.append((Node, Sc))
Walk(Node.value, Sc)
return
if isinstance(Node, ast.AnnAssign):
if isinstance(Node.target, ast.Name):
Sc.Add(Node.target.id, Node.target.lineno, Node.target.col_offset,
AnnClass(Node.annotation))
elif isinstance(Node.target, ast.Attribute):
Walk(Node.target, Sc) # `x.y: T = ...` 的属性访问
Walk(Node.annotation, Sc)
Walk(Node.value, Sc)
return
if isinstance(Node, ast.Assign):
Cls = None
V = Node.value
if isinstance(V, ast.Call):
Cls = CastClass(V)
if Cls is None and isinstance(V.func, ast.Name):
Cls = FuncRet.get(V.func.id)
elif isinstance(V, ast.Name):
Cls, _ = Resolve(Sc, V.id, V.lineno, V.col_offset)
for T in Node.targets:
if isinstance(T, ast.Name):
Sc.Add(T.id, T.lineno, T.col_offset, Cls)
# 赋值**目标**位置的属性访问也要计入(如 `vtable_node.Ty = ...`)
Walk(T, Sc)
Walk(Node.value, Sc)
return
if isinstance(Node, ast.AugAssign):
if isinstance(Node.target, ast.Name):
Sc.Add(Node.target.id, Node.target.lineno, Node.target.col_offset, None)
Walk(Node.target, Sc)
Walk(Node.value, Sc)
return
if isinstance(Node, (ast.For, ast.AsyncFor)):
for T in ast.walk(Node.target):
if isinstance(T, ast.Name):
Sc.Add(T.id, Node.lineno, 0, None)
Walk(Node.iter, Sc)
for S in Node.body:
Walk(S, Sc)
for S in Node.orelse:
Walk(S, Sc)
return
if isinstance(Node, ast.NamedExpr):
if isinstance(Node.target, ast.Name):
Sc.Add(Node.target.id, Node.target.lineno, Node.target.col_offset, None)
Walk(Node.value, Sc)
return
for Ch in ast.iter_child_nodes(Node):
Walk(Ch, Sc)
# ============================================================
# 4. 扫描(一轮 = ResetTables + BuildFamilies + 遍历 AppRoot)
# ============================================================
def BucketOf(Fam):
return Fam if Fam in ('ast', 'llvmlite') else 'OTHER:' + (Fam or 'support')
def Bump(D, Key):
D[Key] = D.get(Key, 0) + 1
def RunScan(AppRoot, IncludesDir, OutFile, Verbose):
"""跑一轮扫描: 写报告文件 + 返回统计/命中集合(不打印)。"""
ResetTables()
BuildFamilies(AppRoot, IncludesDir)
ComputeFamilyFields()
Nowhere = []
Other = []
ResolvedCount = 0
UnresolvedCount = 0
Files = sorted(glob.glob(os.path.join(AppRoot, '**', '*.vp'), recursive=True))
ParseFail = []
for Path in Files:
try:
Src = open(Path, encoding='utf-8').read()
except Exception:
continue
try:
Tree = ParseSrc(Src, Path)
except Exception as E: # noqa: BLE001 (vast 失败可能是 SyntaxError/ValueError)
ParseFail.append((os.path.relpath(Path, AppRoot), ErrLine(E), str(E)[:80]))
continue
RawLines = Src.splitlines()
Uses.clear()
CollectFuncRets(Tree)
MS = Scope('module', None)
for S in Tree.body:
Walk(S, MS)
for AttrNode, Sc in Uses:
Var = AttrNode.value.id
Field = AttrNode.attr
Cls, DeclLine = Resolve(Sc, Var, AttrNode.lineno, AttrNode.col_offset)
if Cls is None:
UnresolvedCount += 1
continue
Fam, CName = Cls
if (Fam, CName) not in ClassFields:
UnresolvedCount += 1
continue
ResolvedCount += 1
if Field in Member(Fam, CName):
continue
Rel = os.path.relpath(Path, AppRoot)
SrcLine = RawLines[AttrNode.lineno - 1].strip()[:96] if AttrNode.lineno <= len(RawLines) else ''
Row = (Rel, AttrNode.lineno, Var, CName, Field, DeclLine, SrcLine, Fam)
if Field in FamilyFields.get(Fam, set()):
Other.append(Row)
else:
Nowhere.append(Row)
# 稳定排序(便于 diff / 门禁)
SortKey = lambda R: (R[0], R[1], R[2], R[4], R[7], R[3], R[5])
Nowhere.sort(key=SortKey)
Other.sort(key=SortKey)
BucketsN = {}
BucketsO = {}
for R in Nowhere:
Bump(BucketsN, BucketOf(R[7]))
for R in Other:
Bump(BucketsO, BucketOf(R[7]))
ParsedN = len(Files) - len(ParseFail)
UsesN = ResolvedCount + UnresolvedCount
ClassLine = ' '.join('%s=%d' % (f, sum(1 for (x, _n) in ClassFields if x == f))
for f in sorted(set(x for x, _n in ClassFields)))
def W(Fh, Rows, Tag):
for R in Rows:
Fh.write('%-46s:%-5d var=%s cls=%s.%s field=%s decl@%d 判定=%s %s\n' %
(R[0], R[1], R[2], R[7], R[3], R[4], R[5], Tag, R[6]))
OutDir = os.path.dirname(os.path.abspath(OutFile))
if OutDir:
os.makedirs(OutDir, exist_ok=True)
with open(OutFile, 'w', encoding='utf-8') as Fh:
Fh.write('files=%d parsed=%d parsefail=%d uses=%d NOWHERE=%d OTHER=%d RESOLVED=%d UNRESOLVED=%d\n' %
(len(Files), ParsedN, len(ParseFail), UsesN,
len(Nowhere), len(Other), ResolvedCount, UnresolvedCount))
Fh.write('classes: %s\n' % ClassLine)
Fh.write('NOWHERE buckets: %s\n' % sorted(BucketsN.items()))
Fh.write('OTHER buckets: %s\n' % sorted(BucketsO.items()))
if AmbiguousBases:
Fh.write('AMBIGUOUS BASES (跨族重名, 已不解析): %s\n' % sorted(AmbiguousBases))
Fh.write('\n=== NOWHERE (本族任何类都没有该字段) ===\n')
W(Fh, Nowhere, 'NOWHERE')
Fh.write('\n=== OTHER (字段只存在于本族别的类) ===\n')
W(Fh, Other, 'OTHER')
if ParseFail:
Fh.write('\n=== PARSE FAIL (vast 无法解析, 已跳过) ===\n')
for P in ParseFail:
Fh.write('%s:%s %s\n' % P)
return {
'Files': len(Files), 'Parsed': ParsedN, 'ParseFail': ParseFail,
'Uses': UsesN, 'Nowhere': Nowhere, 'Other': Other,
'Resolved': ResolvedCount, 'Unresolved': UnresolvedCount,
'BucketsN': BucketsN, 'BucketsO': BucketsO,
'Ambiguous': sorted(AmbiguousBases), 'Classes': ClassLine,
'OutFile': OutFile,
}
def PrintSummary(Stats, Verbose):
print('files=%d parsed=%d parsefail=%d uses=%d NOWHERE=%d OTHER=%d RESOLVED=%d UNRESOLVED=%d -> %s' %
(Stats['Files'], Stats['Parsed'], len(Stats['ParseFail']), Stats['Uses'],
len(Stats['Nowhere']), len(Stats['Other']),
Stats['Resolved'], Stats['Unresolved'], Stats['OutFile']))
print('NOWHERE buckets: %s' % sorted(Stats['BucketsN'].items()))
print('OTHER buckets: %s' % sorted(Stats['BucketsO'].items()))
if Stats['Ambiguous']:
print('AMBIGUOUS BASES: %s' % Stats['Ambiguous'])
if Stats['ParseFail']:
print('PARSE FAIL: %s' % Stats['ParseFail'])
Limit = 25 if not Verbose else 100000
for R in Stats['Nowhere'][:Limit]:
print('N %-40s:%-5d %s.%s.%s [decl@%d]' % (R[0], R[1], R[7], R[3], R[4], R[5]))
for R in Stats['Other'][:Limit]:
print('O %-40s:%-5d %s.%s.%s [decl@%d]' % (R[0], R[1], R[7], R[3], R[4], R[5]))
# ============================================================
# 5. --selftest: 内置合成用例(临时目录生成, 跑完即删)
# 断言"报出集合 == 预期集合";正例 6 条(含 vast 专有写法)、负例 4 个文件。
# ============================================================
SelftestCases = [
('CaseP1AstNameOther.vp', """import ast
def ProbeP1(x_r: rawptr[ast.AST]) -> int:
return x_r.name
"""),
('CaseP2LlvFunctionTyOther.vp', """import llvmlite
def ProbeP2(x_r: rawptr[llvmlite.Function]) -> int:
return x_r.Ty
"""),
('CaseP3AstNowhere.vp', """import ast
def ProbeP3(x_r: rawptr[ast.AST]) -> int:
return x_r.zzz_not_a_field
"""),
('CaseP4AstStarOther.vp', """import ast
def ProbeP4(x_r: rawptr[ast.AST]*) -> int:
return x_r.name
"""),
('CaseP5AstStarStarOther.vp', """import ast
def ProbeP5(x_r: rawptr[ast.AST]**) -> int:
return x_r.name
"""),
('CaseP6ReturnInferOther.vp', """import llvmlite
def MkP6(x_r: rawptr[llvmlite.Function]) -> rawptr[llvmlite.Function]:
return x_r
def ProbeP6(x_r: rawptr[llvmlite.Function]) -> int:
y_r = MkP6(x_r)
return y_r.Ty
"""),
('CaseN1ParamNodeTy.vp', """import llvmlite
def ProbeN1(x_r: rawptr[llvmlite.ParamNode]) -> int:
return x_r.Ty
"""),
('CaseN2SameNameScopes.vp', """import llvmlite
def ProbeN2A(x_r: rawptr[llvmlite.ParamNode]) -> int:
return x_r.Ty
def ProbeN2B(x_r: rawptr[llvmlite.Function]) -> int:
return x_r.Ty
"""),
('CaseN3DocComment.vp', """import ast
def ProbeN3(x_r: rawptr[ast.AST]) -> int:
\"\"\"docstring: x_r.name / x_r.zzz_not_a_field 只是说明文字, 不是代码。\"\"\"
# 注释: x_r.name / x_r.zzz_not_a_field
return 0
"""),
('CaseN4ReturnInferOk.vp', """import llvmlite
def MkN4(x_r: rawptr[llvmlite.ParamNode]) -> rawptr[llvmlite.ParamNode]:
return x_r
def ProbeN4(x_r: rawptr[llvmlite.ParamNode]) -> int:
y_r = MkN4(x_r)
return y_r.Ty
"""),
]
# (文件, 变量, 族.类, 字段) -> 判定
SelftestExpected = {
('CaseP1AstNameOther.vp', 'x_r', 'ast.AST', 'name'): 'OTHER',
('CaseP2LlvFunctionTyOther.vp', 'x_r', 'llvmlite.Function', 'Ty'): 'OTHER',
('CaseP3AstNowhere.vp', 'x_r', 'ast.AST', 'zzz_not_a_field'): 'NOWHERE',
('CaseP4AstStarOther.vp', 'x_r', 'ast.AST', 'name'): 'OTHER',
('CaseP5AstStarStarOther.vp', 'x_r', 'ast.AST', 'name'): 'OTHER',
('CaseP6ReturnInferOther.vp', 'y_r', 'llvmlite.Function', 'Ty'): 'OTHER',
('CaseN2SameNameScopes.vp', 'x_r', 'llvmlite.Function', 'Ty'): 'OTHER',
}
def SelfTest(IncludesDir, OutFile):
AppRoot = tempfile.mkdtemp(prefix='ScanAstFieldMissSelftest.')
try:
for Name, Src in SelftestCases:
with open(os.path.join(AppRoot, Name), 'w', encoding='utf-8', newline='\n') as Fh:
Fh.write(Src)
Report = OutFile or os.path.join(AppRoot, 'Selftest.txt')
Stats = RunScan(AppRoot, IncludesDir, Report, False)
Reported = {}
for R in Stats['Nowhere']:
Reported[(R[0], R[2], '%s.%s' % (R[7], R[3]), R[4])] = 'NOWHERE'
for R in Stats['Other']:
Reported[(R[0], R[2], '%s.%s' % (R[7], R[3]), R[4])] = 'OTHER'
Ok = True
if Stats['ParseFail']:
Ok = False
print('[selftest] 解析失败(预期 parsefail=0):')
for P in Stats['ParseFail']:
print(' %s:%s %s' % P)
Missing = sorted(set(SelftestExpected) - set(Reported))
Extra = sorted(set(Reported) - set(SelftestExpected))
TagDiff = sorted(K for K in set(Reported) & set(SelftestExpected)
if Reported[K] != SelftestExpected[K])
for K in Missing:
Ok = False
print('[selftest] 漏报: %s -> 预期 %s' % (K, SelftestExpected[K]))
for K in Extra:
Ok = False
print('[selftest] 误报: %s -> %s' % (K, Reported[K]))
for K in TagDiff:
Ok = False
print('[selftest] 判定不符: %s -> %s(预期 %s)' % (K, Reported[K], SelftestExpected[K]))
print('[selftest] files=%d parsed=%d parsefail=%d NOWHERE=%d OTHER=%d 报出=%d 预期=%d' %
(Stats['Files'], Stats['Parsed'], len(Stats['ParseFail']),
len(Stats['Nowhere']), len(Stats['Other']), len(Reported), len(SelftestExpected)))
print('[selftest] %s' % ('PASS' if Ok else 'FAIL'))
return 0 if Ok else 1
finally:
shutil.rmtree(AppRoot, ignore_errors=True)
# ============================================================
# 6. CLI
# ============================================================
def ParseArgs(Argv):
Opt = {'AppRoot': DefaultAppRoot, 'Includes': DefaultIncludesDir, 'Out': None,
'Strict': False, 'Verbose': False, 'SelfTest': False}
Need = {'--app-root': 'AppRoot', '--includes': 'Includes', '--out': 'Out'}
Flags = {'--strict': 'Strict', '--verbose': 'Verbose', '--selftest': 'SelfTest'}
I = 0
while I < len(Argv):
A = Argv[I]
if A in Need:
if I + 1 >= len(Argv):
sys.stderr.write('参数缺值: %s\n' % A)
return None
Opt[Need[A]] = Argv[I + 1]
I += 2
continue
if A in Flags:
Opt[Flags[A]] = True
I += 1
continue
sys.stderr.write('未知参数: %s(用法见本脚本 docstring)\n' % A)
return None
return Opt
def main():
Opt = ParseArgs(sys.argv[1:])
if Opt is None:
return 2
if Opt['SelfTest']:
return SelfTest(Opt['Includes'], Opt['Out'])
Stats = RunScan(Opt['AppRoot'], Opt['Includes'], Opt['Out'] or DefaultOutFile, Opt['Verbose'])
PrintSummary(Stats, Opt['Verbose'])
if Opt['Strict'] and (Stats['Nowhere'] or Stats['Other'] or Stats['ParseFail']):
sys.stderr.write('[strict] 门禁失败: NOWHERE=%d OTHER=%d parsefail=%d\n' %
(len(Stats['Nowhere']), len(Stats['Other']), len(Stats['ParseFail'])))
return 1
return 0
if __name__ == '__main__':
sys.exit(main())