Files
TransPyC/Tools/ScanAstFieldMiss.py
T
TermiNexus c83fc752c6 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 口径等历史改动一并入库
2026-09-26 19:27:22 +08:00

841 lines
32 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 -*-
"""作用域感知静态扫描: `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())