# -*- coding: utf-8 -*- """作用域感知静态扫描: `rawptr[<族>.]` 变量上访问 不存在的字段(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) `REnum`(tagged union)派生类: 变体字段合并进父类成员集 (VPC 允许在父类指针上访问变体字段, 不合并会产生大量误报)。 前端 = **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 | CPtr`(`**` 加两层), 其中合成的 `CPtr` Attribute 节点**连 lineno 属性都没有**(编译器内部脱糖产物, 不是源码里的 访问; 全树实测 555 个, 全部是 ('CPtr'))=> 计入 Uses 时用 `getattr(Node,'lineno',None) is not None` 排除。 B) vast 的 SyntaxError 可能不带 lineno(C 路径)=> 从报文 `at line N` 兜底提取。 C) vast 的装饰器 `@NoVTable` 的 col_offset 是 `@` 的位置(CPython 是其后 1 列); 该 Attribute 的接收者 `t` 不是族指针, 判据不受影响。 D) 其余节点类名/属性名/walk/iter_child_nodes 与 CPython ast 同构(已逐节点对照)。 输出: 文件:行号 / 变量 / 推导出的类 / 字段 / 声明来源行 / 判定, 并统计 NOWHERE / OTHER / RESOLVED / UNRESOLVED, 并按族(ast/llvmlite/其它)分桶。 路径: 全部按**脚本自身位置**推导(ScriptDir = /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 /ViperC/App --includes /includes --out /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 = /TransPyC/Tools # TpcRoot = /TransPyC (TransPyC 仓库根) # RepoRoot = (工作区根) # ============================================================ ScriptDir = os.path.dirname(os.path.abspath(__file__)) TpcRoot = os.path.dirname(ScriptDir) RepoRoot = os.path.dirname(TpcRoot) DefaultAppRoot = os.path.join(RepoRoot, 'ViperC', '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/(使 `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 表示 REnum/CEnum 派生(变体字段合并) REnumClass = {} # (族, 类名) -> 1 表示 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(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[<族>.](可被 `| 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[<族>.](...)` 形态的 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*` 会合成 `CPtr` Attribute, # 该节点**连 lineno 属性都没有**(全树 555 个, 全为 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())