Compare commits

...
70 Commits
Author SHA1 Message Date
TermiNexus 5463fac3d2 feat(vipeye-internal): 支持 match-case 内裸 break 与 fallthrough
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- break:parser 新增 _isInMatchCase(解析 case body 期间置位,与 _isInLoop 同法),_parseBreakStatement 判据放宽为 !_isInLoop && !_isInMatchCase,不再误报 breakOutsideLoop

- fallthrough:新增 ParseNodeType.Fallthrough / FallthroughNode;作为软关键字仅在「viper 方言 + case 体内 + 独占简单语句」时成立,其它位置仍是普通标识符

- 同步 parseTreeWalker 两处 switch 与 visitor、parseNodeUtils 的 ParseNodeTypeMap、binder 流程终止、flowChartBuilder 终止语句、completion 关键字、TextMate 高亮

- 测试 viperMatchBreakFallthrough.test.ts(2 用例),全量 jest 825 -> 827 通过
2026-10-02 22:29:53 +08:00
TermiNexus 9e0d12553d feat(viper): 以选中编译器 exe 目录为基准自动发现 includes 目录
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- 工程未在 .vpj/project.json 声明 includes 时的兜底:从选中编译器 exe 所在目录向上最多 4 层,只查直接子目录 includes/include(不递归),仅收真实存在的绝对路径目录

- 落点 packages/viper/src/api.ts 的 GetCompilerIncludePaths(并加入 ViperApi 接口);由 vscode-vipeye 的 workspace.configuration 中间件追加进 extraPaths(兼容 section 为 viper / viper.analysis 两种回执形状)

- 切换编译器后补发 workspace/didChangeConfiguration,使新搜索根无需重载窗口即生效
2026-10-02 20:31:42 +08:00
TermiNexus 065ab18231 fix(viper): --version 判定跳过 [ 开头的诊断行(ASan traceback 横幅)
用户实测:带 ASan 运行时的编译器 <exe> --version 会先在 stderr 打三行
[traceback] …(stack dump / DbgHelp note / ready 提示),之后才是
"Viper 1.0.5-dev.970 2026-09-28 Non-LTS"。

Node 侧 stdout/stderr 是两条流、到达顺序不定,按"整个缓冲前 5 字符"判定会
误报 PREFIX_ERR。改为 FirstMeaningfulLine:跳过空行与以 [ 开头的行继续往下读,
取第一行有效内容判前缀(同时对行首空白宽容)。VET 把两条流接进同一管道,
不存在该问题,故口径仍是"第一行内容以 Viper 开头"。

已用等价 Node 脚本对真实 exe 实测(脚本已删):
* TransPyV\output\TransPyV.exe:PE 头 5a4d/lfanew=120/4550/magic 20b/
  machine 8664→x86_64,命中 .PRVERSN(rawPtr=4574208/rawSize=512)→
  版本串 0.0.5-dev.479;--version 捕获跳过 3 行 [traceback] 后判定通过。
* notepad.exe → NO_PRVERSN、.js 文本 → NOT_EXE、不存在路径 → NOT_FOUND。

顺带把 ViperCheckResult 改为 import type(类型专用导入,运行时不再 require
./types,便于独立转译/校验)。
2026-10-02 19:48:21 +08:00
TermiNexus 4c30604ecd feat(viper): VET 未启用时仍可校验/选择编译器(TS 模拟 check)
手动「选择编译器…」原先只能走 VET 的 check 通道(stdin 写
{"check": path, "id": N} → stdout 单行回执),该通道依附常驻进程与心跳看门狗:
VET 被禁用 / 手动停止 / 启动失败 / confirmSeconds=0 时选编译器直接失败。

现在 EnvironmentTool.CheckExecutable 先看 CanUseCheckChannel()(要求
state === 'running' && confirmSeconds > 0 && 进程在 && stdin 可写;启动中/重启中
也走本地路径,否则要等它扫完一轮才可能进确认窗口、白等超时),不满足即回退
本地模拟:

* 新增 packages/viper/src/executableProbe.ts(LocalCheckExecutable):
  ① 读 PE:DOS MZ → e_lfanew → PE\0\0 → 可选头 Magic → 节表 → .PRVERSN 段,
     取版本串;COFF Machine → arch。小端字段一律用逐字节「或 + 移位」位操作解码
     (与 VET VPe.ReadU16/ReadU32 同口径)。
  ② 跑 <exe> --version:合并捕获 stdout+stderr、5 秒超时(= VET 的
     VERSION_PROBE_TIMEOUT_MS)、输出前 5 字符须为 Viper。
* 失败原因串与 VET 的 check_msg 逐字一致(NOT_FOUND / NO_PERMISSION / NOT_EXE /
  NO_PRVERSN / PRVERSN_ERR / PREFIX_ERR / TIMEOUT / EXE_ERR,0x<HEX>),故文案与
  选择/回报流程完全复用;本地路径只读,不写缓存、不入库。
* VET 处于 running 时行为不变(仍由 VET 校验并写回它的缓存)。

配套文案:进度标题与提示去掉「VET」前缀(正在检查… / 检查未通过:…);
状态栏 tooltip 增加「VET 未运行,选择时由扩展本地检查该 exe」说明。
2026-10-02 18:57:40 +08:00
TermiNexus 3fd61c2594 feat(vipeye-internal): 支持泛型类型参数带界 class GSList[T: GSListNode]
includes/linkedlist.vp 的 GSList 泛型链表 body 依赖 T 的上界解析成员,原先
共 4 条误报(GSList[T]* 的 Head/Tail 赋值「GSListNode* 不可分配给
rawptr[type[T@GSList]]」+ CPtr[T] 上查 Next「属性 Next 未知」)。补齐三处:

① CPtr 解引用把 TypeVar pointee 具体化为上界(成员访问 / applyAttributeAccess
   Override / 下标三处 Viper CPtr 分支):否则 self.Tail.Next(base = CPtr[T])
   在 CPtr 上找 Next 失败。
② assignType 新增「泛型类型参数值 → CPtr[T]」放行(紧跟既有「类实例 → CPtr[T]
   隐式取地址语义」之后):判据为目标 pointee 与源是同一类型参数
   (TypeVarType.getNameWithScope 相等),或把源具体化后按普通规则递归比较。
   依据:Viper 中类类型的值本身就是指针,覆盖 self.Head = node / self.Tail = node。
③ 未特化泛型基类上解析出的成员类型会退化成 CPtr[builtins.type 实例]
   (即"某个类对象"),按「指针目标未知」放行(与 rawptr/void* 同口径)。

无界(GSList[T])仍报 Next 未知 —— 带界才是修复手段,测试固定该差异。
TransPyC 无需改动(includes/ast/parser.vp 本就跳过 bound)。

新增 viperGenericBound.test.ts 2 用例(带界 0 报错;无界报错行恰为 [20,24])。
jest 823 → 825。
2026-09-29 00:32:18 +08:00
TermiNexus b8101947cf fix(vipeye-internal): 同一继承体系内结构体指针双向转换(放行下转型)
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
指针隐式转换收紧后,结构体分支只认 isDerivedFrom(src, dest) 的**上转型**,
把下转型判为非法,导致 TransPyV 真实代码误报:

  TransPyV/App/lib/core/Gen/Call/CallGenDispatch.vp:824
    len_arg_node: ast.AST* = cargs.get(0)
    self.Gen.CallGenProp._is_list_builtin_receiver(pool, len_val, len_cls_nm,
                                                   "__len__", len_arg_node)
    形参声明 cl: rawptr[ast.Call](Call 派生自 AST)
  → 无法将"rawptr[AST]"类型的参数分配给函数"_is_list_builtin_receiver"中
    类型为"rawptr[Call]"的参数"cl"

Viper 指针运行期不带静态类型(都是 i8*),同一继承体系内的 reinterpret
(含下转型)是既有代码的常规写法,编译器也接受,故改为**继承体系内双向**:
isDerivedFrom(src, dest) || isDerivedFrom(dest, src)(仍在 cloneAsInstantiable
之后比较)。不相关结构体(Node* → Other*)、结构体 ↔ 基本类型仍拒绝。

viperPointerConversion.test.ts 增至 3 用例:新增「AST*↔Call* 放行 + 不相关结构体
报 L28」,既有 2 用例断言([19] / [36,46,49])不变。jest 822 → 823。
2026-09-28 17:37:23 +08:00
TermiNexus bc5bd3a425 fix(vipeye-internal): REnum 变体指针与根指针互转放行(修复 llvmlite 误判)
指针隐式转换收紧后,「pointee 必须兼容」把 REnum 变体指针
rawptr[LLVMType.Ptr] 判为与 rawptr[LLVMType] 不兼容,导致 llvmlite 的
build_load(builder, ty: rawptr[LLVMType], ...) 误报「无法将 rawptr[Ptr]
类型的参数分配给类型为 rawptr[LLVMType] 的参数 ty」。

成因:变体类(class LLVMType(t.REnum) 内的 class Ptr / class Int ...)是
**嵌套类**而非根类的子类,isDerivedFrom 判不出关系;而 match 的变体模式收窄
(patternMatching.narrowTypeBasedOnClassPattern)会把 CPtr[LLVMType] 收窄成
CPtr[LLVMType.Ptr],CallGenCall.vp 里的 lb_inner 正是这样被收窄后传回根指针形参。

TransPyV 对二者只 emit bitcast(tagged-union 负载地址 ↔ 联合地址),故放行同一
REnum 家族内的指针双向互转:新增 isViperREnumFamilyPointee(fullName 前缀嵌套
判定 + isViperREnumRootClass 判 mro 含 .REnum),与收窄判据同口径。
跨变体(Ptr* → Int*)与 REnum ↔ 无关结构体仍拒绝。

新增 viperREnumPointer.test.ts 2 用例:嵌套 match 收窄形状 0 报错;转换矩阵
断言报错行恰为 [32,36,37]。jest 820 → 822。
2026-09-28 17:17:36 +08:00
TermiNexus f31ff4904c chore: 版本号提升至 1.1.427,移除函数 hover 调试信息并更新 README
- 移除 hover 函数时附加的「函数调试信息」区块(位置/修饰符/泛型参数/
  参数/返回类型),hover 只保留签名;删除 _addViperFunctionDebugPart /
  _getViperParamDebugText / _getViperFunctionReturnType 与随之无用的
  ParamCategory / ParameterNode 导入
- viperFunctionHover.test.ts 改为断言「只显示签名、不再含调试信息字段」
- CHANGELOG 拆分:baec16e(1.1.426 发布)之后 14 个提交追加的 16 条
  未发布条目移入新增的 ## 1.1.427 段,## 1.1.426 段保留发布时内容
- 版本号 1.1.426 → 1.1.427(vipeye / vscode-vipeye / vipeye-internal 的
  package.json 与 package-lock.json 共 6 个文件)
- README(根 + 扩展)更新:修正指针隐式转换规则(同级 + pointee 兼容,
  结构体仅允许上转型),补充声明/注解修饰符、[SIZE]T 与 C 风格强转、
  virtual class 方法槽赋值、属性名补全/悬停、状态栏分析进度与 LOC 命令
2026-09-28 17:01:18 +08:00
TermiNexus bc500d060f feat(vipeye-internal): 放行 virtual class 方法槽位赋值(vtable)
ViperOS devfs.vp 的 `serial0_file.open = serial0_open` 原先误报
「无法为类"devfs_file"的属性"open"赋值 / 参数名称不匹配 /
额外参数 "flags"」。

根因:devfs_file 是 virtual class,open/close/read/write/ioctl 是虚方法
(vtable 槽)。该赋值写的是 vtable 槽;编译器只把 RHS 函数 bitcast i8*
后 store,不校验签名与 self。而 LSP 走普通成员赋值路径,比较的是
**绑定方法类型**(self 已剥掉),于是 RHS 显式写的 self: rawptr 成了多余参数。

修法:typeEvaluator 的 usage.method === 'set' 分支里,assignType 之前加
isViperVtableMethodSlotAssignment 命中即视为兼容、跳过报错与收窄。判据:
- 类是 virtual class(ClassNode.d.viperClassModifier === 'virtual'
  或 @t.CVTable 装饰器);注意传入的 classType 是**实例**形态,必须先转
  可实例化形态(第一版没生效的原因)
- 成员在类里声明为方法(FunctionType)
- RHS 的 self 为泛指针(rawptr / t.CPtr / void*)或该类本身/父类的指针
- 其余非变参参数逐个 name + isTypeSame 相同,返回类型 isTypeSame 相同
  (*args / **kwargs 不参与比较)

未放行:非 virtual class、self 非指针、参数名/类型/个数不符、返回类型不符。

- 新增 viperVtableMethodAssign.test.ts(3 用例)
- .gitignore:新增 3 个 __viper_vtable_assign_* 产物目录
- CHANGELOG:1.1.426 追加 1 条
2026-09-28 15:35:47 +08:00
TermiNexus d91fc2d67e feat(vipeye-internal): 指针隐式转换收紧为「同级 + pointee 兼容」
原先「同级指针无条件允许转换」,现改为:级数必须相同,且 pointee 必须兼容。

规则(canImplicitlyConvertCType + 新增 isViperCPtrPointeeAssignable):
- 任一侧 pointee 是 void*/未定(rawptr)→ 允许
  (裸 rawptr 的实参是类自身的 TypeVar,不是 UnknownType,必须一并认)
- pointee 规范化后相同 → 允许(先剥联合里的 C 修饰符、
  再统一实例形态,否则 volatile int / type[ParamNode] 会误报)
- 两侧 pointee 都是基本类型(数值标量)→ 复用 T 的转换规则
- 结构体等非基本类型 → 仅允许上转型(源类派生自目标类,
  比较须 cloneAsInstantiable;MemBuddy* → MemManager* 是真实依赖),
  不相关结构体 / 向下转型 / 结构体↔基本类型一律拒绝
- 单级指针 ↔ t.CVoid(void*)双向允许;多级(Node** → void*)禁止

要点:CPtr↔CPtr 分支必须放在「同名即允许」快捷判断之前(两 CPtr 的
shared.name 都是 'CPtr',否则 pointee 完全不看)。

`&x` / `*x` 的返回类型本就正确(x: T* → *x: T、&x: T**),本次确认它们
已参与上述拦截:a(&x)(x: T*,T** 传给 void*)仍报错。

- 新增 viperPointerConversion.test.ts(2 用例:一元 &/* 类型 + 拦截;
  同级 pointee 兼容矩阵,断言报错行恰为 [36,46,49])
- .gitignore:新增 __viper_ptr_unary__/ __viper_ptr_convert__/
- CHANGELOG:1.1.426 追加 1 条
2026-09-28 15:26:51 +08:00
TermiNexus c7878ab9d7 feat(vipeye-internal): __attribute__ 补全/hover 列出未声明的属性名
属性名是编译器级开放集合,补全/hover 原先只枚举 `t.attr`,故 codegen 支持
但未声明的名字(唯一已知例子 stackrealign)看不到。

- viperAttributeUtils:新增 VIPER_UNDECLARED_ATTRIBUTE_NAMES(来源
  TransPyV FuncGenBase.vp 的 stackrealign 映射、ViperOS main.vp 实际使用),
  以及 getViperAttributeNames(全集,供 hover)与
  getViperUndeclaredAttributeNames(仅未声明,供补全注入)——两处共用同一来源,
  不会与 t.attr 符号表漂移
- hoverProvider:属性清单改用 getViperAttributeNames
- completionProvider:符号表循环后注入未声明属性名(43 项)
- 不改存根 t.vpi(沿用既有决定)

- viperAttribute.test.ts:既有补全/hover 两用例各加 stackrealign 断言
- CHANGELOG:1.1.426 追加 1 条
2026-09-28 15:04:26 +08:00
TermiNexus 71cb333549 feat(vipeye-internal): __attribute__ 属性名结构化高亮(未声明的也不报错)
`@__attribute__(section(".text.startup"), aligned(16), stackrealign)` 里
`stackrealign` 原先不高亮(白色)且误报「无法访问类"type[attr]"的属性」。

根因:属性名脱糖成 `t.attr.<name>`,高亮完全依赖能否解析到 `class attr`
的声明。而 `stackrealign` 在真实 t.py 与 LSP 存根 t.vpi 的 attr 类里都没有
(两边一致、无缺失),只在 TransPyV 的 codegen 映射(FuncGenBase.vp:181)
里支持,ViperOS main.vp:347 在用。

改法(结构化,不依赖存根声明):
- parseNodes:NameNode.d 新增 isViperAttrMarkerName
- parser._makeViperAttrMarker:给合成的属性名成员名打该标记
- semanticTokenProvider._classifyNameNode:该标记直接按 function 着色
  (与 section/aligned/naked 等已声明属性同款)
- typeEvaluator:新增 isViperAttrMarkerMember,与
  isViperBracketArraySyntheticMember 一起跳过 memberAccess 诊断
  (编译器对未知属性是静默丢弃,不是报错,故这是误报)

- viperAttribute.test.ts:增至 9 用例(新增未声明属性名 stackrealign 的
  高亮 + 不报错断言)
- .gitignore:新增 __viper_attribute_unknown__/
- CHANGELOG:1.1.426 追加 1 条
2026-09-28 14:59:44 +08:00
TermiNexus b4808c2821 fix(vipeye-internal): [SIZE]T 数组不要求源码 import t(元素成员可解析)
ViperOS 协程调度器里 `coroutines: [8]Coroutine` 字段在**没 import t** 的
文件中,`m.coroutines[prev_cid].rsp` 报「无法解析类型上的成员“rsp”访问」,
`.rsp` 拿不到语义 token(黄标、无法跳转)。

根因:`[SIZE]T` 脱糖出的合成 `t` 名(isViperBracketArraySynthetic)在
typeEvaluator 的 Name 分支里解析不到时直接降级为 Unknown,于是
`t.CArray[Coroutine, 8]` 整条塌成 Unknown → 字段类型 Unknown → 元素成员访问全断。

修法:该分支改为 `getViperImplicitModuleType(node, name) ?? Unknown`,
回退到 builtins 作用域里绑定的 `t` 模块(与 `T*` 的 rawptr 回退、
`__attribute__` 的隐式模块根同口径);仍取不到才静默 Unknown。

- viperBracketArrayAndConstPtr.test.ts:增至 7 用例(新增「无 import t 时
  [SIZE]StructT 仍解析元素类型、.rsp 得到 variable(3) 语义 token」)
- .gitignore:新增 __viper_bracket_array_no_import_t__/
- CHANGELOG:1.1.426 追加 1 条
2026-09-28 14:22:38 +08:00
TermiNexus f234e38701 fix(vipeye-internal): 撤销注解内 static 前缀写法,只认语句起始形态
不再允许 `memory_regions: static memory_region* = None`,改为
`static memory_regions: memory_region* = None`(仅 Vipeye 侧改动)。

- parser:`_parseViperTypeAnnotationCore` 改为调用新增的
  `_checkViperStaticInAnnotationPosition()`,命中就报
  `viperStaticInAnnotationNotAllowed` 并跳过 static、继续解析剩余类型
  (错误恢复,不再连带报 "static" is not defined)
- 删除 `_parseViperStaticTypeDescriptorPrefix()`、
  `_viperStaticDescriptorInAnnotation` 字段与 `_parseExpressionStatement`
  里的标记读取块
- localize + nls(zh-cn/en-us):新增 viperStaticInAnnotationNotAllowed
- viperStaticModifier.test.ts:原两个注解内 static 用例换成
  「语句起始 static 合法」+「注解内 static 报专用错误」两个用例
2026-09-28 13:46:12 +08:00
TermiNexus 0bc7a7c4d4 feat(vipeye-internal): 支持 export/extern 变量声明(extern 禁止初始值)
- export NAME: T [= v] -> T | t.CExport(裸名定义,不加模块 SHA1 头),允许初始值;
  extern NAME: T -> T | t.State(= CExtern | CExport 仅声明),不允许初始值:
  extern a: int = 1 报「extern 变量声明不能带初始值(符号定义在外部)」
- 与函数形态由第三个 token 区分(`:` 变量 / `(` `[` 函数),与 TPC
  _is_export_extern_var 同判据;export def / export f() 形态逐字不变
- 存储类标记按修饰符处理:Struct|CPtr 折叠与联合下标 skip 列表改用
  isCModifierType(避免 extern p: t.CPtr 折成 CPtr[State]、str|State 下标误报);
  CArray 元素访问快路径放宽到"联合里的 CArray 成员"(gdt[i].access 仍解析)
- typePrinter 增加 CExport->export / State->extern 前缀渲染(hover 显示 export int)
- 新增 viperExportExternVar.test.ts 4 用例,jest 809 -> 813
2026-09-27 18:13:37 +08:00
TermiNexus c8817cacb1 fix(vipeye-internal): CArray 元素访问解析出元素类型(成员可高亮)
- kbd_buf: [KBD_BUF_SIZE]usb_kbd_event 的 kbd_buf[i].modifier 原先报
  「无法解析类型上的成员“modifier”访问」且该成员拿不到语义 token(纯白)
- 根因:t.vpi 的 CArray 是 Generic[T, N] + __getitem__ -> T,但 Viper 侧
  TypeVar(...) 不产出可替换类型变量(类型参数落到合成参数 __P0/__P1),
  返回类型永远具体化不了 → 下标恒为 Unknown
- 修法:getTypeOfIndexedObjectOrClass 新增 Viper 分支,按 C 语义直接取
  CArray 第一个类型实参(与 CPtr 分支读 priv.typeArgs[0] 同法),
  `[SIZE]T` 与显式 t.CArray[T, N] 两种写法都覆盖
- 回归用例 +1(元素类型 + modifier 语义 token),jest 808 -> 809
2026-09-27 18:02:53 +08:00
TermiNexus 5808a76b23 fix(vipeye-internal): 支持注解内 static 前缀;*args 在未 import t 时仍是 va_list 指针
- `x: static T = v`(ViperOS VKernel/Kernel/drivers/** 全仓写法,如
  `buf_head: static uint32 = 0`)脱糖为与语句起始形态相同的 `t.CStatic | T`,
  并在声明节点打 isViperStatic,跨模块取参检查同判据;此前 `static` 在注解里
  被当普通名引用,误报「"static" is not defined」。软关键字边界不变
  (`static: int` / `static = 1` 仍按标识符),tmLanguage 一并高亮注解内 static
- `getViperCPtrType` 增加 builtins `rawptr` 回退:文件没有 `import t` 时
  `*args` 不再退回 Python tuple 语义,`viperlib.vsnprintf(&buf, 1024, fmt, args)`
  不再误报「tuple 不能赋给参数 ap(str)」(与 T* 注解 / [SIZE]T 同机制)
- 回归用例 +3:注解内 static 解析/展示/跨模块拦截 + 软关键字边界、
  未 import t 的 *args 透传;jest 805 -> 808
2026-09-27 14:26:42 +08:00
TermiNexus 2a7757e3b7 fix(vipeye-internal): 拦截 const 指针下标赋值并放行 const 指针迭代
- const 指针的 p[i] = v 下标赋值与 *p = v 解引用赋值共用
  viperConstDerefAssignment 诊断,指向只读数据一律报错;
  非 const 指针(buf: str)的下标/解引用赋值仍合法
- 迭代判据改认全 Viper 指针(CPtr / str / bytes):新增
  viperTypeContainsPointer 并替换 getTypeOfIterator / getTypeOfIterable
  里只认 CPtr 的旧写法,path: const str 的 for ch in path: 不再
  误报「“CConst”不可迭代 / 未定义 __iter__」;const int 仍不可迭代
- 补 2 个回归用例(下标读写、const 指针可迭代),jest 803 -> 805
2026-09-27 13:49:50 +08:00
TermiNexus a7a00a23d5 fix(vipeye-internal): [SIZE]T 支持整型常量运算;const 指针只读(解引用可读不可写)
- [SIZE]T 的 SIZE 是值表达式,允许任意整型常量运算(如
  [MAX_VOLUMES * MAX_PARTITIONS]uint8 / [N - 1]uint8 / [1 << 3]uint8)。
  parser:解析 SIZE 期间清除 _isParsingTypeAnnotation,否则 Viper 的
  「星号是指针后缀 + 注解中停止解析」分支会吞掉 '*',误报「[ 未关闭」与
  「类型表达式中不允许使用解包运算符」。
  evaluator:operations 的 TypeExpression 分支新增整型常量算术放行
  (isViperIntegerConstantArithmetic),此前注释已写明但代码未实现,
  t.CArray[T, MAX_ORDER + 1] 同样受益;类名相加仍报 binaryOperationNotAllowed。
- const 指针(lfn: const str = CConst | str)只读:解引用读允许
  (*lfn / lfn[0] → char,getTypeOfViperDeref 跳过 C 修饰符子类型,
  不再误报 类型"const str"不支持运算符"*"),解引用赋值报新诊断
  viperConstDerefAssignment(*lfn = c);非 const 指针 *buf = c 仍合法。
- 新增 viperBracketArrayAndConstPtr.test.ts(3 用例);全量 jest 800 → 803 通过。
2026-09-27 13:44:30 +08:00
TermiNexus 90f4118603 feat(vipeye-internal): 支持 static 修饰符(禁止跨模块取参)与 f-string 前缀高亮
- static 是软关键字:`static def f` / `static fn f`(走 virtual 同一机制,记在
  FunctionNode.d.viperModifier)与 `static x: T = v`(注解脱糖为 `t.CStatic | T`,
  与 TPC _parse_type_ann 同形,展示为 `static int`)。其余位置仍是普通标识符
  (`static = 1` / `static: int` / `static(x)` / `def f(static: int)`)。
- 语义 = C 的 internal linkage:跨模块取参报错。两处拦截:模块成员访问
  (`mod.name` / `mod.fn()`)与 `from mod import name`;按声明所在文件判定,
  同模块自引用/自导入放行。新增诊断 viperStaticUsedOutsideOfModule(zh-cn/en-us)。
  判据 parseTreeUtils.isViperStaticDeclarationNode 供两处共用。
- static 并入补全关键字与 TextMate 修饰符高亮。
- f-string 的 r/u/b/f 前缀以 storage.type.string.viper 高亮(默认深色主题下为蓝色),
  之前该前缀不在字符串范围内、无任何规则命中,显示为白色。
- 新增 viperStaticModifier.test.ts(4 用例);全量 jest 796 → 800 通过。
2026-09-27 13:19:57 +08:00
TermiNexus 4d5a0d203b feat(vipeye-internal): __attribute__ 免 import t/c 与状态栏分析进度
- __attribute__ 脱糖出的 t / c 模块根改为「隐式模块根」(零长度合成名 +
  isViperImplicitModule 标记),作用域链取不到时回退 builtins 直接查
  symbolTable,故不再要求源码 import t / import c;手写裸 t / c 仍报未定义。
  补上 builtins.vpi 缺失的 import c(原先只有 import t,装饰器路径一直报
  「未定义 c」)。
- 新增左下角状态栏分析进度:逐文件回调 → 后台线程节流 50ms 回传
  analysisProgress 消息 → 前台 sendNotification('viper/analysisStatus')
  → 客户端 StatusBarItem 显示 `C:\xxx\1.vp (4/90) 4.44% loading...`,
  分析结束自动隐藏,让用户能区分「插件在忙」与「插件卡死」。
- viperAttribute.test.ts 补回被误删的 hover 用例并新增 2 个回归用例;
  jest 794 → 796 通过。
2026-09-27 13:03:03 +08:00
TermiNexus baec16e540 feat(vipeye-internal): Viper 1.1.426 语法/语义/补全增强
把 TransPyC 现行语法与 Viper 的类型组合语义补齐到 LSP 侧(编译器 TPC/TPV 不改动):

- 类型描述符:`const` / `volatile` 可前缀任意类型注解并成为硬关键字(仅注解前缀
  合法,`const = 1` / `a: const` 报语法错误),解析层脱糖为等价的 `CConst | T` /
  `CVolatile | T`,与手写 TypeSet 完全同形;`const T*` 得 `CConst | CPtr[T]`;
  `T*` / `&x` 不再要求 import t(回退 builtins 的 rawptr);描述符按「前缀限定符」
  渲染(`const int` / `const volatile int` / `const rawptr[int]`)而不泄漏为联合成员
- 新增 `inline while` / `inline for`;`const` / `volatile` / `inline` 高亮为修饰符
- 新增内建类型名 uint / uchar,并把 rawptr / sizet / int8..uint64 / uintfast* /
  uintleast* 等内建到 builtins.vpi,任意 Viper 工程无需配置 includes
- 新增 `virtual` / `novirtual` 声明修饰符(等价 @t.CVTable / @t.NoVTable),
  含放置位置校验,并并入补全与高亮
- `(T**)x` 两级指针强转修复:指针类型后缀统一按类型表达式求值,不再被当成解引用
- 裸内置内联汇编 / LLVM IR:`__asm__` / `asminp` / `asmout` / `__llvmir__` /
  `irinp` / `irout` / `ASM_DESCR`,与 c.* / t.ASM_DESCR 写法等价
- `X | t.CPtr` 在 X 本身是指针时再包一层(HANDLE | t.CPtr = void**),
  修复 CreatePipe 一类 LPHANDLE 的「跨级指针隐式转换不允许」误报
- 补全并入 Viper 专有关键字(define / const / volatile / typedef / inline / export /
  extern / virtual / novirtual / enum / rnum / union / struct / fn / fun / func / function)
- `T*(x)`:裸模块名 + 模块内同名类归一为 CPtr[bootinfo];并按 TPC `_AtPtrSuffixCall`
  的判据把「类型原子 + 紧贴星串 + 紧贴 (」解析为调用,使
  `uint32*(info.vendor_id)[0] = res.ebx` 成为合法赋值目标
- 新增「重复声明」检查:模块级已有同名运行期变量时,函数内再用类型注解声明同名
  变量无条件报 TypeError(与 TPV AssignGenMixin 同口径);排除 CDefine 宏、CTypedef
  别名、同名函数/类与类体成员。合法写法只剩 `global x` + 无注解赋值,或换名
- 支持 `__attribute__`:装饰器 `@__attribute__(...)` 脱糖为
  `@c.Attribute(t.attr.X, ...)`;注解修饰器 `a: __attribute__(...) const int`
  脱糖为 `t.attr.X | (CConst | int)`;注解联合中的 `t.attr.*` 标记求值为 t.CAttr
  修饰符(运算/下标/赋值/展示均按被修饰的基础类型处理);值位置
  `volatile f()` / `volatile 结构体()` 前缀;`__a` 补全、括号内属性名补全与悬停、
  TextMate 高亮
- 注解位置小写 `callable[[A, B], R]` 等价 t.Callable:parser 仅在类型注解位置把裸名
  `callable` 脱糖为 `Callable`,builtins.vpi 内建 `Callable` 别名(无需 import t);
  值位置 `callable(x)` 仍是内置函数
- 版本号提升至 1.1.426 并更新 CHANGELOG;新增 6 个回归测试文件、更新 4 个,
  全量 64 套件 / 794 用例全绿(另有 1 套件 8 用例为既有 skip)
2026-09-27 12:50:02 +08:00
TermiNexus b98102b78f feat(viper): 新增「统计项目 LOC」命令与报表 Webview
- 右键 .vpj → Count Project LOC(viper.countLines):递归扫描项目的 source_dir,
  统计文件数 / 总行 / 代码行 / 注释行 / 空行 / 注释率 / 平均行长 / 字符数
- 先打开 Viper LOC Webview 面板再扫描:确定性进度条(先收集文件,再按 N/M 处理
  并显示当前文件),完成后渲染 KPI 卡片、按语言的堆叠条形图(代码/注释/空行,
  以最大语言为基准缩放)、语言筛选(勾选后重算 KPI/图表/表格)与明细表(含合计行)
- 同一份报表以纯文本写入 Viper LOC 输出通道,便于复制
- 计入本扩展注册的语言扩展名 .vp / .vpi / .vpnci / .vpj,并按需求支持 .def
  (; 与 // 行注释、/* */ 块注释,跨行块注释按注释计)
- 跳过项目 build_dir、隐藏目录、node_modules、.git、__pycache__,不跟随符号链接
2026-09-27 12:49:25 +08:00
TermiNexus aa1c84a326 feat(vipeye-internal): 支持 typedef 语句与 (T)x 强转写法
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- typedef:新增 `typedef T1 = T2` 软关键字语句,与 `T1: t.CTypedef = T2` 完全等价。
  解析层脱糖为同形「带注解赋值」(合成零长 t.CTypedef 紧贴别名名之后,满足
  AST 子节点位置递增不重叠的不变量),绑定/类型求值/hover/语义高亮全部复用
  既有 TypeAlias 通路,且不要求文件 import t(合成 t 标记 isViperTypedef,
  未导入时静默退化,与 [SIZE]T 脱糖同一取舍)。仅 `typedef <Name> =` 形状生效,
  `typedef = 1` / `typedef(x)` 行为不变
- 高亮:viper.tmLanguage.json 为 typedef 增加 storage.type 规则(带 `\s+Name\s*=`
  前瞻,避免误伤赋值);语义高亮无需改动(合成名零长,被既有过滤跳过)
- 强转:`(T)x` 与 `(T*)x` 统一按「括号内为类型原子 + 紧跟操作数起点」归一为
  Call,等价既有 T(x) 通路;`(T*)[i]` 仍是下标、`(a)*b` 仍是乘法
- 回归用例:viperBuiltinsVpTypedef.test.ts 新增 typedef 3 例(含不 import t 与
  typedef 作普通标识符),viperUnaryPointer.test.ts 新增 (T)x 2 例
- 版本号提升至 1.1.425 并更新 CHANGELOG;全量 57 套件 / 764 用例全绿(0 失败)
2026-09-22 22:26:07 +08:00
TermiNexus e7cf7643de feat(vipeye-internal): 支持 (T*)x、(T*)(x)、T*(x) 三种 C 风格强转写法
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
TransPyC 的"新语法类型强转"除已有的调用形式(T(x) / T(x, t.CPtr) / (T)(x) /
(T1|T2)(x))外,还使用指针写法,本次一并支持,三者结果均为指针类型 CPtr[T]:

- operations.getTypeOfUnaryOperation:值上下文里一元 '*' 的操作数是「类型对象」
  (可实例化类,如 t.CInt / ast.AST)时,按指针类型表达式求值(CPtr[T]),
  不再当作对类对象的解引用而报「不支持运算符 *」
- parser:`(T*)operand` 归一为 CallNode(func = 括号内指针类型, args = [operand]),
  与已支持的 `(T*)(x)` 通路完全一致;仅当括号内由 _parseViperPointerTypeSuffix
  标记时才生效,`(a)*b`、`(a) * (b)` 等乘法不受影响
- operations.validateBinaryOperation:二元 '*' 的左操作数是类型对象时按强转
  (等价 (T*)(x))处理,从而 T*(x) 成立;真值乘法(2 * 3 / offset * 2)零影响
- typeEvaluator:callee 为被标记的指针类型后缀且类型为 CPtr[T] 时按强转返回
  CPtr[T] 实例,跳过可调用性/参数校验(函数指针变量调用 fn(x) 不受影响)
- 回归用例:viperUnaryPointer.test.ts 新增「三种 C 风格强转」测试块,
  含真实乘法与既有强转通路的对照断言
- 版本号提升至 1.1.424 并更新 CHANGELOG;全量 57 套件 / 759 用例全绿
2026-09-22 22:07:58 +08:00
TermiNexus cef653b637 chore: 忽略测试运行生成的产物
__ty_narrow_dump.txt 与 __unary_ptr__/unary_ptr.vp 每次跑 jest 都会被重写,
从版本库移除并加入 .gitignore,避免每次测试后工作区都变脏。
2026-09-22 22:07:44 +08:00
TermiNexus 36dab6433b fix(vipeye-internal): 打通 TransPyC 现行语法的解析/类型检查并清理全部既有失败用例
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- parser:C 指针后缀(T* / T**)不再限定于类型注解上下文,调用实参同样识别
  (va_arg(ap, int64*) / short* / int*),a * b、a ** b、N * 2 等真实运算行为不变
- types:str | t.CPtr、bytes | t.CPtr 折叠为 CPtr[str] / CPtr[bytes](i8**,如 argv),
  与 TransPyC 的类型契约一致
- typeEvaluator:CPtr[str|bytes] 下标返回 pointee(str/bytes)而非 char,修 argparse.vp
  `token: str = entry[0]` 误报;新增 C 风格强转识别 Type(value, t.CPtr),修
  char(addr, t.CPtr) 在 memhub.vp / requests.vp 的误报(int("12", 16) 不受影响)
- flowChartCommand:补 AnalyzerService.run 的 CancellationToken 实参(TS2554),
  此前被该编译错误阻塞的 5 个测试套件恢复运行
- 测试维护:ord 断言改为 TransPyC 现行的 def ord(s: char) -> int;
  AssignGenMixin.vp 收窄守卫改为不写死行号,抗源码漂移
- 版本号提升至 1.1.423 并更新 CHANGELOG;全量 57 套件 / 758 用例全绿(0 失败)
2026-09-22 21:50:58 +08:00
TermiNexus 988ba922f4 refactor(vipeye-internal): str/bytes 迭代元素类型统一为 char
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
接上次下标统一:`for ch in s` 的迭代元素,以及 str/bytes 作为 Iterable/Iterator 实参
时的泛型推断(enumerate/zip/map/filter/reversed/sorted/list() 等)也由 t.CChar 别名
int 改为内置 char。至此 `s[i]`、`*s`、迭代三种取字符方式元素类型一致。
删除已无引用的 getViperCCharType 辅助函数。

回归用例:viperEnumerate.test.ts 断言改为 enumerate[char] / ch 为 char。
版本号提升至 1.1.422 并更新 CHANGELOG。
2026-09-22 21:28:50 +08:00
TermiNexus d2f66811fb refactor(vipeye-internal): str/bytes 下标统一返回 char(与 *s 解引用一致)
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
`s[i]` 原返回 t.CChar,而 `*s` 返回 char,同一 i8 元素出现两种显示。现把 str/bytes
的下标通路(普通下标 + TypeSet 下标)统一改为内置 char 对象,二者本就可隐式互转。
迭代(for ch in s)与 Iterable 实参推断仍保持 t.CChar,待确认是否一并统一。

回归用例:viperUnaryPointer.test.ts 中 fmt[0] 断言改为 char。
版本号提升至 1.1.421 并更新 CHANGELOG。
2026-09-22 21:26:16 +08:00
TermiNexus 1aa1b1f2f0 feat(vipeye-internal): str/bytes 指针算术与写入解引用,并补齐 short/ushort/ulong/ulonglong 内建 typedef
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- 指针算术:str/bytes(char*,i8*)的 `p + n` / `p - n` / `n + p` 返回指针本身,
  不再报 reportOperatorIssue(与增强赋值 `dest += n` 的处理对齐)
- 写入解引用:`*p = v` 走 getTypeOfViperDeref 的解引用类型(str/bytes → char),
  赋值目标校验的 Viper 通路上次已就位,本次补回归用例
- 内建 typedef 补齐 short/ushort/ulong/ulonglong(t.CShort/t.CUnsignedShort/
  t.CUnsignedLong/t.CUnsignedLongLong),与已有 long/longlong 同处
- 回归用例:viperUnaryPointer.test.ts 新增「指针算术与下标」用例、解引用读写道,
  viperBuiltinsVpTypedef.test.ts 扩展开 g() 参数断言
- 版本号提升至 1.1.420 并更新 CHANGELOG
2026-09-22 21:23:21 +08:00
TermiNexus 00b781a5cc fix(vipeye-internal): str/bytes 支持一元 '*' 解引用,结果为 char
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
Viper 里 str 与 bytes 都是 C 的 char*(i8*),`*s` 应取首字符且类型为 char(i8);
此前被当成非指针类型处理,报「类型 "str" 不支持运算符 "*"」(reportOperatorIssue)。
在 getTypeOfViperDeref 中把 str/bytes 与已有的 CPtr[X] 分支并列处理。

回归用例:viperUnaryPointer.test.ts 新增 *fmt(str)与 *buf(bytes)→ char,
并断言不报运算符错误。版本号提升至 1.1.419 并更新 CHANGELOG。
2026-09-22 21:01:18 +08:00
TermiNexus 4bc462e0f3 feat(vipeye-internal): long/longlong 也作为内置 C 基本类型 typedef
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
对齐编译器 t.py 的 _CNAME_ALIAS(char/short/long/unsigned 等是语言内置名称,
无需 import):在 builtins.vpi 内置 long → t.CLong、longlong → t.CLongLong。
C 的 "long long" 含空格不能作标识符,故 Viper 写作 longlong;无符号形式仍可用
t.CUnsignedLongLong。

回归用例补充 g(e: long, h: longlong) -> long 的注解类型断言(CLong/CLongLong)。
版本号提升至 1.1.418 并更新 CHANGELOG。
2026-09-22 20:39:52 +08:00
TermiNexus f547737cf0 fix(vipeye-internal): C 整型 typedef 内建到 builtins.vpi,修复 uint32 报未定义
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
TransPyC 的 includes/builtins.vp 提供 sizet/size_t/uzt/int8..uint64 以及
intfast*/uintfast*/intleast*/uintleast* 等 typedef。LSP 侧原先只靠 program.ts
的 _mergeViperBuiltinsVpSymbols 定位并合并该文件,而查找依赖工作区根与打开的
文件集合,在 Trae 等环境下找不到时这些注解会集体报"未定义"(同一工程在不同
编辑器/不同打开顺序下表现不一致)。

现把等价声明内建进 typeshed-fallback/stdlib/builtins.vpi:任意 Viper 工程无需
配置 includes 即可解析;工程自带 builtins.vp 时仍以工程声明为准(合并逻辑覆盖
同名符号,行为不变)。

新增回归用例:无 includes/builtins.vp 的工程可解析 uint32/sizet/int8/uint64
并得到 CUInt32T/CSizeT/CInt8T/CUInt64T。版本号提升至 1.1.417 并更新 CHANGELOG。
2026-09-22 19:30:55 +08:00
TermiNexus de2d49cc4a feat(vscode-vipeye): 新增「清理并临时关停」命令并修复字符串内括号着色
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- 新增命令 viper.cleanupAndDeactivate(Viper: Cleanup and Deactivate):终结本窗口的
  语言服务进程、释放全部已注册资源(命令/视图/状态栏/文件监视/流程图面板),可选
  清除全局语义高亮磁盘缓存(~/.vipeye/cache/semantic-tokens)。多窗口下服务端进程
  会 chdir 占住扩展目录(Windows 无法删除),磁盘缓存又跨窗口共享,导致卸载删不
  干净、其它实例读到半删残留且重启扩展宿主无效
- 修复字符串/注释/f-string 插值内的 []{}() 被编辑器内置括号配对着色误染:viper
  grammar 贡献补 balancedBracketScopes 与 unbalancedBracketScopes
- 语义高亮缓存路径改由 common/pathUtils 统一提供,服务端与客户端共用一份定义
- 版本号提升至 1.1.416 并更新 CHANGELOG
2026-09-22 18:23:01 +08:00
TermiNexus 86fc6c541f feat(vipeye-internal): [SIZE]T 支持泛型实参位置与嵌套
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- 下标实参以 '[' 开头时按 token 前瞻判定是否为数组类型:匹配的 ']' 之后
  紧跟元素类型(标识符或嵌套 '[')则是数组,故 `list[[4]int]` 可用,
  而 `t.Callable[[int, str], None]`(带逗号)与 `t.Callable[[], None]` 不受影响
- `[SIZE]T` 的元素类型内允许再次出现 `[SIZE]T`,支持 `list[[2][3]int]`
- 测试:新增与显式写法 `list[t.CArray[int, 4]]` 的类型等价断言及 Callable 回归
2026-09-22 16:21:32 +08:00
TermiNexus a7844d359f chore: 版本号提升至 1.1.415 并更新 CHANGELOG
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- [SIZE]T 数组类型语法(等价 t.CArray[T, SIZE]、[]T 不定长)
- 修复字符串转义引号导致的作用域泄漏
- 修复 builtins.vp typedef 合并被首个绑定文件环境"污染"
2026-09-22 16:13:54 +08:00
TermiNexus 91f668a033 fix(vscode-vipeye): 字符串转义引号导致 TextMate 作用域泄漏
字符串规则没有转义子规则,`'\''` / `"\""` 会被提前闭合:后续关键字
(如 or/def/return/pass)被渲染成字符串颜色。

- 新增 string-escape(constant.character.escape.viper)并让 4 条字符串规则 include
- 单行字符串 end 由引号改为「引号或行尾」,即使写错也不会把作用域拖到下一行
2026-09-22 16:13:49 +08:00
TermiNexus d9559869e7 feat(vipeye-internal): 新增 [SIZE]T 数组类型语法(脱糖为 t.CArray[T, SIZE])
- `[SIZE]T` 等价 t.CArray[T, SIZE],`[]T` 等价 t.CArray[T](不定长)
- 支持整数字面量 / t.CDefine 常量长度、多维嵌套 [2][3]T、指针元素 [4]T*
- 支持变量、参数、返回值、类成员与 type X = 注解位置
- 解析层脱糖:AST 与手写 t.CArray[T, SIZE] 完全一致,元素类型/索引返回 T/
  数组名退化为指针/字符数组降级 str·bytes 等语义零分叉
- 下标实参按源码位置排序(AST 子节点不重叠不变量),getTypeArgs 内按语义
  顺序重排实参;合成 t/CArray 名为零长度,不产生多余语义高亮
- 不在下标实参内生效,避免把 t.Callable[[], T] 误判为数组语法
- 源文件未 import t 时静默退化为 Unknown(不报未定义 t/成员访问失败)
- hover 与诊断按源码原形显示(如 buf: [4]t.CInt)
2026-09-22 16:13:43 +08:00
TermiNexus a6299ab878 fix(vipeye-internal): builtins.vp typedef 合并改为按执行环境+文件目录缓存
合并缓存原本只用 typeshed builtins 存根的 uri 作键(全工程共用一个),
且在查找 builtins.vp 之前就标记为已合并:若第一个被绑定的文件所在环境
找不到 includes/builtins.vp(工程外散落文件、多根工作区另一根等),
后续所有文件都会永久失去 typedef,导致 uint32/int8/sizet 等集体报
"未定义"(同一工程在 VSCode 与 Trae 因打开顺序不同而表现不一致)。

- 缓存键改为(typeshed 存根 + 执行环境 root/extraPaths + 被分析文件目录)
- 新增 _isMergingViperBuiltinsVp 递归保护,替代原先"先标记"顺带承担的防重入
- 新增回归测试:先绑定其它环境文件后,本工程 typedef 仍可解析
2026-09-22 16:13:26 +08:00
TermiNexus f100f585d0 chore: 版本号提升至 1.1.414 并更新 CHANGELOG
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
2026-09-21 23:13:45 +08:00
TermiNexus 525a654f5c feat(vipeye-internal): 支持 enum X: 枚举语法与 define 常量声明
- enum:`enum NAME:` 识别为 Viper 枚举(等价 class NAME(t.CEnum)),
  成员支持裸名与逗号分隔(enum X: a, b, c),并支持同行声明后换行 TAB 续写
- enum:取值按编译期规则推导(从 0 递增,中间显式赋值后从该值 +1 续推),
  hover 显示 (variable) IsAsync: ASTFlag = 0,成员可隐式转换为 int
- enum:成员推导值超出声明类型位宽时报错(如 i1 存不下 2)
- define:`define NAME: 类型 = 值` / `define NAME = 值` 解析为常量声明,
  `define f(...)` 宏函数行为不变
- 新增整数字面量位宽校验:字面量无法存入声明类型时提示扩容,
  define 与普通声明均生效(位宽表源自 wiki 02-type-system.md)
- 新增 viperEnumFlag.test.ts(5 项)与 viperDefineConst.test.ts(4 项)
2026-09-21 23:13:38 +08:00
TermiNexus 70a4405294 chore(viper): 选择列表首项去掉手动指定 exe 路径描述并修正过时注释
该入口已改为文件选择对话框,原描述与输入框取路径等注释不再贴切。
2026-09-20 18:56:16 +08:00
TermiNexus 95a0dbf0ad feat(viper): 选择列表加「选择编译器…」,关闭 VET 后清单与选中仍可用
- 选择列表首项为「选择编译器…」(文件选择对话框 → VET check → 并入清单并切换),其后一条分隔线才是 VET 搜到的环境
- 状态栏在 VET 关闭时显示 VET 已关闭,且点击仍打开选择列表(重启改走命令面板)
- EnvironmentManager 新增 AddEnvironment:check 通过的环境无需等下一轮快照即可选中
- 抽出 PickAndCheckExecutable,去掉 check 命令与该入口的重复实现
2026-09-20 18:54:09 +08:00
TermiNexus ad3fd9810c feat(viper): VET 启动参数加 --first-no-pace,首次扫描不节流
VET 常驻模式默认对扫盘节流(每 16 个目录项 / 64 个文件 Sleep 20ms),首次全盘搜索明显变慢,正是 Viper 搜索很慢的来源。带上该开关后首轮全速、后续轮次仍按 VET 自身节流跑。
2026-09-20 18:54:03 +08:00
TermiNexus 26216889de feat(viper): 新增手动指定 exe 的 VET check 检查
- 新增 viper.checkEnvironment(命令面板 + Environments 视图标题按钮):选择 exe 后经 check 通道校验 PE/.PRVERSN 与 --version 前缀
- 回执按回带的 id 配对;check_msg 九种原因(NOT_FOUND/NO_PRVERSN/TIMEOUT/CHANNEL_UNAVAILABLE 等)映射为可读说明
- stdin 写入改为入队后仅在心跳窗口内逐行送出(check 在前、心跳最后、行间 300ms、按窗口时长限量),规避看门狗对同批多行的读取行为
- 进程退出/停止时唤醒等待中的请求,避免调用方悬挂
2026-09-20 18:19:52 +08:00
TermiNexus 5d6fcf7c82 feat(viper): 接入 VET 环境发现,支持停止/查看输出与粘性选择
- 新增 EnvironmentTool / EnvironmentManager:常驻 scan 进程客户端(启动、心跳确认、快照解析、异常退出的指数退避重启)与全量快照 diff
- 侧边栏新增 Environments 视图(按 source 分组、选中项打勾);状态栏只保留 VET 环境项,移除硬编码 C:\v.exe
- 默认复用上次使用的环境 uuid,VET 未给出该 uuid 时取发现的第一个,一个都没发现时回退配置/兜底路径
- VET 的 stdout 与 stderr 全量转发到 Vipeye VET 输出通道,彻底移除日志文件落盘
- 新增 Stop VET / Restart VET / Show VET Output / Select Environment 命令,并补回被回写覆盖丢失的 contributes
2026-09-20 18:19:18 +08:00
TermiNexus b0f4b8b5fd chore(vipeye): 同步 package-lock 中的包版本号到 1.1.413 2026-09-19 17:59:23 +08:00
TermiNexus 640d7e4a07 feat(viper): 新增函数跳转图(Ctrl+Alt+G,面板渲染 Mermaid,可拖动缩放并导出 PNG)
- vipeye-internal/flowChartBuilder:AST -> 语句级控制流图(基本块合并)输出 Mermaid,
  覆盖 if/elif/else、for/for-else、while/while-else、match-case、try/except/else/finally、
  break/continue/return/raise、with;嵌套 def/class 折叠为单节点
- finally 穿透:return/raise/break/continue 与未匹配异常都先经过 finally 再继续,
  嵌套 finally 由 _routeThroughFinally 按最内层到最外层串联,并对边去重
- vipeye-internal/flowChartCommand:新增 viper.showFunctionFlowChart 命令,按 offset
  取最内层函数,返回 Mermaid 与「节点 -> 行号」映射
- vscode-vipeye:底部停靠面板 WebviewView 渲染 mermaid(外部脚本 media/flowChartWebview.js,
  数据经 postMessage 下发),支持拖拽平移(指针锁定无限拖 + 视口内保留可见图块)、
  滚轮缩放、适应宽度/重置、点击节点跳转源码行
- 导出 PNG:倍率/宽高/裁剪空白边距/透明或白色背景 + 实时预计尺寸,
  经 data: URL -> canvas 光栅化(导出前用 htmlLabels:false 重渲染,避开 foreignObject)
- 入口:快捷键 Ctrl+Alt+G、编辑器右键菜单、命令面板
- 依赖与构建:新增 mermaid 依赖,rspack 拷贝 mermaid 预构建产物与面板脚本到 dist/media
- 新增 9 个用例覆盖上述语句结构与 finally 穿透
- 同步 package-lock 中的版本号与 mermaid 依赖
2026-09-19 17:58:59 +08:00
TermiNexus cfccbac6f4 feat(vipeye-internal): 类型展示保留源码书写的完整 module 路径
- typePrinter 新增 ViperSourceQualifiedNames(1<<15),打印类名时优先取
  ClassDetailsPriv.displayName(源码里实际书写的 ast.AST 形式),未写限定路径的
  裸名(list/str)保持短名;UniqueNameMap 与打印共用 getPrintableClassName,
  避免名字唯一性判定与实际打印名不一致导致误降级
- types 新增 displayName 字段与 cloneForDisplayName(仅用于展示,不参与类型判定)
- typeEvaluator 在成员访问处记录点号路径(getDottedNamePath),仅 Viper 方言生效
- hoverProvider / tooltipUtils 透传 PrintTypeOptions.sourceQualifiedNames
- 修正 hover 与补全的代码围栏注释:```viper 实际可着色,但只有通用 scope 能命中
  主题规则(def/class 的 storage.type.*.viper 与按 Python 精调的主题规则不匹配),
  故继续使用 ```python
- 同步 viperUnaryPointer / viperFunctionHover 用例的展示断言
2026-09-19 17:58:49 +08:00
TermiNexus c9acd4ecb8 chore(vipeye-internal): 修剪 samples 残留与孤儿测试辅助
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- samples 根目录 1277 个散落 .py/.pyi 夹具删除,仅保留被现存用例引用的 15 个
  (badToken1/test_file1/lines1/parser1-2/suiteExpectedColon1-3/maxParseDepth1-2/
   parserRecovery1-3/fstring6/finallyExit1)+ 2 个 viper_*.vp;子目录夹具不动
- 删除已无引用的 harness/fourslash/runner.ts(fourslash 夹具与 testState.test 已删)
- 保留 tests/lsp/languageServerTestUtils.ts:仍被 typeServer.virtualFileRedirect 引用

验证: 全量串行 53 套件通过、1 跳过,732 用例全绿
2026-09-18 23:10:41 +08:00
TermiNexus 04d5a32502 test(vipeye-internal): 删除剩余 18 个内联 .py 夹具套件,测试集转为 Viper-only
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- 删除 completions/signatureHelp/diagnostics/service/languageServer/importResolver(+Support)/
  importStatementUtils/documentSymbolCollector/chainedSourceFiles/sourceMapperUtils/workspaceEditUtils/
  testState/classDeclaration/wildcardImportPackageMerge/privateImportUsage/typeServer 等 18 个套件
- 保留并加注 tests/fourslash/typings/fourslash.d.ts:其 declare namespace _ 仍被
  harness/fourslash/testState.ts 引用,删除会导致 ts-jest TS2694 整批套件无法运行

验证: 全量串行 53 套件通过、1 跳过,732 用例全绿(0 失败)
2026-09-18 23:05:17 +08:00
TermiNexus a955a5356f test(vipeye-internal): 清理 Python 时代测试资产并修复陈旧期望
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- 删除 fourslash 夹具(258 个 .py 用例)、fourSlashRunner、hoverProvider、typeEvaluator1-8、checker(共 276 个 pyright Python 语义用例文件)
- config/sourceFile: reportPrivateImportUsage 基本默认值期望改为 Viper 的 'none'
- config/uri/filesystem/diagnosticOverrides/localizer: 期望改为 .vp/.vpi 扩展、viper.analysis.* 命名空间与 locale 无关断言
- samples: project1/4、venv_auto_detect_*、extended_config 夹具 .py/.pyi 更名为 .vp/.vpi

验证: 相关 32 个套件 343 用例全绿
2026-09-18 22:50:03 +08:00
TermiNexus e4635eca37 fix(vipeye-internal): 恢复 py.typed 探测,修复 PartialStub 映射失效导致的 viperFileSystem 5 个失败
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- pyTypedUtils: 去掉按方言短路(IsViperDialect 恒为 true 使探测恒返回 undefined)
- partialStubService: 虚拟映射过滤器补回 .pyi(存根后缀支持 .vpi/.vpnci/.pyi)
- launch.json: 调试宿主同时禁用已安装的 TermiNexus.vipeye(真实扩展 ID)
2026-09-18 22:17:45 +08:00
TermiNexus d5398ab4e2 fix(vipeye-internal): 无类型参数的别名被下标特化时保留类型实参显示
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
- typePrinter: rawptr[T] 不再退化为裸 rawptr,修复 Ty: rawptr[LLVMType] 成员访问丢参
- parser: 补充注解内 C 指针后缀 list[ast.AST*]* 的回归用例
- tests: viperTyNarrowDump 行号刷新/断言收窄到 rhs_val.Ty,修复 viperVariadicArgs 与 parser.test 既有失败
- vscode-vipeye: 调试模式下发布版自动让路给开发版,避免 command 'viper.*' already exists
- docs(changelog): 补充 rawptr[T] 类型实参显示修复条目
2026-09-18 21:59:57 +08:00
TermiNexus 86e858667e 修改了 README
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
2026-09-18 21:49:07 +08:00
TermiNexus d9b5015c71 "feat(vipeye-internal): 新增内置 char 类型(复刻 int,8 位字符/整型)feat(vipeye-internal): rowptr 更名为 rawptr(编译器内置裸指针 typedef)feat(vipeye-internal): None 返回函数仅允许裸 return/return Nonefix(vipeye-internal): builtins.vp 发现扩展到文件祖先目录与 includes 子目录chore: 版本号提升至 1.1.413 并更新 CHANGELOG"
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
2026-09-16 11:51:48 +08:00
TermiNexus aef18c2891 更新了一些事项,feat(vipeye-internal): builtins.vpi 声明编译器内置 typedef rowptr = t.CPtr;feat(vipeye-internal): None 返回函数仅允许裸 return/return None,return 值为 None 的表达式报错;fix(vipeye-internal): builtins.vp 发现扩展到文件祖先目录与 includes 子目录,修复 uint64 等 typedef 未定义
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
2026-09-10 16:32:23 +08:00
TermiNexus 23cc75e444 feat(vipeye-internal): 新增内置 double 类型(复刻 float,64 位浮点)
- typeshed builtins.vpi 增加 double 类定义(复刻 float 结构)
- 内置 float<->double 按 C 浮点规则互转,int->double 无损
- 内置 double 与 t.CDouble 双向隐式转换
- viperGenericTypeVarCall 12/12 测试通过
2026-08-23 13:16:46 +08:00
TermiNexus ac08b01baa feat(vipeye-internal): extern/export 支持直接声明形式 extern A(...)
- extern/export 后跟标识符时按函数声明解析(等价 extern def A),
  def 形式保留(extern def A / export def A 均可用)
- extern/export 声明均可省略函数体(无冒号无 pass)
- checker 对 extern/export 声明跳过缺失返回值检查
- TextMate 高亮与缩进规则支持 extern/export 直接声明
- viperGenericTypeVarCall 11/11 测试通过
2026-08-17 10:35:46 +08:00
TermiNexus 41bc302916 feat(vipeye-internal): Viper len 优先调用 __len__,结构体缺 __len__ 报错
- len 对用户自定义结构体优先解析 __len__ 返回类型,无 __len__ 报错
- t.CType 原生类型(C 标量/指针/str/bytes)豁免,返回 t.SizeT
- 新增本地化消息 viperLenNoLenMethod
- viperGenericTypeVarCall 10/10 测试通过
2026-08-17 10:32:19 +08:00
TermiNexus bbd8b1022e feat(vipeye-internal): union/fn 别名/extern 无函数体及 builtins 6 函数签名
- union 与 enum/rnum 对齐(union class / union A: 单独转 class)
- fn/fun/func/function 行尾单独使用按 def 处理并报缺函数名,
  后跟 :/=/ 保持标识符,避免误伤 func: 注解、func = x、func(x)
- extern def/fn 声明可省略函数体(无冒号无 pass),: pass 方式保留
- TextMate 高亮与 language-configuration 缩进规则同步(含 inline fn 修饰关键字)
- typeshed builtins.vpi 新增 Asm/LLVMIR/AsmInp/AsmOut/LInp/LOut,
  隐式内置直接可用(签名照抄 vipershed c.vpi)
- viperGenericTypeVarCall 9/9、tokenizer 91 测试通过
2026-08-08 18:31:25 +08:00
TermiNexus 817bc47ee9 feat(vipeye-internal): extern 函数体必须为 pass/None;fn/fun/func/function 作为 def 别名
extern 关键字声明的函数(FunctionNode 记录 viperModifier)函数体只允许 pass 或 None,否则报错;同时 extern 声明函数跳过缺失返回值的检查。fn/fun/func/function 在语句起始(或紧跟修饰关键字)且后跟函数名时按 def 处理。tmLanguage 与 language-configuration 同步支持 def 别名高亮与自动缩进。
2026-08-08 17:29:21 +08:00
TermiNexus e5aacbfd0b feat(vipeye-internal): 修饰关键字收窄为 inline/export/extern,单独使用报错;修复修饰函数自动缩进
state 不再作为关键字(恢复普通标识符,hashlib 的 state: 字段与 self.state 不受影响)。inline/export/extern 在语句起始且未与 def 搭配时报 '修饰关键字必须与 def 搭配使用'(新增 KeywordType.ViperModifier 与 LocMessage);后跟 ':' 的注解用法仍按标识符。tmLanguage 高亮与 language-configuration 的 onEnterRules/increaseIndentPattern 同步(修饰前缀函数回车自动缩进)。
2026-08-08 17:15:10 +08:00
TermiNexus 9c816ec5b9 fix(vipeye-internal): enum/rnum 支持单独使用(enum A: 等价 class A:)
此前只支持 enum/rnum 后跟 class/struct 的前缀形式;用户实际写法为 num A: 直接声明。补充:enum/rnum 在语句起始且后跟类名时按 class 关键字处理;tmLanguage 高亮同步。回归测试新增 enum A:/rnum B: 用例。
2026-08-08 17:01:29 +08:00
TermiNexus 4182a1fd53 feat: 支持 inline/export/extern/state/enum/rnum/struct 关键字
tokenizer 层支持 C 风格修饰/声明关键字:inline/export/extern/state 后跟 def、enum/rnum 后跟 class/struct 时跳过该标识符 token(保留位置与缩进);struct 在语句起始且后跟类名时按 class 关键字处理(避免误伤 import struct as s 等合法用法)。viper.tmLanguage.json 新增对应关键字高亮。新增回归测试(struct 等价 class、修饰 def 等价普通函数、rnum class 嵌套类)。
2026-08-08 16:48:35 +08:00
TermiNexus f4e19ed72a fix(vipeye-internal): c.Deref 对结构体联合类型返回具体类型
c.Deref(p) 对 IntPair | t.CPtr 这类 TypeSet 联合返回用户自定义结构体类实例(CPtr[Po] 返回 Po),使 c.Deref(p).a 解析为 CInt,不再显示 Unknown;C 标量(t.CInt 等)、内置 str/bytes(i8*)、裸指针保持 Unknown,避免 memcpy(c.Deref(out), ...) 等把解引用结果当指针传递的代码引入误报。新增负向回归测试。
2026-08-08 13:15:43 +08:00
TermiNexus ad3eecb077 fix(vipeye-internal): 泛型函数显式类型实参调用不再误报不可下标
PEP 695 风格 fn[T] 的显式类型实参 fn[t.CInt] 在 Viper 方言下直接忽略并按普通函数求值,避免把函数误报为不可下标。新增 double[t.CInt](21) 回归测试;临时测试目录改到工作区内以适配沙箱写权限限制。
2026-08-08 13:01:17 +08:00
TermiNexus a20c643b72 fix(vipeye): 递归发现嵌套子项目配置,修复 G4 等两级子目录导入失败
findViperProjectConfigsInChildDirs 原仅扫描一级子目录,TransPyC/Test/G4 等两级子项目的 project.json 不被发现,其 includes 未加入导入根,导致 Import g4node could not be resolved。

改为递归扫描:深度限制 6,跳过 .git/node_modules/__pycache__ 等常见非项目目录;直接子目录配置优先收集(保持主配置优先级不变),非项目目录再递归深入。
2026-08-08 11:53:53 +08:00
TermiNexus 2b752a7aec fix(vipeye): 修复 REnum 变体字段语义高亮为 property
REnum 变体字段(如 LLVMType.Ptr 的 Pointee)常规声明查找找不到,语义 token 误显示 undefined。根因:描述符协议在 Viper 结构体字段上失效,CPtr 等 C 类型类的 __get__ 查找返回 Unknown 而非 undefined,把字段类型(Value.Ty)污染为 Unknown,导致 x.Ty.Pointee 无法解析。

- typeEvaluator: applyDescriptorAccessMethod 的 isAnyOrUnknown 分支在 Viper 方言下返回 memberType 保留原始字段类型;新增 REnum 变体字段查找辅助(getViperREnumVariantFieldInfo)覆盖诊断与符号查找
- semanticTokenProvider: 空 declInfo 成员访问时用 getTypeOfExpression 求值,非 Unknown/Unbound 渲染为 property
2026-08-08 11:07:37 +08:00
TermiNexus 5e07800c7c fix(vipeye): 支持 project.json 子项目 source_dir 全局导入
findViperProjectConfigsInChildDirs 扫描一级子目录的 .vpj/project.json,
累计 source_dir 与 includes 到 defaultExtraPaths(去重而非覆盖),
使 App 根级 import lib.core.VLogger 可在 TransPyC 目录打开时正确解析。
2026-08-08 11:06:51 +08:00
1728 changed files with 23354 additions and 95324 deletions

No files matched your search

+37
View File
@@ -135,6 +135,43 @@ AGENTS.md
# Generated benchmark output
packages/vipeye-internal/src/tests/benchmarks/.generated/
# 测试运行生成的产物(每次跑 jest 都会被重写,不入库)
packages/vipeye-internal/src/tests/__ty_narrow_dump.txt
packages/vipeye-internal/src/tests/__unary_ptr__/
packages/vipeye-internal/src/tests/__global_redeclare__/
packages/vipeye-internal/src/tests/__viper_attribute__/
packages/vipeye-internal/src/tests/__viper_attribute_completion__/
packages/vipeye-internal/src/tests/__viper_attribute_hover__/
packages/vipeye-internal/src/tests/__viper_attribute_prefix__/
packages/vipeye-internal/src/tests/__viper_attribute_noimport__/
packages/vipeye-internal/src/tests/__viper_attribute_implicit_scope__/
packages/vipeye-internal/src/tests/__viper_attribute_unknown__/
packages/vipeye-internal/src/tests/__viper_ptr_unary__/
packages/vipeye-internal/src/tests/__viper_ptr_convert__/
packages/vipeye-internal/src/tests/__viper_ptr_hierarchy__/
packages/vipeye-internal/src/tests/__viper_vtable_assign_ok__/
packages/vipeye-internal/src/tests/__viper_vtable_assign_bad__/
packages/vipeye-internal/src/tests/__viper_vtable_assign_plain__/
packages/vipeye-internal/src/tests/__viper_volatile__/
packages/vipeye-internal/src/tests/__viper_volatile_bad__/
packages/vipeye-internal/src/tests/__viper_callable__/
packages/vipeye-internal/src/tests/__viper_static__/
packages/vipeye-internal/src/tests/__viper_bracket_array_expr__/
packages/vipeye-internal/src/tests/__viper_type_binop_bound__/
packages/vipeye-internal/src/tests/__viper_const_ptr_deref__/
packages/vipeye-internal/src/tests/__viper_const_ptr_index__/
packages/vipeye-internal/src/tests/__viper_const_ptr_iter__/
packages/vipeye-internal/src/tests/__viper_variadic_noimport__/
packages/vipeye-internal/src/tests/__viper_carray_element__/
packages/vipeye-internal/src/tests/__viper_bracket_array_no_import_t__/
packages/vipeye-internal/src/tests/__viper_export_extern__/
packages/vipeye-internal/src/tests/__viper_renum_pointer__/
packages/vipeye-internal/src/tests/__viper_renum_pointer_matrix__/
packages/vipeye-internal/src/tests/__viper_generic_bound__/
packages/vipeye-internal/src/tests/__viper_generic_bound_none__/
packages/vipeye-internal/src/tests/__viper_match_break_fallthrough__/
packages/vipeye-internal/src/tests/__viper_match_break_fallthrough_neg__/
# perfCompare.py cached commit bundles
/build/perfCompare/
+4 -2
View File
@@ -41,7 +41,8 @@
// / "Cannot register 'viper.xxx' ... already registered" 激活失败。
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-vipeye",
// 禁用已安装的发布版,避免与调试版同时加载冲突。
"--disable-extension=TermiNexus.vscode-vipeye"
"--disable-extension=TermiNexus.vscode-vipeye",
"--disable-extension=TermiNexus.vipeye",
],
"smartStep": true,
"sourceMaps": true,
@@ -59,7 +60,8 @@
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-vipeye",
"--disable-extension=TermiNexus.vscode-vipeye",
"--disable-extension=be5invis.vscode-custom-css",
"--disable-extension=msgcenterext.msgcenter"
"--disable-extension=msgcenterext.msgcenter",
"--disable-extension=TermiNexus.vipeye",
],
"smartStep": true,
"sourceMaps": true,
+69 -5
View File
@@ -1,10 +1,74 @@
import w32.win32console as w32cmd
import stdio
import t, c
def add(a: int, b: int) -> int:
return a + b
pagecode: t.CDefine = 65001
def main() -> int:
stdio.printf("Hello, World!")
return 0
class TestStruct():
a: t.CInt
b: t.CInt
class Obj():
val: t.CInt
export def main() -> int:
w32cmd.SetConsoleOutputCP(pagecode)
w32cmd.SetConsoleCP(pagecode)
print("=== Unary * / & (Deref/AddrOf) Test ===")
# --- Test 1: 标量 & 取地址 + * 解引用 ---
x: int = 42
px: int | t.CPtr = &x
d: int = *px
print(f"T1: x={x} *(&x)={d} (expect 42, 42)")
if d == 42:
print("T1 OK")
else:
print("T1 FAIL")
# --- Test 2: 通过指针写回(c.DerefAs 兼容形式) ---
c.DerefAs(px, 100)
print(f"T2: after c.DerefAs(px, 100) x={x} (expect 100)")
if x == 100:
print("T2 OK")
else:
print("T2 FAIL")
# --- Test 3: 数组元素地址 &arr[i] + * 解引用 ---
a: t.CArray[t.CInt, 4]
a[0] = 7
pa: t.CInt | t.CPtr = &a[0]
v3: int = *pa
print(f"T3: a[0]={a[0]} *&a[0]={v3} (expect 7, 7)")
if v3 == 7:
print("T3 OK")
else:
print("T3 FAIL")
# --- Test 4: 结构体字段地址 &s.f + * 解引用 ---
s: TestStruct
s.a = 10
pf: t.CInt | t.CPtr = &s.a
v4: int = *pf
print(f"T4: s.a={s.a} *&s.a={v4} (expect 10, 10)")
if v4 == 10:
print("T4 OK")
else:
print("T4 FAIL")
# --- Test 5: &obj 对象指针(对象变量即指针) ---
o: Obj
o.val = 99
po: Obj | t.CPtr = &o
print(f"T5: o.val={o.val} &o->val={po.val} (expect 99, 99)")
if po.val == 99:
print("T5 OK")
else:
print("T5 FAIL")
print("=== Unary * / & Test Done ===")
return 0
+70 -10
View File
@@ -1,10 +1,52 @@
![Vipeye](docs/img/PyrightLarge.png)
![Vipeye](Viper.png)
# Static Type Checker for Viper
# Vipeye — Static Type Checker and Language Server for Viper
Vipeye is a full-featured, standards-based static type checker for Viper, written in TypeScript. It is designed for high performance and can be used with large Viper source bases.
Vipeye is a full-featured, standards-based static type checker and language server for **Viper**, written in TypeScript. It is designed for high performance and can be used with large Viper source bases.
Vipeye includes both a [command-line tool](docs/command-line.md) and an [extension for Visual Studio Code](packages/vscode-vipeye). It is built on the Pyright codebase, adapted from the ground up to Viper semantics (AOT compilation, C-style types, pointer and memory semantics), and no longer type-checks Python.
Vipeye ships in three forms:
- a **language server** (`vipeye-langserver`) speaking LSP over stdin/stdout;
- a **command-line type checker** (`vipeye`);
- an **extension for Visual Studio Code** (`vscode-vipeye`), which bundles both the LSP client and the Viper core UI (project / toolchain).
It is built on the Pyright codebase, adapted from the ground up to Viper semantics (AOT compilation, C-style type system, pointer and memory semantics). Python analysis is no longer supported — only the Viper dialect is checked.
## What is Viper
Viper is an ahead-of-time (AOT) compiled language whose syntax is modeled on CPython but whose semantics are entirely different:
- **TPC** — the seed compiler, itself based on CPython, used to bootstrap the toolchain.
- **TPV** — the first self-hosted compiler, written in Viper. Once `tpv --lsp` lands, it will replace the Pyright-derived analyzer in this repository.
Vipeye exists to provide type checking, diagnostics and editor intelligence for `.vp` / `.vpi` sources while TPV's own language server is under development.
## Capabilities
### Viper language semantics
- C-style type system: `t.CPtr`, `t.CArray`, `t.CVoid`, `t.CTypedef`, `t.CDefine`, `t.CEnum`, `t.REnum`, plus the compiler built-ins `char`, `double` and `rawptr` (a raw-pointer typedef).
- Pointer and memory semantics: `CPtr[T]` / `rawptr[T]`, `c.Deref`, address-of, pointer arithmetic and indexing; a `__new__` returning a raw pointer is auto-wrapped into `CPtr[T]`.
- C declaration modifiers (`t.CInline`, `t.CStatic`, `t.CConst` …) are filtered out during type merging — only the underlying data type is kept.
- Declaration modifiers on functions, with `fn` / `fun` / `func` / `function` as aliases of `def`.
- Declaration and annotation modifiers: `const` / `volatile` type descriptors (`a: const int`, `p: const t.CPtr`), `static` (C internal linkage — a static symbol cannot be used from another module), `export` / `extern` variables (`extern` takes no initializer), `virtual` / `novirtual` classes and methods, and `__attribute__(...)` in both the decorator and the annotation-modifier spelling.
- `[SIZE]T` fixed-length array types (plus `[]T` and `typedef T1 = T2`), and C-style casts in every spelling TransPyC accepts: `(T)x`, `(T*)x`, `(T*)(x)` and `T*(x)`.
- A `virtual class` method slot can be filled with a plain function (`obj.method = func`), matching how the compiler writes the vtable slot: the assigned function's `self` is a generic pointer (`rawptr` / `t.CPtr`) or a pointer to that class / one of its base classes, and the rest of the signature must match (`*args` / `**kwargs` may differ).
- `struct` / `union` / `enum` / `rnum`, including nested `t.REnum` variants destructured by field order.
- Variadic `*args` bound as `va_list` in the function body (`va_arg`, and pointer passthrough such as `vsnprintf(buf, size, fmt, args)`).
- Implicit conversion rules: integer/float convert in all directions (safe narrowing, lossless widening); integer → float requires float width ≥ integer width; float → integer is rejected; pointers convert only when the level matches **and** the pointee is compatible — either side being `void*`, identical pointees, and basic (numeric) pointees which follow the pointee's own conversion rules — while struct pointers must be the same type or an **upcast** (subclass → base class); different levels and unrelated structs are rejected; explicit casts are always allowed.
- Global/local variable rules: reading a global never errors; assigning an undeclared global without `global` does; a local redeclaration shadows the global; and so on.
- Type representations: `bool` / `t.CBool` map to `i1`; `str` / `bytes` are `i8*`, while `str | t.CPtr` / `bytes | t.CPtr` are `i8**`.
- PEP 695 syntax is pre-processed for Viper: type parameter lists are stripped and unknown `T | t.CPtr` unions are folded to `i8*`.
- Fail-fast analysis: missing type information is reported as an error instead of silently degrading to `i8*` / `void` / `i32`.
### Editor features
- Diagnostics with human-readable messages, plus Viper-tuned semantic highlighting (keywords, storage/type modifiers, decorators, `t.CDefine` constants, `__attribute__(...)` attribute names, and undefined symbols rendered in white).
- Completion, hover, go-to-definition, find references, rename, document symbols, and Organize Imports. Inside `__attribute__(...)`, both completion and hover list the available attribute names (including ones the compiler supports but that the `attr` stub does not declare).
- Activity-bar UI (Project / Toolchain / Settings), `.vpj` project files with a dedicated custom editor, and a status-bar compiler indicator.
- Analysis progress in the status bar (the file being analyzed, `N / M`, percentage) while the workspace is being checked, and a **Count Project LOC** report (KPIs, per-language chart, detail table) for `.vpj` projects.
- Import resolution over `VIPER_HOME` (stdlib), `viperconfig.json` extra paths, the workspace root, and recursively discovered child project configurations.
## Packages
@@ -14,16 +56,34 @@ Vipeye includes both a [command-line tool](docs/command-line.md) and an [extensi
- **`packages/vscode-vipeye`** — VS Code extension client (Vipeye LSP), with the Viper core source (`packages/viper`) bundled at build time.
- **`packages/viper`** — Viper core source library: project (`.vpj`) management, toolchain, and the activity bar UI (Project / Toolchain / Settings).
## Key Differences from Pyright
## Relationship to Pyright
- Viper dialect: `viperconfig.json` instead of `pyrightconfig.json`; config namespace `viper.*`.
- Viper stdlib stubs bundled in `vipeye-internal/vipershed-fallback`.
- Type semantics follow Viper rules: implicit conversions, pointer (`CPtr`/`CArray`), `t.CTypedef`/`t.CDefine`/`t.CEnum`, `T | t.CPtr` pointer folding, global-variable access rules, and more.
- Semantic highlighting and diagnostics are tailored to Viper syntax.
- Viper dialect: `viperconfig.json` instead of `pyrightconfig.json`; configuration namespace `viper.*` instead of `python.analysis.*` / `pyright.*`.
- Viper stdlib stubs bundled in `packages/vipeye-internal/vipershed-fallback`.
- Type semantics follow Viper rules: implicit conversions, pointer types (`CPtr` / `CArray`), `t.CTypedef` / `t.CDefine` / `t.CEnum` / `t.REnum`, `T | t.CPtr` pointer folding, global-variable access rules, and more.
- Diagnostics and semantic highlighting are tailored to Viper syntax.
## Building and Testing
See [docs/build-debug.md](docs/build-debug.md).
```bash
# Install all packages (from repo root)
npm install
# Build the core library
cd packages/vipeye-internal && npm run build
# Run all tests (rebuilds the test server first)
cd packages/vipeye-internal && npm test
# Run tests without rebuilding the test server (faster iteration)
cd packages/vipeye-internal && npm run test:norebuild
# Build the CLI / VS Code extension bundles (from repo root)
npm run build:cli:dev
npm run build:extension:dev
```
See [docs/build-debug.md](docs/build-debug.md) for details.
## Documentation
+119 -1
View File
@@ -77,6 +77,10 @@
"id": "viper.projectView",
"name": "Project"
},
{
"id": "viper.environmentView",
"name": "Environments"
},
{
"id": "viper.toolchainView",
"name": "Toolchain"
@@ -118,6 +122,36 @@
"title": "Open Settings",
"category": "Viper"
},
{
"command": "viper.selectEnvironment",
"title": "Select Environment",
"category": "Viper",
"icon": "$(server-environment)"
},
{
"command": "viper.restartEnvironmentTool",
"title": "Restart VET",
"category": "Viper",
"icon": "$(debug-restart)"
},
{
"command": "viper.stopEnvironmentTool",
"title": "Stop VET",
"category": "Viper",
"icon": "$(debug-stop)"
},
{
"command": "viper.showEnvironmentLog",
"title": "Show VET Output",
"category": "Viper",
"icon": "$(output)"
},
{
"command": "viper.checkEnvironment",
"title": "Check Viper Executable...",
"category": "Viper",
"icon": "$(check)"
},
{
"command": "viper.refreshView",
"title": "Refresh",
@@ -128,13 +162,37 @@
"command": "viper.openVpjAsJson",
"title": "Open .vpj as JSON",
"category": "Viper"
},
{
"command": "viper.countLines",
"title": "Count Project LOC",
"category": "Viper"
}
],
"menus": {
"commandPalette": [
{
"command": "viper.selectEnvironment"
},
{
"command": "viper.restartEnvironmentTool"
},
{
"command": "viper.stopEnvironmentTool"
},
{
"command": "viper.showEnvironmentLog"
},
{
"command": "viper.checkEnvironment"
},
{
"command": "viper.openVpjAsJson",
"when": "resourceExtname == .vpj"
},
{
"command": "viper.countLines",
"when": "resourceExtname == .vpj"
}
],
"editor/title/context": [
@@ -149,12 +207,22 @@
"command": "viper.openVpjAsJson",
"when": "resourceExtname == .vpj",
"group": "1_open"
},
{
"command": "viper.countLines",
"when": "resourceExtname == .vpj",
"group": "1_open@2"
}
],
"view/title": [
{
"command": "viper.checkEnvironment",
"when": "view == viper.environmentView",
"group": "navigation"
},
{
"command": "viper.refreshView",
"when": "view == viper.projectView || view == viper.toolchainView || view == viper.settingsView",
"when": "view == viper.projectView || view == viper.environmentView || view == viper.toolchainView || view == viper.settingsView",
"group": "navigation"
}
]
@@ -162,6 +230,56 @@
"configuration": {
"title": "Viper",
"properties": {
"viper.environmentTool.enabled": {
"type": "boolean",
"default": true,
"description": "启用 VET(Viper Environment Tool)常驻扫盘发现 Viper 编译器。",
"scope": "window"
},
"viper.environmentTool.path": {
"type": "string",
"default": "D:\\Users\\TermiNexus\\Desktop\\TransPyC\\VET\\.tpv_build\\output\\VET.exe",
"description": "VET 可执行文件路径(Viper 自身实现的环境发现工具)。",
"scope": "window"
},
"viper.environmentTool.scanPaths": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "传给 VET 的 --path 扫描目录。留空则让 VET 自行建立常用目录表;一旦填写则完全替换它(不再自动建表)。",
"scope": "window"
},
"viper.environmentTool.confirmSeconds": {
"type": "number",
"default": 10,
"minimum": 0,
"description": "VET 心跳确认周期(秒,对应 --need-confirm);0 表示关闭确认(不建议:关闭后 VET 不会因超时自杀)。",
"scope": "window"
},
"viper.environmentTool.confirmTimeoutMs": {
"type": "number",
"default": 5000,
"minimum": 0,
"description": "VET 每轮确认窗口长度(毫秒,对应 --need-confirm-timeout);窗口内没收到 {\"ok\":1} 就会退出。",
"scope": "window"
},
"viper.environmentTool.scanIntervalMs": {
"type": "number",
"default": 10000,
"minimum": 0,
"maximum": 86400000,
"description": "VET 每轮扫盘间隔(毫秒,对应 --sleep)。",
"scope": "window"
},
"viper.environmentTool.maxRestarts": {
"type": "number",
"default": 5,
"minimum": 0,
"description": "VET 异常退出后的自动重启次数上限(指数退避,单次最多等 30 秒);超过后通知错误并停止重启。",
"scope": "window"
},
"viper.toolchain.path": {
"type": "string",
"default": "C:\\v.exe",
+62
View File
@@ -3,12 +3,19 @@
* 导出给其他扩展(如 vscode-viper-lsp)使用的 ViperApi 实现。
*/
import * as fs from 'fs';
import * as path from 'path';
import { Event } from 'vscode';
import { ProjectManager } from './projectManager';
import { ToolchainManager } from './toolchainManager';
import { ToolchainInfo, ViperApi, ViperProject } from './types';
/** 自动 include 查找:每一层尝试的目录名(对齐 TransPyV 的 `./includes` 与 `../include` 两种约定)。 */
const CompilerIncludeDirNames = ['includes', 'include'];
/** 自动 include 查找:从编译器 exe 所在目录向上最多再找几层。 */
const CompilerIncludeMaxUpLevels = 4;
export class ViperApiImpl implements ViperApi {
constructor(
private readonly projects: ProjectManager,
@@ -37,6 +44,10 @@ export class ViperApiImpl implements ViperApi {
return this.toolchain.GetToolchain();
}
GetCompilerIncludePaths(): string[] {
return FindCompilerIncludePaths(this.toolchain.GetToolchain().Path);
}
GetTargetTriple(workspaceFolder?: string): string | undefined {
return this.projects.GetProject(workspaceFolder)?.Target.Triple;
}
@@ -49,3 +60,54 @@ export class ViperApiImpl implements ViperApi {
return this.toolchain.OnDidChangeToolchain;
}
}
/**
* 自动发现编译器自带的 includes 目录(工程未在 .vpj / project.json 声明 includes 时的兜底)。
*
* 基准是**当前选中编译器的 exe 所在目录**(不是被分析的 .vp 所在目录):
* 从 exe 所在目录起,向上最多再找 {@link CompilerIncludeMaxUpLevels} 层,每层只看**直接子目录**
* `includes` / `include`(不递归),返回其中真实存在的目录(绝对路径)。
* 例:`TransPyC/exe/TransPyV.exe` → 命中 `TransPyC/includes`(对齐 project.vpj 里的 `"../includes"`)。
*
* 只认绝对路径:相对路径(如默认占位 `C:\v.exe` 之外的相对写法)会以扩展宿主 CWD 为基准,
* 可能误命中无关目录,故直接放弃。目录不存在时静默跳过(非 Viper 工具链下整体为空操作)。
*/
function FindCompilerIncludePaths(exePath: string): string[] {
const found: string[] = [];
if (!exePath || !path.isAbsolute(exePath)) {
return found;
}
const seen = new Set<string>();
let dir = path.dirname(exePath);
for (let level = 0; level <= CompilerIncludeMaxUpLevels; level++) {
for (const name of CompilerIncludeDirNames) {
const candidate = path.join(dir, name);
const key = candidate.toLowerCase();
if (seen.has(key)) {
continue;
}
seen.add(key);
if (IsDirectory(candidate)) {
found.push(candidate);
}
}
const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
return found;
}
/** 目录存在且确实是目录;不存在 / 无权限 / 是文件时返回 false。 */
function IsDirectory(candidate: string): boolean {
try {
return fs.statSync(candidate).isDirectory();
} catch {
return false;
}
}
+209
View File
@@ -0,0 +1,209 @@
/*
* environmentManager.ts
* Viper 环境(编译器)清单与选中状态。
*
* 数据来源是 VET 每轮输出的**全量**快照,所以这里要自己 diff(Added / Removed),
* 并把「选中哪个环境」持久化到扩展状态(globalState)——刻意不写回 .vpj,保持项目文件干净。
*/
import {
Disposable,
Event,
EventEmitter,
Memento,
QuickPickItem,
QuickPickItemKind,
window,
} from 'vscode';
import { ViperEnvironment } from './types';
/** 一次快照带来的变化。 */
export interface EnvironmentChange {
/** 当前全部环境(已排序)。 */
Environments: ViperEnvironment[];
/** 本轮新增。 */
Added: ViperEnvironment[];
/** 本轮移除。 */
Removed: ViperEnvironment[];
}
/**
* 「选择编译器」的处理(由扩展入口注入:弹文件选择框 + 走 VET 的 check 通道)。
* 返回被选中的环境表示已处理完;返回 undefined 表示用户取消或校验未通过。
*/
export type ManualCompilerPicker = () => Promise<ViperEnvironment | undefined>;
export class EnvironmentManager implements Disposable {
private static readonly SelectedIdKey = 'viper.environmentTool.selectedId';
private environments: ViperEnvironment[] = [];
private selectedId: string | undefined;
private manualCompilerPicker: ManualCompilerPicker | undefined;
private readonly _onDidChangeEnvironments = new EventEmitter<EnvironmentChange>();
readonly OnDidChangeEnvironments: Event<EnvironmentChange> = this._onDidChangeEnvironments.event;
private readonly _onDidChangeSelection = new EventEmitter<ViperEnvironment | undefined>();
readonly OnDidChangeSelection: Event<ViperEnvironment | undefined> = this._onDidChangeSelection.event;
constructor(private readonly globalState: Memento) {
this.selectedId = globalState.get<string>(EnvironmentManager.SelectedIdKey);
}
/** 应用一轮全量快照,返回 diff 结果。 */
ApplySnapshot(environments: ViperEnvironment[]): EnvironmentChange {
const previousIds = new Set(this.environments.map((environment) => environment.Id));
const next = [...environments].sort(CompareEnvironments);
const nextIds = new Set(next.map((environment) => environment.Id));
const added = next.filter((environment) => !previousIds.has(environment.Id));
const removed = this.environments.filter((environment) => !nextIds.has(environment.Id));
this.environments = next;
this.ResolveStickySelection();
const change: EnvironmentChange = { Environments: next, Added: added, Removed: removed };
this._onDidChangeEnvironments.fire(change);
return change;
}
/**
* 选定「本次生效的环境」:
* 1. 优先复用上次用的那个 exe(按 VET 给的 uuid 在快照里找);
* 2. 找不到(第一次用、或那个 exe 已被删/移走)则用发现的第一个;
* 3. VET 一个都没给出时保留上次记录,等它下一轮回来。
* 结果写回 globalState,作为下一次的「上次用的」。
*/
private ResolveStickySelection(): void {
const preferred = this.environments.find((environment) => environment.Id === this.selectedId)
?? this.environments[0];
if (!preferred || preferred.Id === this.selectedId) {
return;
}
this.selectedId = preferred.Id;
void this.globalState.update(EnvironmentManager.SelectedIdKey, preferred.Id);
this._onDidChangeSelection.fire(preferred);
}
/** 当前全部环境。 */
GetEnvironments(): ViperEnvironment[] {
return this.environments;
}
/** 当前选中的环境 id。 */
GetSelectedId(): string | undefined {
return this.selectedId;
}
/** 当前选中的环境(若已被 VET 移除则为 undefined)。 */
GetSelectedEnvironment(): ViperEnvironment | undefined {
if (!this.selectedId) {
return undefined;
}
return this.environments.find((environment) => environment.Id === this.selectedId);
}
/**
* 选择编译器。列表结构:首项是「选择编译器…」(挑一个 exe),随后一条分隔线,
* 再之后才是 VET 搜到的环境。VET 关闭时这个列表照样可用(已发现的环境仍能选用)。
* @param environmentId 传入则直接选中该 id;不传则弹 QuickPick 让用户选。
*/
async SelectEnvironment(environmentId?: string): Promise<ViperEnvironment | undefined> {
if (environmentId) {
return this.ApplySelection(environmentId);
}
const items: CompilerPickItem[] = [
{
label: '$(file) 选择编译器…',
Manual: true,
},
];
if (this.environments.length > 0) {
items.push({ label: '', kind: QuickPickItemKind.Separator });
items.push(
...this.environments.map<EnvironmentQuickPickItem>((environment) => ({
label: environment.DisplayName,
description: `${environment.Version} · ${environment.Arch} · ${environment.Source}`,
detail: environment.Executable,
picked: environment.Id === this.selectedId,
Environment: environment,
}))
);
}
const picked = await window.showQuickPick(items, {
title: '选择编译器',
placeHolder: '选择要为当前工作区使用的 Viper 编译器',
matchOnDescription: true,
matchOnDetail: true,
});
if (!picked) {
return undefined;
}
if ('Manual' in picked) {
return this.manualCompilerPicker ? await this.manualCompilerPicker() : undefined;
}
if ('Environment' in picked) {
return this.ApplySelection(picked.Environment.Id);
}
return undefined;
}
/** 注入「选择编译器」的处理(扩展入口提供:文件选择框 + VET check)。 */
SetManualCompilerPicker(picker: ManualCompilerPicker): void {
this.manualCompilerPicker = picker;
}
/**
* 把一个刚通过 VET check 的环境并入清单 —— 下一轮全量快照(≤ scanIntervalMs)到达前
* 就能立刻选中它;快照到达时因为已在表中,diff 不会重复算作新增。
*/
AddEnvironment(environment: ViperEnvironment): void {
const next = [...this.environments.filter((item) => item.Id !== environment.Id), environment].sort(
CompareEnvironments
);
this.environments = next;
this._onDidChangeEnvironments.fire({ Environments: next, Added: [environment], Removed: [] });
}
dispose(): void {
this._onDidChangeEnvironments.dispose();
this._onDidChangeSelection.dispose();
}
private async ApplySelection(environmentId: string): Promise<ViperEnvironment | undefined> {
const environment = this.environments.find((item) => item.Id === environmentId);
if (!environment) {
return undefined;
}
this.selectedId = environmentId;
await this.globalState.update(EnvironmentManager.SelectedIdKey, environmentId);
this._onDidChangeSelection.fire(environment);
return environment;
}
}
/** 「选择编译器…」项:点击后走手动输入路径。 */
interface ManualPickItem extends QuickPickItem {
Manual: true;
}
interface EnvironmentQuickPickItem extends QuickPickItem {
Environment: ViperEnvironment;
}
/** 选择列表上的项:手动项 / VET 搜到的环境 / 分隔线。 */
type CompilerPickItem = ManualPickItem | EnvironmentQuickPickItem | QuickPickItem;
/** 排序:先按来源分组,再按展示名;保证每轮快照顺序稳定(避免视图抖动)。 */
function CompareEnvironments(left: ViperEnvironment, right: ViperEnvironment): number {
if (left.Source !== right.Source) {
return left.Source.localeCompare(right.Source);
}
return left.DisplayName.localeCompare(right.DisplayName);
}
+729
View File
@@ -0,0 +1,729 @@
/*
* environmentTool.ts
* VET(Viper Environment Tool)常驻进程客户端。
*
* VET 是一个用 Viper 自身写的扫盘发现器(见 TransPyC/VET),协议如下:
* VET.exe scan --cache <path> [--sleep <毫秒>] [--first-no-pace]
* [--need-confirm <秒> [--need-confirm-timeout <毫秒>]] [--path <dir>]...
* - stdout:每轮输出一行**完整**的编译器 JSON 数组(所以增量变化要自己 diff)
* - stderr:滚动日志(量很大,原样进输出通道)
* - --need-confirm:每 <秒> 秒一个确认窗口(窗口长度 = --need-confirm-timeout,缺省 5s),
* 窗口内必须从 stdin 收到一行 {"ok":1};否则 VET 在 stdout 输出 {"exit":1,...} 并以退出码 1 终止。
*
* 本类负责:进程生命周期(自动重启 + 指数退避 + 重启上限)、心跳确认、输出转发、快照解析。
*
* 输出:VET 的 stdout 与 stderr **原样**转发到 "Vipeye VET" 输出通道,不落盘。
*/
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { commands, Disposable, Event, EventEmitter, OutputChannel, window, workspace } from 'vscode';
import { LocalCheckExecutable } from './executableProbe';
import { ViperCheckResult, ViperEnvironment } from './types';
/**
* VET 进程状态。
* disabled —— 配置里 viper.environmentTool.enabled = false,扩展不启动它
* stopped —— 用户手动执行了「停止 VET」命令(与 disabled 区分,便于状态栏给出「点击可重新启动」)
* starting / running / restarting / failed —— 进程生命周期与自动重启
*/
export type EnvironmentToolState = 'disabled' | 'stopped' | 'starting' | 'running' | 'restarting' | 'failed';
/** 进程存活超过该时长即认为这一轮是健康的,重启计数清零。 */
const HealthyRunMs = 30000;
/** 两次心跳之间的最小间隔,避免同一轮重复发送。 */
const MinConfirmIntervalMs = 1500;
/**
* check 请求的等待上限:看门狗只在心跳窗口内读 stdin(窗口外的请求要等下一个窗口,
* 最长 confirmSeconds),父进程还要等到下一个轮询点,故给足余量。
*/
const MinCheckTimeoutMs = 30000;
const ExtraCheckTimeoutMs = 20000;
/**
* 两次 stdin 写入之间的间隔。
* 原因:VET 看门狗读 stdin 时,若同一批 ReadFile 一次读到多行,只保留第一行、
* 其余字节被丢弃(VGuard.ReadConfirmLine 的已知缺陷),下次读就会拼出错位的残行,
* 被判定为"非 {"ok": 1} 的输入"从而**强杀整个 scan 进程**。
* 因此所有写入都排在心跳窗口内、逐行送出并留出本间隔,让每次 ReadFile 只含一行。
*/
const StdinLineGapMs = 300;
/** 每个心跳窗口留给写入的时长(窗口本身是 confirmTimeoutMs,留出余量避免踩线)。 */
const StdinWindowBudgetMs = 1000;
/** 一条尚未收到回执的 check 请求。 */
interface PendingCheck {
Resolve: (result: ViperCheckResult) => void;
Reject: (error: Error) => void;
Timer: NodeJS.Timeout;
}
export class EnvironmentTool implements Disposable {
private process: ChildProcessWithoutNullStreams | undefined;
private state: EnvironmentToolState = 'disabled';
private restarts = 0;
private startedAt = 0;
private lastConfirmAt = 0;
private stdoutBuffer = '';
private stderrBuffer = '';
private restartTimer: NodeJS.Timeout | undefined;
private watchdogTimer: NodeJS.Timeout | undefined;
private disposed = false;
/** 上次记录过的退出信息,避免状态栏/提示重复报同一件事。 */
private lastExitSummary = '';
/** check 请求 id:进程内递增即可(VET 原样回带,仅用于配对)。 */
private nextCheckId = 1;
/** 尚未收到回执的 check 请求。 */
private readonly pendingChecks = new Map<number, PendingCheck>();
/** 待写入 stdin 的 check 请求行(只在心跳窗口内逐行送出,见 BeginConfirmWindow)。 */
private readonly stdinQueue: string[] = [];
/** 本次窗口是否还需要补一行心跳。 */
private pendingHeartbeat = false;
private stdinWriting = false;
private readonly channel: OutputChannel;
private readonly _onDidChangeEnvironments = new EventEmitter<ViperEnvironment[]>();
/** 每轮 stdout 快照(全量)。 */
readonly OnDidChangeEnvironments: Event<ViperEnvironment[]> = this._onDidChangeEnvironments.event;
private readonly _onDidChangeState = new EventEmitter<EnvironmentToolState>();
readonly OnDidChangeState: Event<EnvironmentToolState> = this._onDidChangeState.event;
constructor(private readonly storageDir: string, channel: OutputChannel) {
this.channel = channel;
}
/** 当前进程状态。 */
GetState(): EnvironmentToolState {
return this.state;
}
/** 缓存文件路径(交给 VET 自己维护)。 */
GetCachePath(): string {
return path.join(this.storageDir, 'vet-cache.json');
}
/** 当前配置里的 VET 可执行文件路径。 */
GetExecutablePath(): string {
return this.ReadConfig().exePath;
}
/** 启动(已启动则先停掉再起)。 */
Start(): void {
this.StopProcess();
this.restarts = 0;
this.lastExitSummary = '';
this.Spawn();
}
/** 手动重启(命令入口):重置重启计数,让用户能自己解围。 */
Restart(): void {
this.AppendLog('[vscode] 手动重启 VET');
this.Start();
}
/**
* 手动停止(命令入口):停掉进程并置为 stopped(区别于配置禁用),不再自动重启。
* 之后用「重启 VET」即可恢复(Restart → Start 会重新 Spawn)。
* 说明:不清空已发现的环境与选中项 —— VET 只是发现器,关掉它不影响已经选定的编译器。
*/
Stop(): void {
this.AppendLog('[vscode] 手动停止 VET');
this.StopProcess();
this.restarts = 0;
this.SetState('stopped');
}
/**
* 检查一个 exe 是否可用作 Viper 编译器(用户手动指定位置时用)。
*
* 优先走 VET 的 check 通道;**VET 未启用/未运行/心跳关闭时回退到本地模拟**
* (`executableProbe.LocalCheckExecutable`:读 PE/.PRVERSN + `<exe> --version` 带超时),
* 两条路径的通过判据与失败原因串完全一致,调用方无需区分。
*/
CheckExecutable(exePath: string): Promise<ViperCheckResult> {
if (this.CanUseCheckChannel()) {
return this.CheckViaVet(exePath);
}
this.AppendLog(
`[vscode] VET check 通道不可用(${DescribeStateForMessage(this.state)}),改用本地检查: ${exePath}`
);
return LocalCheckExecutable(exePath, (line) => this.AppendLog(line));
}
/**
* check 通道是否可用:需要 VET 处于 **running**(启动中/重启中的请求要等它扫完一轮
* 才可能进确认窗口,等不到回执就会白等超时)、开了心跳(看门狗是请求的转发方)、
* 且 stdin 仍可写。其余情形一律走本地模拟,保证选编译器随时可用。
*/
private CanUseCheckChannel(): boolean {
const config = this.ReadConfig();
return this.state === 'running' && config.confirmSeconds > 0 && !!this.process && this.process.stdin.writable;
}
/**
* 通过 VET 的 check 通道检查一个 exe。
*
* 通道形态:请求从 **stdin** 写入一行 `{"check": "<绝对路径>", "id": <整数>}`,
* 由看门狗(confirm-watch)识别后经匿名管道转发给 scan;回执是 **stdout** 的一行 JSON,
* 顶层 `id` 回带请求 id。故 check 通道依附心跳看门狗:没开 --need-confirm 就没有该通道。
*
* 通过后 VET 会把该项入库并写入缓存文件,下一轮(≤ scanIntervalMs)快照里就会出现它。
*/
private CheckViaVet(exePath: string): Promise<ViperCheckResult> {
const config = this.ReadConfig();
if (config.confirmSeconds <= 0) {
return Promise.reject(
new Error(
'check 通道依附心跳看门狗:请把 viper.environmentTool.confirmSeconds 设为大于 0 的值(例如 10)。'
)
);
}
const child = this.process;
if (!child || !child.stdin.writable) {
return Promise.reject(new Error(`VET 未在运行(当前状态:${DescribeStateForMessage(this.state)})。`));
}
const id = this.nextCheckId++;
const timeoutMs = Math.max(MinCheckTimeoutMs, config.confirmSeconds * 1000 + ExtraCheckTimeoutMs);
return new Promise<ViperCheckResult>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingChecks.delete(id);
reject(
new Error(
`等待 VET 回执超时(${Math.round(timeoutMs / 1000)} 秒)。请确认 VET 正在运行、且心跳确认已开启。`
)
);
}, timeoutMs);
this.pendingChecks.set(id, { Resolve: resolve, Reject: reject, Timer: timer });
// 只入队:等下一个心跳窗口才送出(窗口外写入会被积压成多行,见 BeginConfirmWindow)。
// 路径里的反斜杠/引号等必须转义;VET 侧用 json 库解析这一行。
this.stdinQueue.push(`${JSON.stringify({ check: exePath, id })}\n`);
this.AppendLog(`[vscode] 发出 check 请求 #${id}: ${exePath}`);
});
}
/**
* 逐行送出 stdin 队列。
*
* 顺序有讲究:**先 check 请求,最后才发心跳**。因为看门狗一旦确认 ok=1 就立刻离开
* 等待循环(回去等下一个 interval),此后写进去的行只能积压到下一个窗口,下个窗口
* 又会与新的心跳凑成同一批 —— 正是要避免的局面。而 check 行会被转发后继续留在
* 等待循环里,所以只要逐行、留间隔,每一行都会被单独读走。
*
* 每窗口发送量按窗口时长封顶,保证心跳一定能在窗口内发出。
*/
private async DrainStdinQueue(): Promise<void> {
if (this.stdinWriting) {
return;
}
this.stdinWriting = true;
try {
const config = this.ReadConfig();
const budget = Math.max(0, config.confirmTimeoutMs - StdinWindowBudgetMs);
const maxChecks = Math.max(0, Math.floor(budget / StdinLineGapMs));
let sent = 0;
while (this.stdinQueue.length > 0 && sent < maxChecks) {
if (!this.WriteOneLine(this.stdinQueue.shift() as string)) {
return;
}
sent++;
// 只在后面还有内容要写时才等间隔(省掉最后一次多余的等待)。
if (this.stdinQueue.length > 0 || this.pendingHeartbeat) {
await Delay(StdinLineGapMs);
}
}
if (this.pendingHeartbeat) {
this.pendingHeartbeat = false;
this.WriteOneLine('{"ok":1}\n');
}
} finally {
this.stdinWriting = false;
}
}
/** 写一行到 VET 的 stdin;返回 false 表示进程已不可写(并清空队列)。 */
private WriteOneLine(line: string): boolean {
const child = this.process;
if (!child || !child.stdin.writable) {
this.stdinQueue.length = 0;
this.pendingHeartbeat = false;
return false;
}
try {
child.stdin.write(line);
return true;
} catch (e) {
this.AppendLog(`[vscode] 写入 stdin 失败: ${String(e)}`);
return true; // 单行失败不放弃整个队列(异常多为 CLOSED,下一行会走上面的分支)
}
}
/** 追加一行日志(进 "Vipeye VET" 输出通道),供扩展其它部分记录与 VET 相关的信息。 */
AppendLog(line: string): void {
this.channel.appendLine(line);
}
/** 显示输出通道。 */
ShowLog(): void {
this.channel.show(true);
}
dispose(): void {
this.disposed = true;
this.StopProcess();
this._onDidChangeEnvironments.dispose();
this._onDidChangeState.dispose();
this.channel.dispose();
}
// ===== 进程生命周期 =====
private Spawn(): void {
if (this.disposed) {
return;
}
const config = this.ReadConfig();
if (!config.enabled) {
this.SetState('disabled');
this.AppendLog('[vscode] viper.environmentTool.enabled = false,不启动 VET');
return;
}
if (!fs.existsSync(config.exePath)) {
// 路径配错不会自己好,直接失败并提示,不做无意义的重启。
this.SetState('failed');
this.AppendLog(`[vscode] 找不到 VET 可执行文件: ${config.exePath}`);
void window
.showErrorMessage(
`VET 可执行文件不存在:${config.exePath}。请检查设置 viper.environmentTool.path。`,
'打开设置'
)
.then((picked) => {
if (picked === '打开设置') {
void commands.executeCommand('workbench.action.openSettings', 'viper.environmentTool.path');
}
});
return;
}
// VET 要把发现缓存写到该目录;目录不存在时缓存写失败(只告警),会导致每轮都从空缓存重扫。
try {
fs.mkdirSync(this.storageDir, { recursive: true });
} catch {
// 建不出目录不阻断启动(VET 仍能运行,只是缓存不可用)。
}
this.SetState('starting');
const args = this.BuildArgs(config);
this.AppendLog(`[vscode] 启动: ${config.exePath} ${args.join(' ')}`);
let child: ChildProcessWithoutNullStreams;
try {
child = spawn(config.exePath, args, { windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
} catch (e) {
this.AppendLog(`[vscode] 启动失败: ${String(e)}`);
this.HandleUnexpectedExit(-1, 'spawn-error');
return;
}
this.process = child;
this.startedAt = Date.now();
this.lastConfirmAt = 0;
this.stdoutBuffer = '';
this.stderrBuffer = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => this.HandleStdout(chunk));
child.stderr.on('data', (chunk: string) => this.HandleStderr(chunk));
child.on('error', (err: Error) => {
this.AppendLog(`[vscode] 进程错误: ${err.message}`);
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
this.SetState('failed');
void window.showErrorMessage(`无法启动 VET(${config.exePath}):${err.message}`);
}
});
child.on('exit', (code, signal) => {
if (this.process === child) {
this.process = undefined;
}
const aliveMs = Date.now() - this.startedAt;
this.AppendLog(`[vscode] VET 退出: code=${code} signal=${signal ?? '-'} 存活 ${aliveMs} ms`);
this.RejectPendingChecks('VET 进程已退出,check 请求未完成。');
if (this.disposed) {
return;
}
this.HandleUnexpectedExit(code ?? -1, signal ?? 'exit');
});
this.EnsureWatchdog();
this.SetState('running');
}
/** 组装 scan 子命令参数。 */
private BuildArgs(config: EnvironmentToolConfig): string[] {
// --first-no-pace:首轮不节流。VET 常驻模式默认每 16 个目录项 / 64 个文件 Sleep 20ms,
// 首次全盘搜索会明显变慢;首轮全速、后续轮次仍按 VET 自身的节流跑。
const args = [
'scan',
'--cache', this.GetCachePath(),
'--sleep', String(config.scanIntervalMs),
'--first-no-pace',
];
if (config.confirmSeconds > 0) {
args.push('--need-confirm', String(config.confirmSeconds));
args.push('--need-confirm-timeout', String(config.confirmTimeoutMs));
}
// 一旦出现 --path,VET 就不再自动建常用目录表,所以仅在显式配置时追加。
for (const scanPath of config.scanPaths) {
if (scanPath && scanPath.trim().length > 0) {
args.push('--path', scanPath);
}
}
return args;
}
private StopProcess(): void {
if (this.restartTimer) {
clearTimeout(this.restartTimer);
this.restartTimer = undefined;
}
if (this.watchdogTimer) {
clearInterval(this.watchdogTimer);
this.watchdogTimer = undefined;
}
const child = this.process;
this.process = undefined;
this.RejectPendingChecks('VET 进程已停止,check 请求未完成。');
if (!child) {
return;
}
this.AppendLog('[vscode] 请求停止 VET 进程');
child.stdout.removeAllListeners();
child.stderr.removeAllListeners();
child.removeAllListeners('exit');
child.kill();
}
/**
* 非主动退出:按「重启上限 + 指数退避」处理,并且每次都通知用户。
* @param code 退出码(-1 表示启动失败等非进程退出情形)
* @param reason 退出原因描述
*/
private HandleUnexpectedExit(code: number, reason: string): void {
const config = this.ReadConfig();
const aliveMs = Date.now() - this.startedAt;
if (aliveMs >= HealthyRunMs) {
this.restarts = 0;
}
const summary = `code=${code} reason=${reason}`;
if (summary !== this.lastExitSummary) {
this.lastExitSummary = summary;
void window
.showErrorMessage(`VET 异常退出(${summary})。`, '查看日志')
.then((picked) => {
if (picked === '查看日志') {
this.ShowLog();
}
});
}
if (this.restarts >= config.maxRestarts) {
this.SetState('failed');
this.AppendLog(
`[vscode] 已连续异常退出 ${this.restarts} 次,达到上限 ${config.maxRestarts},不再自动重启`
);
void window
.showErrorMessage(
`VET 已连续异常退出 ${this.restarts} 次(上限 ${config.maxRestarts}),不再自动重启。`,
'重启 VET',
'打开设置'
)
.then((picked) => {
if (picked === '重启 VET') {
this.Restart();
} else if (picked === '打开设置') {
void import('vscode').then((vscode) =>
vscode.commands.executeCommand('workbench.action.openSettings', 'viper.environmentTool')
);
}
});
return;
}
this.restarts++;
const delay = Math.min(30000, 1000 * 2 ** (this.restarts - 1));
this.SetState('restarting');
this.AppendLog(`[vscode] 第 ${this.restarts}/${config.maxRestarts} 次重启,${delay} ms 后重试`);
this.restartTimer = setTimeout(() => {
this.restartTimer = undefined;
this.Spawn();
}, delay);
}
// ===== stdout / stderr =====
private HandleStdout(chunk: string): void {
this.stdoutBuffer += chunk;
let newlineIndex = this.stdoutBuffer.indexOf('\n');
while (newlineIndex >= 0) {
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
if (line.length > 0) {
this.HandleStdoutLine(line);
}
newlineIndex = this.stdoutBuffer.indexOf('\n');
}
}
private HandleStdoutLine(line: string): void {
// stdout 是数据通道(每轮一行完整 JSON 数组)。原样进 "Vipeye VET",与 stderr 对照排查。
this.channel.appendLine(`[stdout] ${line}`);
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch {
// 非 JSON 的 stdout(例如 VET 的 [OOM] 文本)已在上面原样输出。
return;
}
if (Array.isArray(parsed)) {
this._onDidChangeEnvironments.fire(this.ToEnvironments(parsed));
return;
}
// check 回执:{"check_result": bool, "id": <请求 id>, "viper"|"check_msg": …}
if (typeof parsed === 'object' && parsed !== null && 'check_result' in parsed) {
this.HandleCheckReply(parsed as Record<string, unknown>);
return;
}
// {"exit":1, "msg":"timeout/ok!=1"} —— VET 因确认失败而终止。
const exitInfo = parsed as { exit?: number; msg?: string };
this.AppendLog(`[vscode] VET 报告退出: exit=${exitInfo.exit ?? '?'} msg=${exitInfo.msg ?? ''}`);
}
/** 处理 check 回执:按回带的 id 唤醒等待中的请求。 */
private HandleCheckReply(reply: Record<string, unknown>): void {
const id = Number(reply.id);
const pending = this.pendingChecks.get(id);
if (!pending) {
// 超时后迟到的回执,或不是本扩展发出的请求:只记录,不当作错误。
this.AppendLog(`[vscode] 收到无人等待的 check 回执: id=${reply.id}`);
return;
}
this.pendingChecks.delete(id);
clearTimeout(pending.Timer);
const ok = reply.check_result === true;
const raw = reply.viper;
const environment =
ok && typeof raw === 'object' && raw !== null
? this.ToEnvironment(raw as Record<string, unknown>)
: undefined;
this.AppendLog(
ok
? `[vscode] check #${id} 通过: ${environment?.DisplayName ?? '(无详情)'}`
: `[vscode] check #${id} 未通过: ${String(reply.check_msg ?? '未知原因')}`
);
pending.Resolve({
Ok: ok,
Id: id,
Environment: environment,
Message: typeof reply.check_msg === 'string' ? reply.check_msg : undefined,
});
}
/** 进程消失时:丢弃待写的 stdin 行,并唤醒所有等待中的 check 请求,避免调用方一直挂着。 */
private RejectPendingChecks(reason: string): void {
this.stdinQueue.length = 0;
this.pendingHeartbeat = false;
if (this.pendingChecks.size === 0) {
return;
}
for (const [, pending] of this.pendingChecks) {
clearTimeout(pending.Timer);
pending.Reject(new Error(reason));
}
this.pendingChecks.clear();
}
/** 把 VET 输出的 JSON 条目规整为强类型(字段名转 PascalCase,缺字段给空串)。 */
private ToEnvironments(entries: unknown[]): ViperEnvironment[] {
return entries
.filter((entry): entry is Record<string, unknown> => typeof entry === 'object' && entry !== null)
.map((entry) => this.ToEnvironment(entry))
.filter((environment) => environment.Id.length > 0 || environment.Executable.length > 0);
}
private ToEnvironment(entry: Record<string, unknown>): ViperEnvironment {
return {
Id: String(entry.id ?? ''),
Executable: String(entry.executable ?? ''),
Version: String(entry.version ?? ''),
Arch: String(entry.arch ?? ''),
Source: String(entry.source ?? ''),
DisplayName: String(entry.displayName ?? entry.executable ?? ''),
};
}
private HandleStderr(chunk: string): void {
this.stderrBuffer += chunk;
let newlineIndex = this.stderrBuffer.indexOf('\n');
while (newlineIndex >= 0) {
const line = this.stderrBuffer.slice(0, newlineIndex).replace(/\r$/, '');
this.stderrBuffer = this.stderrBuffer.slice(newlineIndex + 1);
if (line.length > 0) {
this.HandleStderrLine(line);
}
newlineIndex = this.stderrBuffer.indexOf('\n');
}
}
private HandleStderrLine(line: string): void {
// stderr 全部原样进 "Vipeye VET"(含 [scan] 进度行),不做过滤。
this.channel.appendLine(line);
// 每轮结束会提示进入确认窗口(两行:请在… / 当前… 本轮期限…)。
if (line.includes('本轮期限') || line.includes('[confirm] 请在')) {
this.BeginConfirmWindow();
}
}
/**
* 心跳窗口打开:看门狗此刻正在等 stdin,是**唯一安全**的写入时机。
* 窗口外写入会在管道里积压成多行,被看门狗一次 ReadFile 读走时只保留第一行、
* 其余字节丢弃 ⇒ 拼出错位残行 ⇒ 判定失败并强杀 scan 进程(见 StdinLineGapMs)。
* 故:心跳与 check 请求都只入队,统一在这里开始逐行送出(行间留间隔)。
*/
private BeginConfirmWindow(): void {
const child = this.process;
if (!child || !child.stdin.writable) {
return;
}
// 提示是两行,间隔很短;此处去重避免同一窗口重复插心跳。
if (Date.now() - this.lastConfirmAt < MinConfirmIntervalMs) {
return;
}
this.lastConfirmAt = Date.now();
this.pendingHeartbeat = true;
void this.DrainStdinQueue();
}
/**
* 心跳兜底:万一一直没识别到确认提示(例如 VET 改了措辞),
* 超过「确认周期 + 5 秒」没有发送过就补一次,避免 VET 因超时自杀。
* 这是异常路径,只能无视窗口直接写;为免与积压行凑成一批,先丢弃排队中的请求。
*/
private EnsureWatchdog(): void {
if (this.watchdogTimer) {
return;
}
this.watchdogTimer = setInterval(() => {
const config = this.ReadConfig();
const child = this.process;
if (config.confirmSeconds <= 0 || !child) {
return;
}
if (Date.now() - this.lastConfirmAt <= (config.confirmSeconds + 5) * 1000) {
return;
}
this.stdinQueue.length = 0;
if (!child.stdin.writable) {
return;
}
this.lastConfirmAt = Date.now();
try {
child.stdin.write('{"ok":1}\n');
this.AppendLog('[vscode] 兜底发送心跳(未识别到确认窗口提示)');
} catch (e) {
this.AppendLog(`[vscode] 兜底心跳写入失败: ${String(e)}`);
}
}, 2000);
}
// ===== 状态 =====
private SetState(next: EnvironmentToolState): void {
if (this.state === next) {
return;
}
this.state = next;
this._onDidChangeState.fire(next);
}
private ReadConfig(): EnvironmentToolConfig {
const config = workspace.getConfiguration('viper.environmentTool');
return {
enabled: config.get<boolean>('enabled', true),
exePath: config.get<string>('path', '').trim(),
scanPaths: config.get<string[]>('scanPaths', []),
confirmSeconds: config.get<number>('confirmSeconds', 10),
confirmTimeoutMs: config.get<number>('confirmTimeoutMs', 5000),
scanIntervalMs: config.get<number>('scanIntervalMs', 10000),
maxRestarts: Math.max(0, config.get<number>('maxRestarts', 5)),
};
}
}
interface EnvironmentToolConfig {
enabled: boolean;
exePath: string;
scanPaths: string[];
confirmSeconds: number;
confirmTimeoutMs: number;
scanIntervalMs: number;
maxRestarts: number;
}
/** 简单延时(用于 stdin 逐行写入的间隔)。 */
function Delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/** 状态的简短中文描述(用于错误消息)。 */
function DescribeStateForMessage(state: EnvironmentToolState): string {
switch (state) {
case 'running':
return '运行中';
case 'starting':
return '启动中';
case 'restarting':
return '重启中';
case 'stopped':
return '已手动关闭';
case 'failed':
return '已停止(连续失败)';
default:
return '未启用';
}
}
+382
View File
@@ -0,0 +1,382 @@
/*
* executableProbe.ts
* VET 未启用时的**本地**编译器校验(VET check 通道的 TypeScript 模拟)。
*
* 背景:手动「选择编译器…」原先只能经 VET 的 check 通道(stdin 请求 → stdout 回执),
* 而该通道依附常驻进程与心跳看门狗 —— VET 被禁用 / 手动停止 / 启动失败 / 未开
* confirmSeconds 时,选编译器直接失败。本模块用 TS 复刻同一条口径,使 VET 缺席时仍可选用:
* ① 读 PE:DOS/PE 头 → 节表 → `.PRVERSN` 段 → 版本串;COFF Machine → arch
* (与 VET/App/lib/VPe.vp 的 TryReadPeInfo + ParsePeHead 逐条同口径,小端读取用
* 逐字节"或 + 移位"的位操作,不用 Buffer 的 LE 读取)
* ② 执行 `<exe> --version`:合并捕获 stdout+stderr,**5 秒超时**(与 VProc.RunVersionProbe
* 的 VERSION_PROBE_TIMEOUT_MS 一致),输出前 5 字符必须是 "Viper"
*
* 失败原因串与 VET 的 check_msg **逐字一致**(NOT_FOUND / NO_PERMISSION / NOT_EXE /
* NO_PRVERSN / PRVERSN_ERR / PREFIX_ERR / TIMEOUT / EXE_ERR,0x<HEX>),故调用方沿用
* 同一套文案(extension.DescribeCheckMessage)即可,不必区分"校验来自 VET 还是本地"。
*
* 只读不写:本地检查不落盘、不入库(VET 会把它写进自己的缓存文件,这里没有缓存)。
*/
import { ChildProcess, spawn } from 'child_process';
import { randomUUID } from 'crypto';
import * as fs from 'fs';
import type { ViperCheckResult } from './types';
// ===== PE/COFF 常量(与 VET/App/lib/VPe.vp 一一对应) =====
const DosSignature = 0x5a4d; // "MZ" 小端
const NtSignature = 0x00004550; // "PE\0\0" 小端
const OptMagicPe32 = 0x10b;
const OptMagicPe32Plus = 0x20b;
const SecEntrySize = 40;
const ELfanewOffset = 0x3c; // DOS 头 e_lfanew
const CoffMachineOffset = 4; // PE签名(4) + COFF: Machine @ COFF+0
const CoffNumSectionsOffset = 6; // PE签名(4) + COFF: NumberOfSections @ COFF+2
const CoffOptSizeOffset = 20; // PE签名(4) + COFF: SizeOfOptionalHeader @ COFF+16
const OptMagicOffset = 24; // PE签名(4) + COFF(20) 之后为可选头 Magic
const SecNameOffset = 0;
const SecRawSizeOffset = 16;
const SecRawPtrOffset = 20;
const VersionSectionName = '.PRVERSN';
const PeHeadRead = 8192; // 头部窗口(DOS + COFF + 可选头 + 节表),与 VPe.PE_HEAD_READ 同值
const VersionCap = 128; // .PRVERSN 版本串上限(VPe.VER_CAP)
const VersionProbeCap = 4096; // --version 捕获上限(VProc.VERSION_PROBE_DIAG_CAP)
const VersionProbeTimeoutMs = 5000; // --version 探测超时(VProc.VERSION_PROBE_TIMEOUT_MS)
const VersionPrefix = 'Viper'; // --version 输出前缀(VProc.VERSION_PREFIX)
/** check 失败原因串(与 VET VCheck 的 CK_* 常量逐字一致)。 */
export const CheckReason = {
NotFound: 'NOT_FOUND',
NoPermission: 'NO_PERMISSION',
NotExe: 'NOT_EXE',
NoPrversn: 'NO_PRVERSN',
PrversnErr: 'PRVERSN_ERR',
PrefixErr: 'PREFIX_ERR',
Timeout: 'TIMEOUT',
} as const;
// ===== 小端读取(逐字节位操作,与 VPe.ReadU16 / ReadU32 同口径) =====
/** 小端读 u16。 */
function ReadU16(buf: Buffer, offset: number): number {
return buf[offset] | (buf[offset + 1] << 8);
}
/** 小端读 u32(`>>> 0` 归为无符号,避免最高位为 1 时按有符号数参与比较)。 */
function ReadU32(buf: Buffer, offset: number): number {
return (buf[offset] | (buf[offset + 1] << 8) | (buf[offset + 2] << 16) | (buf[offset + 3] << 24)) >>> 0;
}
/** COFF Machine → arch 字符串(与 VPe.FillArch 的不同取值分支一致)。 */
function MachineToArch(machine: number): string {
switch (machine) {
case 0x8664:
return 'x86_64';
case 0x14c:
return 'x86';
case 0xaa64:
return 'arm64';
case 0x1c0:
case 0x1c4:
return 'arm';
default:
// 与 VPe 的 snprintf("unknown(0x%X)") 同格式:大写十六进制、不补零
return `unknown(0x${machine.toString(16).toUpperCase()})`;
}
}
/**
* PE 解析失败码 → 固定原因串(与 VET VCheck.MapPeFailure 同口径)。
* 本模块只发出 rc=6/7(.PRVERSN 段越界/读取失败)与 100+prc(ParsePeHead 的解析码)。
*/
function MapPeFailure(rc: number): string {
if (rc === 6 || rc === 7) {
return CheckReason.PrversnErr;
}
if (rc >= 100) {
const parseRc = rc - 100;
if (parseRc === 7) {
return CheckReason.NoPrversn; // 未找到 .PRVERSN
}
if (parseRc === 6) {
return CheckReason.PrversnErr; // 节表超出头部窗口(无法定位段)
}
}
return CheckReason.NotExe;
}
/** 打开失败 → NOT_FOUND / NO_PERMISSION(与 VET 的 OpenErr 口径:找不到文件才算 NOT_FOUND,其余归无权限)。 */
function MapOpenFailure(error: unknown): string {
return (error as NodeJS.ErrnoException).code === 'ENOENT' ? CheckReason.NotFound : CheckReason.NoPermission;
}
/** 取段内容里到第一个 NUL 为止的版本串(长度上限 VersionCap-1 = 127,与 VPe.CopyVersion 一致)。 */
function CopyVersion(buf: Buffer, offset: number, length: number): string {
let n = 0;
while (n < length && n < VersionCap - 1) {
if (buf[offset + n] === 0) {
break;
}
n++;
}
return buf.toString('utf8', offset, offset + n);
}
/** 从头部窗口解析 PE:节表里找 `.PRVERSN`,取段位置(raw_ptr / raw_size)与 arch。 */
function ParsePeHead(
head: Buffer,
got: number
): { Ok: true; RawPtr: number; RawSize: number; Arch: string } | { Ok: false; Message: string } {
// 各失败码与 VPe.ParsePeHead 的 prc 编码一致(101=窗口不足 … 107=未找到 .PRVERSN)
if (got < 0x40) {
return { Ok: false, Message: MapPeFailure(101) };
}
if (ReadU16(head, 0) !== DosSignature) {
return { Ok: false, Message: MapPeFailure(102) };
}
const lfanew = ReadU32(head, ELfanewOffset);
if (lfanew + 24 > got) {
return { Ok: false, Message: MapPeFailure(103) };
}
if (ReadU32(head, lfanew) !== NtSignature) {
return { Ok: false, Message: MapPeFailure(104) };
}
const numSections = ReadU16(head, lfanew + CoffNumSectionsOffset);
const optSize = ReadU16(head, lfanew + CoffOptSizeOffset);
const magic = ReadU16(head, lfanew + OptMagicOffset);
if (magic !== OptMagicPe32 && magic !== OptMagicPe32Plus) {
return { Ok: false, Message: MapPeFailure(105) };
}
const machine = ReadU16(head, lfanew + CoffMachineOffset);
const sectionTableOffset = lfanew + 24 + optSize;
if (sectionTableOffset + numSections * SecEntrySize > got) {
return { Ok: false, Message: MapPeFailure(106) };
}
for (let index = 0; index < numSections; index++) {
const entry = sectionTableOffset + index * SecEntrySize;
// 节名恰为 8 字节的 ".PRVERSN"(与 VPe 的 strncmp(name, ".PRVERSN", 8) 等价)
const name = head.toString('latin1', entry + SecNameOffset, entry + SecNameOffset + 8);
if (name === VersionSectionName) {
return {
Ok: true,
RawPtr: ReadU32(head, entry + SecRawPtrOffset),
RawSize: ReadU32(head, entry + SecRawSizeOffset),
Arch: MachineToArch(machine),
};
}
}
return { Ok: false, Message: MapPeFailure(107) };
}
/**
* 读一个 exe 的 `.PRVERSN` 版本串与 arch(check 的第 ① 步)。
* 成功返回版本串 + arch;失败返回固定原因串(可直接作为 check_msg 展示)。
*
* 与 VET 的差别只在"段内容不在头部窗口时怎么拿":VET 因 Win32 seek 传参缺陷改走整文件读,
* 这里按段位置定位读(得到同一段字节,且不必把上百 MB 的 exe 读进内存)。
*/
function ReadPeVersionInfo(
exePath: string
): { Ok: true; Version: string; Arch: string } | { Ok: false; Message: string } {
let fd: number;
try {
fd = fs.openSync(exePath, 'r');
} catch (error) {
return { Ok: false, Message: MapOpenFailure(error) };
}
try {
const fileSize = fs.fstatSync(fd).size;
if (fileSize < 0x40) {
return { Ok: false, Message: CheckReason.NotExe };
}
const headCap = Math.min(PeHeadRead, fileSize);
const head = Buffer.alloc(headCap);
const got = fs.readSync(fd, head, 0, headCap, 0);
if (got < 0x40) {
// 文件在"取大小"与"读头部"之间被改小等极端情形:读不出头部即非有效 PE
// (VET 用 rc=5,其 MapPeFailure 归到 NO_PRVERSN;这里按语义归 NOT_EXE)
return { Ok: false, Message: CheckReason.NotExe };
}
const parsed = ParsePeHead(head, got);
if (!parsed.Ok) {
return { Ok: false, Message: parsed.Message };
}
const { RawPtr, RawSize, Arch } = parsed;
if (RawSize === 0 || RawPtr + RawSize > fileSize) {
return { Ok: false, Message: MapPeFailure(6) };
}
let section = head;
let sectionOffset = RawPtr;
if (RawPtr + RawSize > got) {
section = Buffer.alloc(RawSize);
const readBytes = fs.readSync(fd, section, 0, RawSize, RawPtr);
if (readBytes < RawSize) {
return { Ok: false, Message: MapPeFailure(7) };
}
sectionOffset = 0;
}
const version = CopyVersion(section, sectionOffset, RawSize);
if (version.length === 0) {
// 段在但内容为空(首字节即 NUL)——与 VET CheckExecutable 的判据一致
return { Ok: false, Message: CheckReason.PrversnErr };
}
return { Ok: true, Version: version, Arch };
} catch {
return { Ok: false, Message: CheckReason.NoPermission };
} finally {
try {
fs.closeSync(fd);
} catch {
// 关闭失败无关紧要(进程会释放句柄)
}
}
}
/** 拉起失败 → `EXE_ERR,0x<大写十六进制>`(与 VET VCheck.FormatExeErr 同格式)。 */
function FormatExeErr(error: unknown): string {
const err = error as NodeJS.ErrnoException;
let code = 0;
if (err.code === 'ENOENT') {
code = 2; // Win32 ERROR_FILE_NOT_FOUND
} else if (err.code === 'EACCES' || err.code === 'EPERM') {
code = 5; // Win32 ERROR_ACCESS_DENIED
} else if (typeof err.errno === 'number' && err.errno > 0) {
code = err.errno;
}
return `EXE_ERR,0x${code.toString(16).toUpperCase()}`;
}
/**
* 从捕获输出里取**第一行有效内容**:跳过以 `[` 开头的诊断行(以及空行)后继续往下读。
*
* 为什么需要:编译器是带 ASan 运行时的构建,`<exe> --version` 会先在 stderr 打几行
* `[traceback] …`(如 `[traceback] ready: Ctrl+C prints stack then exits, …`),
* 之后才在 stdout 打真正的 `Viper 1.0.5-dev.970 …`。VET 把两条流接到同一个管道,
* 靠"整个缓冲前 5 字符"判定;Node 侧两条流分开、到达顺序不定,故显式跳过 `[` 开头的行,
* 再对第一行有效内容判前缀(同时对行首空白宽容,避免缩进导致误判)。
*/
function FirstMeaningfulLine(output: string): string {
for (const line of output.split(/\r?\n/)) {
const trimmed = line.trimStart();
if (trimmed.length === 0 || trimmed.startsWith('[')) {
continue;
}
return trimmed;
}
return '';
}
/**
* 执行 `<exe> --version`(check 的第 ② 步):合并捕获 stdout+stderr、5 秒超时、
* 第一行有效内容的前 5 字符必须是 "Viper"(`[` 开头的诊断行跳过,见 FirstMeaningfulLine)。
*
* 与 VET 的差别:VET 把 stdout 与 stderr 接到**同一个**管道;Node 做不到,故按到达顺序
* 追加到同一缓冲。只判第一行有效内容,两种接法结论一致。
*/
function ProbeVersion(exePath: string): Promise<{ Ok: true } | { Ok: false; Message: string }> {
let child: ChildProcess;
try {
child = spawn(exePath, ['--version'], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
} catch (error) {
return Promise.resolve({ Ok: false, Message: FormatExeErr(error) });
}
return new Promise((resolve) => {
let output = '';
let settled = false;
let timedOut = false;
const Finish = (result: { Ok: true } | { Ok: false; Message: string }): void => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve(result);
};
const timer = setTimeout(() => {
timedOut = true;
try {
child.kill();
} catch {
// 进程可能已退出:忽略
}
Finish({ Ok: false, Message: CheckReason.Timeout });
}, VersionProbeTimeoutMs);
const Capture = (chunk: Buffer): void => {
if (output.length >= VersionProbeCap) {
return;
}
output = (output + chunk.toString('utf8')).slice(0, VersionProbeCap);
};
child.stdout?.on('data', Capture);
child.stderr?.on('data', Capture);
child.on('error', (error) => Finish({ Ok: false, Message: FormatExeErr(error) }));
child.on('close', () => {
if (timedOut) {
return; // 超时已回执,忽略随后的 close
}
Finish(
FirstMeaningfulLine(output).startsWith(VersionPrefix)
? { Ok: true }
: { Ok: false, Message: CheckReason.PrefixErr }
);
});
});
}
/**
* 本地校验一个编译器:读 PE/.PRVERSN + `<exe> --version`(VET check 通道的 TS 模拟)。
* 失败原因串与 VET 的 check_msg 一致,调用方可直接用同一套文案解释。
* 返回的 Id 固定 0:本地检查不经"请求 id / 回执配对"。
*
* @param log 可选的日志出口(接进 "Vipeye VET" 输出通道),用于留下"这一步做了什么"。
*/
export async function LocalCheckExecutable(
exePath: string,
log?: (line: string) => void
): Promise<ViperCheckResult> {
const pe = ReadPeVersionInfo(exePath);
if (!pe.Ok) {
log?.(`[probe] PE 检查未通过: ${pe.Message}`);
return { Ok: false, Id: 0, Message: pe.Message };
}
const probe = await ProbeVersion(exePath);
if (!probe.Ok) {
log?.(`[probe] --version 检查未通过: ${probe.Message}`);
return { Ok: false, Id: 0, Message: probe.Message };
}
log?.(`[probe] 检查通过: Viper ${pe.Version}(${pe.Arch})`);
return {
Ok: true,
Id: 0,
Environment: {
Id: randomUUID(),
Executable: exePath,
Version: pe.Version,
Arch: pe.Arch,
// 与 VET check 命中入库时的 source 取值一致(同一语义:本次答复的来源)
Source: 'check',
DisplayName: `Viper ${pe.Version}`,
},
};
}
+272 -10
View File
@@ -3,11 +3,17 @@
* Viper Core 扩展入口。
*/
import { commands, ExtensionContext, window, workspace } from 'vscode';
import * as path from 'path';
import { commands, ExtensionContext, OutputChannel, ProgressLocation, Uri, window, workspace } from 'vscode';
import { EnvironmentManager } from './environmentManager';
import { EnvironmentTool } from './environmentTool';
import { CountProjectLoc, FormatLocReport, LocReport } from './lineCounter';
import { LocReportView } from './locReportView';
import { ProjectManager } from './projectManager';
import { ToolchainManager } from './toolchainManager';
import { ViperCheckResult, ViperEnvironment } from './types';
import { ViperViewProvider } from './viperViewProvider';
import { StatusBarManager } from './statusBar';
import { EnvironmentStatusBar } from './statusBar';
import { ViperApiImpl } from './api';
import { VpjEditorProvider } from './vpjEditorProvider';
@@ -15,6 +21,16 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
const projects = new ProjectManager();
const toolchain = new ToolchainManager();
// VET(Viper Environment Tool)常驻进程:扫盘发现编译器,每轮输出全量 JSON 快照。
// stdout 与 stderr 都原样转发到该输出通道(不落盘)。
const vetChannel = window.createOutputChannel('Vipeye VET');
const environmentTool = new EnvironmentTool(context.globalStorageUri.fsPath, vetChannel);
const environments = new EnvironmentManager(context.globalState);
// LOC 统计输出通道(右键 .vpj → 统计该工程源目录下所有已知语言的行数)。
const locChannel = window.createOutputChannel('Viper LOC');
context.subscriptions.push(locChannel);
// 初始扫描项目文件。
await projects.DiscoverProjects();
@@ -22,19 +38,21 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
const watcher = projects.WatchProjectFiles();
context.subscriptions.push(watcher);
// 注册三个树视图。
const projectView = new ViperViewProvider('viper.projectView', projects, toolchain);
const toolchainView = new ViperViewProvider('viper.toolchainView', projects, toolchain);
const settingsView = new ViperViewProvider('viper.settingsView', projects, toolchain);
// 注册四个树视图。
const projectView = new ViperViewProvider('viper.projectView', projects, toolchain, environments, environmentTool);
const toolchainView = new ViperViewProvider('viper.toolchainView', projects, toolchain, environments, environmentTool);
const settingsView = new ViperViewProvider('viper.settingsView', projects, toolchain, environments, environmentTool);
const environmentView = new ViperViewProvider('viper.environmentView', projects, toolchain, environments, environmentTool);
context.subscriptions.push(
window.registerTreeDataProvider('viper.projectView', projectView),
window.registerTreeDataProvider('viper.toolchainView', toolchainView),
window.registerTreeDataProvider('viper.settingsView', settingsView)
window.registerTreeDataProvider('viper.settingsView', settingsView),
window.registerTreeDataProvider('viper.environmentView', environmentView)
);
// 状态栏。
context.subscriptions.push(new StatusBarManager(toolchain));
// 状态栏:只保留 VET 环境项(编译器位置完全由发现结果决定)。
context.subscriptions.push(new EnvironmentStatusBar(environments, environmentTool));
// 注册 .vpj 自定义编辑器。
context.subscriptions.push(VpjEditorProvider.Register(context));
@@ -50,6 +68,45 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
})
);
// VET 快照 → 环境清单(自己 diff;VET 每轮都给全量)。
context.subscriptions.push(
environmentTool.OnDidChangeEnvironments((snapshot) => {
const change = environments.ApplySnapshot(snapshot);
const summary = [`本轮: 共 ${change.Environments.length} 个`];
if (change.Added.length > 0) {
summary.push(`+${change.Added.length}`);
}
if (change.Removed.length > 0) {
summary.push(`-${change.Removed.length}`);
}
environmentTool.AppendLog(`[vscode] ${summary.join(' ')}`);
// 生效环境同步给工具链:有环境就用它(上次用的那个 → 否则发现的第一个),
// 一个都没发现时回退到配置/硬编码的兜底路径。
const selected = environments.GetSelectedEnvironment();
if (selected) {
toolchain.ApplyEnvironment(selected.Executable, selected.Version);
} else {
toolchain.ClearEnvironmentOverride();
}
}),
environments.OnDidChangeEnvironments(() => environmentView.Refresh()),
// 进程状态变化(含手动停止)会影响 Environments 视图的空态文案与状态栏。
environmentTool.OnDidChangeState(() => environmentView.Refresh()),
environments.OnDidChangeSelection((selected) => {
if (selected) {
toolchain.ApplyEnvironment(selected.Executable, selected.Version);
} else {
toolchain.ClearEnvironmentOverride();
}
environmentView.Refresh();
})
);
// 选择列表里的「选择编译器…」:手动输入路径 → VET check → 并入环境清单并切过去。
environments.SetManualCompilerPicker(() => PickManualCompiler(environmentTool, environments));
// 命令。
context.subscriptions.push(
RegisterCommandSafe(context, 'viper.createProject', async () => {
@@ -72,6 +129,22 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
projectView.Refresh();
toolchainView.Refresh();
settingsView.Refresh();
environmentView.Refresh();
}),
RegisterCommandSafe(context, 'viper.selectEnvironment', async (environmentId?: unknown) => {
await environments.SelectEnvironment(typeof environmentId === 'string' ? environmentId : undefined);
}),
RegisterCommandSafe(context, 'viper.restartEnvironmentTool', () => {
environmentTool.Restart();
}),
RegisterCommandSafe(context, 'viper.stopEnvironmentTool', () => {
environmentTool.Stop();
}),
RegisterCommandSafe(context, 'viper.showEnvironmentLog', () => {
environmentTool.ShowLog();
}),
RegisterCommandSafe(context, 'viper.checkEnvironment', async () => {
await CheckEnvironmentCommand(environmentTool);
}),
RegisterCommandSafe(context, 'viper.openVpjAsJson', async (uri?: import('vscode').Uri) => {
const target = uri ?? window.activeTextEditor?.document.uri;
@@ -81,18 +154,30 @@ export async function activate(context: ExtensionContext): Promise<ViperApiImpl>
}
// 用默认文本编辑器打开,绕过自定义 webview 编辑器。
await window.showTextDocument(target, { preview: false });
}),
RegisterCommandSafe(context, 'viper.countLines', async (uri?: unknown) => {
await CountProjectLocCommand(context, projects, locChannel, IsUri(uri) ? uri : undefined);
})
);
// 监听配置变更,刷新工具链。
// 监听配置变更,刷新工具链 / 重启 VET。
context.subscriptions.push(
workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('viper.toolchain')) {
toolchain.Refresh();
}
if (e.affectsConfiguration('viper.environmentTool')) {
environmentTool.Restart();
}
})
);
// 生命周期:随扩展一起释放(dispose 会停掉 VET 进程并关闭日志流)。
context.subscriptions.push(environmentTool, environments);
// 启动 VET 扫盘发现。
environmentTool.Start();
// 导出 API。
const api = new ViperApiImpl(projects, toolchain);
return api;
@@ -170,6 +255,119 @@ async function CreateProjectCommand(): Promise<void> {
window.showInformationMessage(`Created ${uri.fsPath}`);
}
/**
* 弹文件选择框挑一个 exe,并做 Viper 编译器校验。
* 校验优先走 VET 的 check 通道;VET 未启用/未运行时由扩展本地模拟(读 PE + `--version`,均带超时)。
* 返回回执;用户取消时返回 undefined(校验失败会在这里给出错误提示)。
*/
async function PickAndCheckExecutable(tool: EnvironmentTool): Promise<ViperCheckResult | undefined> {
const picked = await window.showOpenDialog({
title: '选择 Viper 编译器',
canSelectMany: false,
openLabel: '选择',
filters: { 'Viper 可执行文件': ['exe'] },
});
const exePath = picked?.[0]?.fsPath;
if (!exePath) {
return undefined;
}
return window.withProgress(
{ location: ProgressLocation.Notification, title: `正在检查 ${path.basename(exePath)}…` },
async () => {
try {
return await tool.CheckExecutable(exePath);
} catch (e) {
void window.showErrorMessage(`检查失败:${e instanceof Error ? e.message : String(e)}`);
return undefined;
}
}
);
}
/**
* 「选择编译器…」:选一个 exe,校验通过后并入环境清单并立刻切换过去
* (走 VET check 时 VET 会把该 exe 入库,后续轮次快照里也会带着它)。
*/
async function PickManualCompiler(
tool: EnvironmentTool,
environments: EnvironmentManager
): Promise<ViperEnvironment | undefined> {
const result = await PickAndCheckExecutable(tool);
if (!result) {
return undefined;
}
const environment = result.Environment;
if (result.Ok && environment) {
environments.AddEnvironment(environment);
const selected = await environments.SelectEnvironment(environment.Id);
void window.showInformationMessage(`已切换到 ${environment.DisplayName}(${environment.Arch})`);
return selected;
}
void window.showErrorMessage(`检查未通过:${DescribeCheckMessage(result.Message)}`);
return undefined;
}
/** 挑一个 exe 做检查(只检查、不切换),结果通过信息消息回报。 */
async function CheckEnvironmentCommand(tool: EnvironmentTool): Promise<void> {
const result = await PickAndCheckExecutable(tool);
if (!result) {
return;
}
const environment = result.Environment;
if (result.Ok && environment) {
void window
.showInformationMessage(
`检查通过:${environment.DisplayName}(${environment.Arch})`,
'选择为当前环境',
'显示详情'
)
.then((choice) => {
if (choice === '选择为当前环境') {
void commands.executeCommand('viper.selectEnvironment', environment.Id);
} else if (choice === '显示详情') {
void window.showInformationMessage(environment.Executable, { modal: true });
}
});
return;
}
void window.showErrorMessage(`检查未通过:${DescribeCheckMessage(result.Message)}`);
}
/** 把校验回执里的原因串(VET 的 check_msg / 本地模拟的同名原因串)翻译成可读说明。 */
function DescribeCheckMessage(message: string | undefined): string {
if (!message) {
return '未知原因(未给出原因串)';
}
switch (message) {
case 'NOT_FOUND':
return '文件不存在';
case 'NO_PERMISSION':
return '无法打开该文件(权限不足或被占用)';
case 'NOT_EXE':
return '不是有效的 PE 可执行文件';
case 'NO_PRVERSN':
return '缺少 .PRVERSN 段(不是 TransPyV 工具链产物)';
case 'PRVERSN_ERR':
return '.PRVERSN 段定位或读取失败';
case 'PREFIX_ERR':
return '--version 输出不以 Viper 开头';
case 'TIMEOUT':
return '--version 探测超时';
case 'CHANNEL_UNAVAILABLE':
return 'VET 的 check 通道不可用(内部缓冲未就绪)';
default:
// EXE_ERR,0x<HEX>:拉起失败,错误码原样带上
return message.startsWith('EXE_ERR,')
? `无法启动该 exe(错误码 ${message.slice('EXE_ERR,'.length)})`
: message;
}
}
/** 显示当前项目信息。 */
function ShowProjectInfoCommand(projects: ProjectManager): void {
const project = projects.GetProject();
@@ -194,3 +392,67 @@ function ShowProjectInfoCommand(projects: ProjectManager): void {
channel.appendLine(lines.join('\n'));
channel.show();
}
/** 判断命令参数是否为 Uri(右键菜单传入的第一个参数)。 */
function IsUri(value: unknown): value is Uri {
return !!value && typeof value === 'object' && typeof (value as Uri).fsPath === 'string';
}
/** 取本命令要统计的工程文件:右键传入的 .vpj → 当前活动编辑器(是 .vpj 时)。 */
function ResolveProjectFile(uri?: Uri): Uri | undefined {
const isProjectFile = (candidate: Uri | undefined): boolean =>
!!candidate && path.extname(candidate.fsPath).toLowerCase() === '.vpj';
if (isProjectFile(uri)) {
return uri;
}
const activeUri = window.activeTextEditor?.document.uri;
return isProjectFile(activeUri) ? activeUri : undefined;
}
/**
* 「统计项目 LOC」:右键 .vpj → 统计该工程源目录(source_dir,通常为 App)下
* 所有已知语言(.vp/.vpi/.vpnci/.vpj/.def)的文件数与行数,
* 结果在 Webview 面板里以进度条 + 图表 + 明细表展示(另存一份纯文本到输出通道)。
* 未传文件参数(命令面板)时退回当前工作区已加载的工程。
*/
async function CountProjectLocCommand(
context: ExtensionContext,
projects: ProjectManager,
channel: OutputChannel,
uri?: Uri
): Promise<void> {
const projectFile = ResolveProjectFile(uri);
// 右键的 .vpj 可能不是 DiscoverProjects 留下(每个工作区只保留最后一个)的那个,
// 故按点击的文件重新解析,保证统计的就是被点中的工程。
const project = projectFile ? projects.LoadProject(projectFile) : projects.GetProject();
if (!project) {
void window.showWarningMessage('未找到 Viper 工程:请对 .vpj 文件使用本命令。');
return;
}
const view = LocReportView.Show(context);
view.StartRun(project.Name, project.ProjectFile, project.SourceDir);
let report: LocReport;
try {
report = await CountProjectLoc(project.SourceDir, [project.BuildDir], (progress) => {
view.ReportProgress(progress);
});
} catch (error) {
view.ReportError(`统计失败:${error instanceof Error ? error.message : String(error)}`);
return;
}
// 纯文本一份留档(便于复制),主力展示在 Webview。
channel.clear();
channel.appendLine(FormatLocReport(report, project.Name, project.ProjectFile));
view.ReportResult({ ...report, ProjectName: project.Name, ProjectFile: project.ProjectFile });
if (report.Total.FileCount > 0) {
void window.showInformationMessage(
`LOC:${report.Total.CodeLines} 行代码 / ${report.Total.TotalLines} 行(${report.Total.FileCount} 个文件,注释率 ${(report.Total.CommentRate * 100).toFixed(1)}%)`
);
}
}
+388
View File
@@ -0,0 +1,388 @@
/*
* lineCounter.ts
* 项目 LOC(代码行)统计:递归扫描工程源目录,按「已知语言」汇总文件数与行数。
*
* 只依赖 Node 的 fs/path,不引用 vscode,便于单独复用与测试;
* 展示层(locReportView.ts)负责进度条、图表与报表渲染。
*/
import * as fs from 'fs';
import * as path from 'path';
/** 一种已知语言:扩展名集合 + 注释语法。 */
export interface KnownLanguage {
/** 语言显示名(报表中的一行)。 */
Name: string;
/** 归属该语言的扩展名(小写,含点)。 */
Extensions: string[];
/** 行注释前缀;无行注释时为空数组。 */
LineCommentPrefixes: string[];
/** 块注释定界符;无块注释时留空。 */
BlockComments?: { Start: string; End: string }[];
}
/**
* 已知语言表(统计范围)。
* Viper 系列与 vscode-vipeye 的 languages 贡献点一致(.vp/.vpi/.vpnci,另有 .vpj 工程文件),
* 并按用户要求支持 .def(定义文件):既可能是 MODULE-DEFINITION 的 ';' 行注释,
* 也可能是 C 风格的 '//' 行注释与块注释,故三者都识别。
*/
export const KnownLanguages: KnownLanguage[] = [
{ Name: 'Viper', Extensions: ['.vp'], LineCommentPrefixes: ['#'] },
{ Name: 'Viper Stub', Extensions: ['.vpi'], LineCommentPrefixes: ['#'] },
{ Name: 'Viper Annotation', Extensions: ['.vpnci'], LineCommentPrefixes: ['#'] },
{ Name: 'Viper Project', Extensions: ['.vpj'], LineCommentPrefixes: [] },
{
Name: 'Definition',
Extensions: ['.def'],
LineCommentPrefixes: [';', '//'],
BlockComments: [{ Start: '/*', End: '*/' }],
},
];
/** 扩展名 → 语言(由 KnownLanguages 展开)。 */
const ExtensionToLanguage = new Map<string, KnownLanguage>();
for (const language of KnownLanguages) {
for (const extension of language.Extensions) {
ExtensionToLanguage.set(extension, language);
}
}
/** 固定跳过的目录名:依赖、版本库元数据、字节码缓存。 */
const SkippedDirectoryNames = new Set(['node_modules', '.git', '__pycache__']);
/** 单语言 / 汇总的行数统计。 */
export interface LineCount {
FileCount: number;
TotalLines: number;
CodeLines: number;
CommentLines: number;
BlankLines: number;
/** 字符数(不含换行符),用于算平均行字符数。 */
Chars: number;
}
/** 派生指标。 */
export interface LocMetrics {
/** 注释率 = 注释行 /(代码行 + 注释行);无非空行时为 0。 */
CommentRate: number;
/** 平均行字符数 = 字符数 / 总行数。 */
AverageLineLength: number;
}
/** 报表的一行(语言明细 / 合计)。 */
export interface LocReportRow extends LineCount, LocMetrics {
Language: string;
}
/** 一次 LOC 统计的结果。 */
export interface LocReport {
/** 被扫描的源目录(绝对路径)。 */
SourceDir: string;
/** 扫描时跳过的子目录(相对源目录的路径)。 */
SkippedDirectories: string[];
/** 按语言的汇总(代码行降序,只含确实存在文件的语言)。 */
Languages: LocReportRow[];
/** 全部已知语言的合计。 */
Total: LocReportRow;
}
/** 扫描进度(用于进度条)。 */
export interface LocScanProgress {
/** 已统计完成的文件数。 */
ProcessedFiles: number;
/** 待统计的已知语言文件总数。 */
TotalFiles: number;
/** 当前文件(相对源目录)。 */
CurrentFile: string;
}
/** 行分类:代码 / 注释 / 空行。 */
type LineKind = 'code' | 'comment' | 'blank';
/** 逐行扫描的跨行状态(块注释未闭合时的结束定界符)。 */
interface ScanState {
InBlockCommentEnd: string | undefined;
}
/** 待统计的文件。 */
interface ScannedFile {
FilePath: string;
Language: KnownLanguage;
}
/** 创建全 0 的行数计数。 */
export function CreateEmptyLineCount(): LineCount {
return { FileCount: 0, TotalLines: 0, CodeLines: 0, CommentLines: 0, BlankLines: 0, Chars: 0 };
}
/** 把 source 累加进 target(原地)。 */
export function AccumulateLineCount(target: LineCount, source: LineCount): void {
target.FileCount += source.FileCount;
target.TotalLines += source.TotalLines;
target.CodeLines += source.CodeLines;
target.CommentLines += source.CommentLines;
target.BlankLines += source.BlankLines;
target.Chars += source.Chars;
}
/** 注释率 = 注释行 /(代码行 + 注释行)。 */
export function ComputeCommentRate(count: LineCount): number {
const nonBlankLines = count.CodeLines + count.CommentLines;
return nonBlankLines > 0 ? count.CommentLines / nonBlankLines : 0;
}
/** 平均行字符数 = 字符数 / 总行数。 */
export function ComputeAverageLineLength(count: LineCount): number {
return count.TotalLines > 0 ? count.Chars / count.TotalLines : 0;
}
/** 取文件所属的已知语言;未登记(或没有扩展名)返回 undefined。 */
export function GetKnownLanguage(filePath: string): KnownLanguage | undefined {
const extension = path.extname(filePath).toLowerCase();
if (!extension) {
return undefined;
}
return ExtensionToLanguage.get(extension);
}
/**
* 统计一段文本的行数(纯函数,按语言的注释语法分类)。
* 行尾的最后一个换行不算作额外空行;跨行的块注释正确归为注释。
*/
export function CountTextLines(content: string, language: KnownLanguage): LineCount {
const count = CreateEmptyLineCount();
count.FileCount = 1;
const lines = content.split(/\r\n|\r|\n/);
// "a\n" 切出 ['a', '']:结尾换行不产生额外空行。
if (lines.length > 0 && lines[lines.length - 1] === '') {
lines.pop();
}
const state: ScanState = { InBlockCommentEnd: undefined };
for (const line of lines) {
count.TotalLines += 1;
count.Chars += line.length;
switch (ClassifyLine(line, language, state)) {
case 'code':
count.CodeLines += 1;
break;
case 'comment':
count.CommentLines += 1;
break;
default:
count.BlankLines += 1;
break;
}
}
return count;
}
/** 读取并统计单个文件;读取失败按全 0 计(不中断整次统计)。 */
export async function CountFileLines(filePath: string, language: KnownLanguage): Promise<LineCount> {
try {
const content = await fs.promises.readFile(filePath, 'utf8');
return CountTextLines(content, language);
} catch {
return CreateEmptyLineCount();
}
}
/**
* 统计工程源目录下所有已知语言的行数。
* extraSkipDirectories 传工程构建目录(源目录内的构建产物不参与统计)。
* onProgress 每读完一个文件回调一次(先回调一次 ProcessedFiles = 0 的总数播报)。
*/
export async function CountProjectLoc(
sourceDir: string,
extraSkipDirectories: string[] = [],
onProgress?: (progress: LocScanProgress) => void
): Promise<LocReport> {
const skippedDirectories: string[] = [];
const files: ScannedFile[] = [];
const extraSkipPaths = new Set(extraSkipDirectories.map((directory) => path.resolve(directory)));
// 阶段一:先收集文件清单,得到总数(进度条的分母)。
await CollectKnownFiles(sourceDir, sourceDir, extraSkipPaths, skippedDirectories, files);
const totalFiles = files.length;
onProgress?.({ ProcessedFiles: 0, TotalFiles: totalFiles, CurrentFile: '' });
// 阶段二:逐个统计并汇报进度。
const counts = new Map<string, LineCount>();
for (const [index, file] of files.entries()) {
const current = counts.get(file.Language.Name) ?? CreateEmptyLineCount();
AccumulateLineCount(current, await CountFileLines(file.FilePath, file.Language));
counts.set(file.Language.Name, current);
onProgress?.({
ProcessedFiles: index + 1,
TotalFiles: totalFiles,
CurrentFile: path.relative(sourceDir, file.FilePath),
});
}
// 只保留确实存在文件的语言,按代码行降序。
const languages: LocReportRow[] = [];
for (const language of KnownLanguages) {
const count = counts.get(language.Name);
if (count && count.FileCount > 0) {
languages.push(CreateReportRow(language.Name, count));
}
}
languages.sort((left, right) => right.CodeLines - left.CodeLines);
const total = CreateEmptyLineCount();
for (const entry of languages) {
AccumulateLineCount(total, entry);
}
return {
SourceDir: sourceDir,
SkippedDirectories: skippedDirectories,
Languages: languages,
Total: CreateReportRow('Total', total),
};
}
/** 生成可读报表(等宽对齐,供输出通道展示)。 */
export function FormatLocReport(report: LocReport, projectName: string, projectFile: string): string {
const columnWidth = 12;
const nameWidth = Math.max(
18,
'Language'.length,
'Total'.length,
...report.Languages.map((entry) => entry.Language.length)
);
const formatRow = (name: string, count: LineCount & LocMetrics): string =>
name.padEnd(nameWidth) +
String(count.FileCount).padStart(columnWidth) +
String(count.TotalLines).padStart(columnWidth) +
String(count.CodeLines).padStart(columnWidth) +
String(count.CommentLines).padStart(columnWidth) +
String(count.BlankLines).padStart(columnWidth) +
`${(count.CommentRate * 100).toFixed(1)}%`.padStart(columnWidth) +
count.AverageLineLength.toFixed(1).padStart(columnWidth);
const header =
'Language'.padEnd(nameWidth) +
'Files'.padStart(columnWidth) +
'Total'.padStart(columnWidth) +
'Code'.padStart(columnWidth) +
'Comment'.padStart(columnWidth) +
'Blank'.padStart(columnWidth) +
'Comment%'.padStart(columnWidth) +
'AvgLen'.padStart(columnWidth);
const lines: string[] = [
'Viper LOC 统计',
`项目: ${projectName}`,
`工程文件: ${projectFile}`,
`源目录: ${report.SourceDir}`,
`跳过目录: ${report.SkippedDirectories.length > 0 ? report.SkippedDirectories.join(', ') : '(none)'}`,
'',
header,
'-'.repeat(header.length),
...report.Languages.map((entry) => formatRow(entry.Language, entry)),
'-'.repeat(header.length),
formatRow('Total', report.Total),
];
return lines.join('\n');
}
/** 由行数计数构造带派生指标的报表行。 */
function CreateReportRow(language: string, count: LineCount): LocReportRow {
return {
Language: language,
...count,
CommentRate: ComputeCommentRate(count),
AverageLineLength: ComputeAverageLineLength(count),
};
}
/** 递归收集源目录下所有已知语言的文件(跳过目录与符号链接)。 */
async function CollectKnownFiles(
directory: string,
sourceDir: string,
extraSkipPaths: Set<string>,
skippedDirectories: string[],
files: ScannedFile[]
): Promise<void> {
let entries: fs.Dirent[];
try {
entries = await fs.promises.readdir(directory, { withFileTypes: true });
} catch {
// 无权限 / 被占用:跳过该目录,不中断整次统计。
return;
}
for (const entry of entries) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (ShouldSkipDirectory(entry.name, entryPath, extraSkipPaths)) {
skippedDirectories.push(path.relative(sourceDir, entryPath) || entry.name);
continue;
}
await CollectKnownFiles(entryPath, sourceDir, extraSkipPaths, skippedDirectories, files);
continue;
}
// 符号链接一律跳过:避免环与重复计数。
if (!entry.isFile()) {
continue;
}
const language = GetKnownLanguage(entryPath);
if (language) {
files.push({ FilePath: entryPath, Language: language });
}
}
}
/** 目录是否不参与统计:依赖/元数据目录、隐藏目录(含 .tpv_build 这类构建产物)、工程构建目录。 */
function ShouldSkipDirectory(name: string, entryPath: string, extraSkipPaths: Set<string>): boolean {
return SkippedDirectoryNames.has(name) || name.startsWith('.') || extraSkipPaths.has(entryPath);
}
/** 判断单行的类别;块注释状态在 state 上跨行传递。 */
function ClassifyLine(text: string, language: KnownLanguage, state: ScanState): LineKind {
let rest = text;
let sawComment = false;
for (;;) {
if (state.InBlockCommentEnd !== undefined) {
const endIndex = rest.indexOf(state.InBlockCommentEnd);
if (endIndex < 0) {
// 整行仍在块注释内。
return 'comment';
}
rest = rest.slice(endIndex + state.InBlockCommentEnd.length);
state.InBlockCommentEnd = undefined;
sawComment = true;
continue;
}
const trimmed = rest.trimStart();
if (trimmed.length === 0) {
return sawComment ? 'comment' : 'blank';
}
if (language.LineCommentPrefixes.some((prefix) => trimmed.startsWith(prefix))) {
return 'comment';
}
const blockComment = (language.BlockComments ?? []).find((block) => trimmed.startsWith(block.Start));
if (blockComment) {
state.InBlockCommentEnd = blockComment.End;
rest = trimmed.slice(blockComment.Start.length);
sawComment = true;
continue;
}
return 'code';
}
}
+717
View File
@@ -0,0 +1,717 @@
/*
* locReportView.ts
* 「统计项目 LOC」报告面板:统计进度条 + KPI + 语言筛选 + 行数构成图 + 明细表。
*
* 数据由 lineCounter.ts 计算,本文件只负责展示:
* - 面板单例复用(再次统计只刷新同一面板);
* - 骨架 HTML 内联脚本(packages/viper 不在打包器的资源复制清单里,
* 外部脚本要改 rspack 配置才能落地,故用 nonce 内联脚本);
* - 数据经 postMessage 下发,Webview 脚本就绪(ready)前的数据先缓存。
*/
import { randomBytes } from 'crypto';
import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from 'vscode';
import { LocReport, LocScanProgress } from './lineCounter';
/** 报表头部信息(工程名 / 工程文件 / 源目录)。 */
export interface LocReportHeader {
ProjectName: string;
ProjectFile: string;
SourceDir: string;
}
/** 下发给 Webview 的报表数据。 */
export type LocReportPayload = LocReport & LocReportHeader;
/** 宿主 → Webview 的消息。 */
type ReportMessage =
| { type: 'header'; payload: LocReportHeader }
| { type: 'progress'; payload: LocScanProgress }
| { type: 'report'; payload: LocReportPayload }
| { type: 'error'; payload: { message: string } };
/** 两次进度上报之间的最小间隔(毫秒):文件多时不至于刷爆消息通道。 */
const ProgressPostIntervalMs = 40;
export class LocReportView implements Disposable {
private static currentPanel: LocReportView | undefined;
private readonly panel: WebviewPanel;
private readonly listeners: Disposable[] = [];
private scriptReady = false;
private header: LocReportHeader | undefined;
private progress: LocScanProgress | undefined;
private payload: LocReportPayload | undefined;
private lastProgressPostAt = 0;
private constructor(panel: WebviewPanel) {
this.panel = panel;
panel.webview.options = { enableScripts: true };
panel.webview.html = BuildLocReportHtml(CreateNonce());
this.listeners.push(
panel.webview.onDidReceiveMessage((message: { type?: string } | undefined) => {
if (message?.type === 'ready') {
this.scriptReady = true;
// 脚本刚就绪:把已缓存的状态补齐,避免丢失 ready 之前的下发。
this.PostHeader();
this.PostProgressNow();
this.PostReport();
}
}),
panel.onDidDispose(() => this.Dispose())
);
}
/** 打开(或复用)报告面板。 */
static Show(context: ExtensionContext): LocReportView {
if (LocReportView.currentPanel) {
LocReportView.currentPanel.panel.reveal();
return LocReportView.currentPanel;
}
const panel = window.createWebviewPanel(
'viper.locReport',
'Viper LOC',
ViewColumn.Active,
{ enableScripts: true, retainContextWhenHidden: true }
);
const view = new LocReportView(panel);
LocReportView.currentPanel = view;
context.subscriptions.push(view);
return view;
}
/** 开始一轮统计:面板切回进度态并清空上一轮报表。 */
StartRun(projectName: string, projectFile: string, sourceDir: string): void {
this.header = { ProjectName: projectName, ProjectFile: projectFile, SourceDir: sourceDir };
this.progress = { ProcessedFiles: 0, TotalFiles: 0, CurrentFile: '' };
this.payload = undefined;
this.panel.title = `Viper LOC · ${projectName}`;
this.PostHeader();
this.PostProgressNow();
}
/** 上报扫描进度(按间隔节流,但每轮最后一个文件必发)。 */
ReportProgress(progress: LocScanProgress): void {
this.progress = progress;
const isLastFile = progress.TotalFiles > 0 && progress.ProcessedFiles >= progress.TotalFiles;
if (!isLastFile && Date.now() - this.lastProgressPostAt < ProgressPostIntervalMs) {
return;
}
this.PostProgressNow();
}
/** 下发报表。 */
ReportResult(payload: LocReportPayload): void {
this.payload = payload;
this.PostReport();
}
/** 下发错误(源目录不存在等)。 */
ReportError(message: string): void {
this.Post({ type: 'error', payload: { message } });
}
dispose(): void {
this.Dispose();
}
private Dispose(): void {
if (LocReportView.currentPanel === this) {
LocReportView.currentPanel = undefined;
}
for (const listener of this.listeners) {
listener.dispose();
}
this.listeners.length = 0;
this.scriptReady = false;
}
private PostHeader(): void {
if (this.header) {
this.Post({ type: 'header', payload: this.header });
}
}
private PostProgressNow(): void {
this.lastProgressPostAt = Date.now();
if (this.progress) {
this.Post({ type: 'progress', payload: this.progress });
}
}
private PostReport(): void {
if (this.payload) {
this.Post({ type: 'report', payload: this.payload });
}
}
private Post(message: ReportMessage): void {
if (!this.scriptReady) {
return;
}
void this.panel.webview.postMessage(message);
}
}
/** 生成 Webview 脚本用的 nonce。 */
function CreateNonce(): string {
return randomBytes(16).toString('hex');
}
/** 面板骨架:静态 HTML + 内联样式 + 内联脚本(数据随后经 postMessage 下发)。 */
function BuildLocReportHtml(nonce: string): string {
const contentSecurityPolicy = [
`default-src 'none'`,
`style-src 'unsafe-inline'`,
`script-src 'nonce-${nonce}'`,
].join('; ');
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="${contentSecurityPolicy}" />
<title>Viper LOC</title>
<style>
:root {
--seg-code: var(--vscode-charts-blue, #3794ff);
--seg-comment: var(--vscode-charts-green, #89d185);
--seg-blank: var(--vscode-charts-orange, #d18616);
--line: var(--vscode-panel-border, rgba(128, 128, 128, 0.35));
}
body {
margin: 0;
padding: 0 0 24px;
color: var(--vscode-foreground);
background: var(--vscode-editor-background);
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
}
header {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--line);
}
h1 {
margin: 0 0 6px;
font-size: 1.35em;
font-weight: 600;
}
.meta {
opacity: 0.75;
line-height: 1.6;
word-break: break-all;
}
section {
padding: 14px 16px 0;
}
.card {
border: 1px solid var(--line);
border-radius: 6px;
padding: 12px 14px;
margin-bottom: 12px;
background: var(--vscode-editorWidget-background, rgba(128, 128, 128, 0.06));
}
.card-title {
font-weight: 600;
margin-bottom: 10px;
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
}
.card-title .hint {
font-weight: 400;
opacity: 0.7;
}
/* 进度条 */
.bar {
height: 8px;
border-radius: 4px;
overflow: hidden;
background: var(--vscode-input-background, rgba(128, 128, 128, 0.2));
}
#progress-fill {
height: 100%;
width: 0;
background: var(--seg-code);
transition: width 90ms linear;
}
#progress-text {
margin-top: 8px;
font-variant-numeric: tabular-nums;
}
#progress-file {
margin-top: 4px;
opacity: 0.6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* KPI */
#kpis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
gap: 10px;
}
.kpi {
border: 1px solid var(--line);
border-radius: 6px;
padding: 8px 12px;
}
.kpi-value {
font-size: 1.5em;
font-weight: 600;
font-variant-numeric: tabular-nums;
line-height: 1.25;
}
.kpi-label {
opacity: 0.7;
margin-top: 2px;
}
/* 语言筛选 */
#filters {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
}
.filter {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.legend {
display: inline-flex;
align-items: center;
gap: 6px;
opacity: 0.85;
}
.swatch {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 2px;
}
.swatch-code { background: var(--seg-code); }
.swatch-comment { background: var(--seg-comment); }
.swatch-blank { background: var(--seg-blank); }
/* 行数构成图 */
.chart-row {
display: grid;
grid-template-columns: minmax(90px, 140px) 1fr minmax(140px, 200px);
align-items: center;
gap: 10px;
padding: 3px 0;
}
.chart-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chart-track {
height: 16px;
border-radius: 3px;
background: var(--vscode-input-background, rgba(128, 128, 128, 0.16));
}
.chart-bar {
display: flex;
height: 100%;
border-radius: 3px;
overflow: hidden;
}
.seg { height: 100%; }
.seg-code { background: var(--seg-code); }
.seg-comment { background: var(--seg-comment); }
.seg-blank { background: var(--seg-blank); }
.chart-value {
font-variant-numeric: tabular-nums;
opacity: 0.85;
text-align: right;
}
/* 明细表 */
table {
width: 100%;
border-collapse: collapse;
font-variant-numeric: tabular-nums;
}
th, td {
padding: 5px 8px;
text-align: left;
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
th.num, td.num { text-align: right; }
thead th {
opacity: 0.8;
font-weight: 600;
border-bottom: 1px solid var(--line);
}
tbody tr:hover { background: var(--vscode-list-hoverBackground, rgba(128, 128, 128, 0.12)); }
tr.total-row { font-weight: 600; }
tr.total-row th, tr.total-row td { border-top: 1px solid var(--line); border-bottom: none; }
.empty-row {
opacity: 0.7;
padding: 6px 0;
}
#empty, #error {
padding: 16px;
line-height: 1.7;
}
#error { color: var(--vscode-errorForeground); white-space: pre-wrap; }
[hidden] { display: none !important; }
</style>
</head>
<body>
<header>
<h1 id="title">Viper LOC 统计</h1>
<div class="meta">
<div id="meta-source"></div>
<div id="meta-file"></div>
<div id="meta-skipped"></div>
</div>
</header>
<section id="progress-card">
<div class="card">
<div class="card-title">正在统计<span class="hint">按已知语言递归扫描源目录</span></div>
<div class="bar"><div id="progress-fill"></div></div>
<div id="progress-text">正在收集文件…</div>
<div id="progress-file"></div>
</div>
</section>
<section id="report" hidden>
<div class="card">
<div class="card-title">概览<span class="hint" id="kpi-scope"></span></div>
<div id="kpis"></div>
</div>
<div class="card">
<div class="card-title">语言筛选<span class="hint">勾选参与统计的语言(概览、图表与表格同步过滤)</span></div>
<div id="filters"></div>
</div>
<div class="card">
<div class="card-title">
行数构成
<span class="legend"><span class="swatch swatch-code"></span>代码</span>
<span class="legend"><span class="swatch swatch-comment"></span>注释</span>
<span class="legend"><span class="swatch swatch-blank"></span>空行</span>
</div>
<div id="chart"></div>
</div>
<div class="card">
<div class="card-title">语言明细</div>
<table>
<thead>
<tr>
<th>语言</th>
<th class="num">文件数</th>
<th class="num">总行数</th>
<th class="num">代码行</th>
<th class="num">注释行</th>
<th class="num">空行</th>
<th class="num">注释率</th>
<th class="num">平均行长</th>
<th class="num">字符数</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
</section>
<div id="empty" hidden>源目录下没有已知语言的文件。</div>
<div id="error" hidden></div>
<script nonce="${nonce}">
(function () {
'use strict';
var vscode = acquireVsCodeApi();
var state = { header: null, progress: null, report: null, selected: {} };
function el(id) {
return document.getElementById(id);
}
function setText(id, value) {
el(id).textContent = value === undefined || value === null ? '' : String(value);
}
function num(value) {
return Number(value || 0).toLocaleString('en-US');
}
function pct(value) {
return (value * 100).toFixed(1) + '%';
}
/** 注释率 = 注释行 /(代码行 + 注释行)。 */
function commentRate(count) {
var nonBlank = count.CodeLines + count.CommentLines;
return nonBlank > 0 ? count.CommentLines / nonBlank : 0;
}
/** 平均行字符数 = 字符数 / 总行数。 */
function averageLineLength(count) {
return count.TotalLines > 0 ? count.Chars / count.TotalLines : 0;
}
function emptyCount() {
return { FileCount: 0, TotalLines: 0, CodeLines: 0, CommentLines: 0, BlankLines: 0, Chars: 0 };
}
function sumCounts(rows) {
var total = emptyCount();
rows.forEach(function (row) {
total.FileCount += row.FileCount;
total.TotalLines += row.TotalLines;
total.CodeLines += row.CodeLines;
total.CommentLines += row.CommentLines;
total.BlankLines += row.BlankLines;
total.Chars += row.Chars;
});
return total;
}
function makeNode(tag, className, text) {
var node = document.createElement(tag);
if (className) {
node.className = className;
}
if (text !== undefined) {
node.textContent = text;
}
return node;
}
function setHidden(id, hidden) {
if (hidden) {
el(id).setAttribute('hidden', '');
} else {
el(id).removeAttribute('hidden');
}
}
function renderHeader() {
var header = state.header;
if (!header) {
return;
}
setText('title', 'LOC 统计 · ' + header.ProjectName);
setText('meta-source', '项目 ' + header.ProjectName + ' · 源目录 ' + header.SourceDir);
setText('meta-file', '工程文件 ' + header.ProjectFile);
}
function renderProgress() {
var progress = state.progress || { ProcessedFiles: 0, TotalFiles: 0, CurrentFile: '' };
var ratio = progress.TotalFiles > 0 ? progress.ProcessedFiles / progress.TotalFiles : 0;
el('progress-fill').style.width = (ratio * 100).toFixed(2) + '%';
setText(
'progress-text',
progress.TotalFiles > 0
? '已统计 ' + num(progress.ProcessedFiles) + ' / ' + num(progress.TotalFiles) + ' 个文件(' + pct(ratio) + ')'
: '正在收集文件…'
);
setText('progress-file', progress.CurrentFile || '');
}
function renderKpi(rows) {
var totals = sumCounts(rows);
var cards = [
['文件数', num(totals.FileCount)],
['总行数', num(totals.TotalLines)],
['代码行', num(totals.CodeLines)],
['注释行', num(totals.CommentLines)],
['空行', num(totals.BlankLines)],
['注释率', pct(commentRate(totals))],
['平均行字符数', averageLineLength(totals).toFixed(1)],
['字符总数', num(totals.Chars)]
];
var host = el('kpis');
host.textContent = '';
cards.forEach(function (card) {
var node = makeNode('div', 'kpi');
node.appendChild(makeNode('div', 'kpi-value', card[1]));
node.appendChild(makeNode('div', 'kpi-label', card[0]));
host.appendChild(node);
});
}
function renderFilters(languages) {
var host = el('filters');
host.textContent = '';
languages.forEach(function (row) {
var label = makeNode('label', 'filter');
var input = document.createElement('input');
input.type = 'checkbox';
input.checked = !!state.selected[row.Language];
input.addEventListener('change', function () {
state.selected[row.Language] = input.checked;
renderReport();
});
label.appendChild(input);
label.appendChild(document.createTextNode(row.Language + '(' + num(row.FileCount) + ' 个文件)'));
host.appendChild(label);
});
}
function renderChart(rows) {
var host = el('chart');
host.textContent = '';
if (rows.length === 0) {
host.appendChild(makeNode('div', 'empty-row', '未选择任何语言'));
return;
}
var maxTotal = 1;
rows.forEach(function (row) {
maxTotal = Math.max(maxTotal, row.TotalLines);
});
rows.forEach(function (row) {
var line = makeNode('div', 'chart-row');
line.appendChild(makeNode('div', 'chart-label', row.Language));
var track = makeNode('div', 'chart-track');
var bar = makeNode('div', 'chart-bar');
bar.style.width = ((row.TotalLines / maxTotal) * 100).toFixed(2) + '%';
[
['code', row.CodeLines],
['comment', row.CommentLines],
['blank', row.BlankLines]
].forEach(function (part) {
var segment = makeNode('div', 'seg seg-' + part[0]);
segment.style.width =
row.TotalLines > 0 ? ((part[1] / row.TotalLines) * 100).toFixed(2) + '%' : '0%';
bar.appendChild(segment);
});
track.appendChild(bar);
line.appendChild(track);
line.appendChild(
makeNode('div', 'chart-value', num(row.CodeLines) + ' 代码 / ' + num(row.TotalLines) + ' 行')
);
host.appendChild(line);
});
}
function renderTable(rows, totalRow) {
var host = el('table-body');
host.textContent = '';
var appendRow = function (row, label, isTotal) {
var tr = document.createElement('tr');
if (isTotal) {
tr.className = 'total-row';
}
[
label,
num(row.FileCount),
num(row.TotalLines),
num(row.CodeLines),
num(row.CommentLines),
num(row.BlankLines),
pct(commentRate(row)),
averageLineLength(row).toFixed(1),
num(row.Chars)
].forEach(function (value, index) {
var cell = document.createElement(index === 0 ? 'th' : 'td');
cell.textContent = value;
if (index > 0) {
cell.className = 'num';
}
tr.appendChild(cell);
});
host.appendChild(tr);
};
rows.forEach(function (row) {
appendRow(row, row.Language, false);
});
appendRow(totalRow, '合计', true);
}
function renderReport() {
var report = state.report;
if (!report) {
return;
}
report.Languages.forEach(function (row) {
if (state.selected[row.Language] === undefined) {
state.selected[row.Language] = true;
}
});
var selectedRows = report.Languages.filter(function (row) {
return state.selected[row.Language];
});
setText('meta-skipped', '跳过目录 ' + (report.SkippedDirectories.length > 0 ? report.SkippedDirectories.join('、') : '(无)'));
setText('kpi-scope', selectedRows.length === report.Languages.length ? '全部语言' : '已选 ' + selectedRows.length + ' / ' + report.Languages.length + ' 种语言');
renderKpi(selectedRows);
renderFilters(report.Languages);
renderChart(selectedRows);
renderTable(selectedRows, Object.assign({ Language: 'Total' }, sumCounts(selectedRows)));
setHidden('progress-card', true);
setHidden('report', false);
setHidden('empty', report.Languages.length > 0);
setHidden('error', true);
}
window.addEventListener('message', function (event) {
var message = event.data;
if (!message || !message.type) {
return;
}
if (message.type === 'header') {
state.header = message.payload || null;
state.progress = { ProcessedFiles: 0, TotalFiles: 0, CurrentFile: '' };
state.report = null;
state.selected = {};
renderHeader();
setHidden('progress-card', false);
setHidden('report', true);
setHidden('empty', true);
setHidden('error', true);
renderProgress();
return;
}
if (message.type === 'progress') {
state.progress = message.payload;
renderProgress();
return;
}
if (message.type === 'report') {
state.report = message.payload;
state.selected = {};
renderReport();
return;
}
if (message.type === 'error') {
setText('error', message.payload && message.payload.message ? message.payload.message : '统计失败');
setHidden('progress-card', true);
setHidden('report', true);
setHidden('empty', true);
setHidden('error', false);
}
});
vscode.postMessage({ type: 'ready' });
})();
</script>
</body>
</html>`;
}
+87 -17
View File
@@ -1,35 +1,105 @@
/*
* statusBar.ts
* 右下角状态栏:显示当前 Viper 编译器路径与版本。
* 右下角状态栏:显示 VET 发现的 Viper 环境与当前生效项。
*/
import { StatusBarAlignment, StatusBarItem, window } from 'vscode';
import { ToolchainManager } from './toolchainManager';
import { Disposable } from 'vscode';
import { Disposable, StatusBarAlignment, StatusBarItem, window } from 'vscode';
import { EnvironmentManager } from './environmentManager';
import { EnvironmentTool, EnvironmentToolState } from './environmentTool';
export class StatusBarManager implements Disposable {
/**
* 环境状态栏:显示 VET 发现/选中了哪个编译器,点击打开环境选择器。
* 这里只保留这一项:编译器位置完全由 VET 发现结果决定,不再显示硬编码路径。
*/
export class EnvironmentStatusBar implements Disposable {
private readonly item: StatusBarItem;
private readonly toolchain: ToolchainManager;
private readonly disposable: Disposable;
private readonly disposables: Disposable[] = [];
constructor(toolchain: ToolchainManager) {
this.toolchain = toolchain;
this.item = window.createStatusBarItem(StatusBarAlignment.Right, 100);
this.item.command = 'viper.selectToolchain';
constructor(private readonly environments: EnvironmentManager, private readonly tool: EnvironmentTool) {
this.item = window.createStatusBarItem(StatusBarAlignment.Right, 99);
this.item.command = 'viper.selectEnvironment';
this.disposables.push(
environments.OnDidChangeEnvironments(() => this.Update()),
environments.OnDidChangeSelection(() => this.Update()),
tool.OnDidChangeState(() => this.Update())
);
this.disposable = toolchain.OnDidChangeToolchain(() => this.Update());
this.Update();
this.item.show();
}
private Update(): void {
const tool = this.toolchain.GetToolchain();
this.item.text = `$(tools) Viper: ${tool.Path} (${tool.Version})`;
this.item.tooltip = `Viper Toolchain\nPath: ${tool.Path}\nVersion: ${tool.Version}\nSource: ${tool.Source}\nClick to change`;
const state = this.tool.GetState();
if (state === 'disabled') {
this.item.hide();
return;
}
const selected = this.environments.GetSelectedEnvironment();
const count = this.environments.GetEnvironments().length;
const stateText = DescribeState(state);
// 点击状态栏始终打开选择列表:VET 关闭时也一样 —— 已发现的环境仍可选用,
// 需要重启 VET 请走命令面板的「Restart VET」。
this.item.command = 'viper.selectEnvironment';
if (state === 'stopped') {
this.item.text = '$(debug-stop) VET 已关闭';
} else if (selected) {
this.item.text = `$(server-environment) Viper 环境: ${selected.DisplayName}`;
} else if (count > 0) {
this.item.text = `$(server-environment) Viper 环境: 未选择 (${count})`;
} else if (state === 'running' || state === 'starting') {
this.item.text = '$(sync~spin) Viper 环境: 扫描中…';
} else {
this.item.text = `$(warning) Viper 环境: ${stateText}`;
}
this.item.tooltip = [
`VET 状态: ${stateText}`,
`发现环境: ${count}`,
selected ? `当前选中: ${selected.DisplayName}\n${selected.Executable}` : '当前选中: (无)',
`VET: ${this.tool.GetExecutablePath()}`,
'输出: Vipeye VET 输出通道',
DescribePickHint(state),
].join('\n');
this.item.show();
}
dispose(): void {
this.item.dispose();
this.disposable.dispose();
for (const disposable of this.disposables) {
disposable.dispose();
}
}
}
/**
* 状态栏 tooltip 里的「点击选择编译器」提示。
* VET 没在跑时该入口照样可用:选择时改由扩展**本地**检查该 exe(读 PE + `--version`,带超时),
* 不再像以前那样直接失败,所以提示里要说明这一点。
*/
function DescribePickHint(state: EnvironmentToolState): string {
if (state === 'running') {
return '点击选择编译器';
}
const suffix = '点击选择编译器(VET 未运行,选择时由扩展本地检查该 exe:读 PE + --version)';
return state === 'stopped' ? `${suffix}\n重启 VET 请用命令面板的「Restart VET」` : suffix;
}
function DescribeState(state: EnvironmentToolState): string {
switch (state) {
case 'running':
return '运行中';
case 'starting':
return '启动中';
case 'restarting':
return '重启中';
case 'stopped':
return '已关闭(手动)';
case 'failed':
return '已停止(连续失败)';
default:
return '未启用';
}
}
+28
View File
@@ -41,6 +41,11 @@ export class ToolchainManager {
/** 重新解析当前工具链(配置变更后调用)。 */
Refresh(): void {
// 环境覆盖优先:用户已经用 VET 选了环境时,配置变更不应把它顶掉。
if (this.current.Source === 'env') {
return;
}
const next = this.ResolveToolchain();
if (next.Path !== this.current.Path || next.Version !== this.current.Version) {
this.current = next;
@@ -48,6 +53,29 @@ export class ToolchainManager {
}
}
/**
* 用 VET 选中的环境覆盖当前工具链(仅内存,不写配置、不写 .vpj)。
* 这样现有的工具链视图/状态栏/API 都能立刻反映「我选的编译器」。
*/
ApplyEnvironment(path: string, version: string): void {
if (this.current.Source === 'env' && this.current.Path === path && this.current.Version === version) {
return;
}
this.current = { Path: path, Version: version, Source: 'env' };
this._onDidChange.fire(this.current);
}
/** 清除环境覆盖,回到「配置项 → 硬编码」的解析结果。 */
ClearEnvironmentOverride(): void {
if (this.current.Source !== 'env') {
return;
}
this.current = this.ResolveToolchain();
this._onDidChange.fire(this.current);
}
private ResolveToolchain(): ToolchainInfo {
const config = workspace.getConfiguration('viper.toolchain');
const configuredPath = config.get<string>('path');
+30
View File
@@ -32,6 +32,28 @@ export interface ToolchainInfo {
Source: 'hardcoded' | 'config' | 'env' | 'path';
}
/** VET(Viper Environment Tool)发现的一个编译器条目。字段名与 VET 的 stdout JSON 一致。 */
export interface ViperEnvironment {
Id: string; // VET 分配的稳定 id(GUID)
Executable: string; // 编译器可执行文件绝对路径
Version: string; // 版本字符串(来自 .PRVERSN 段与 --version 前缀)
Arch: string; // 架构,如 x86_64
Source: string; // 来源分类,如 workspace / global / ...
DisplayName: string; // 展示名,如「Viper 0.0.3-dev.428」
}
/** VET check 通道的单次检查回执。 */
export interface ViperCheckResult {
/** 检查是否通过(回执的 check_result)。 */
Ok: boolean;
/** 请求时带的 id;VET 原样回带,仅用于把回执与请求配对。 */
Id: number;
/** 通过时的发现项(回执的 viper 子对象,source 固定为 check)。 */
Environment?: ViperEnvironment;
/** 未通过时的固定原因串(回执的 check_msg,如 NOT_FOUND / NO_PRVERSN)。 */
Message?: string;
}
/** 暴露给其他扩展的 API。 */
export interface ViperApi {
/** 获取当前工作区的项目配置。 */
@@ -43,6 +65,12 @@ export interface ViperApi {
/** 获取当前工具链。 */
GetToolchain(): ToolchainInfo;
/**
* 自动发现编译器自带的 includes 目录(以选中编译器 exe 所在目录为基准向上查找,
* 仅返回真实存在的目录)。工程未声明 includes 时由调用方补进搜索路径。
*/
GetCompilerIncludePaths(): string[];
/** 获取 target triple。 */
GetTargetTriple(workspaceFolder?: string): string | undefined;
@@ -61,5 +89,7 @@ export interface ViperTreeNode {
Icon?: string;
Children?: ViperTreeNode[];
Command?: string;
/** 命令参数(点击该节点时随命令一起传入)。 */
CommandArguments?: unknown[];
ContextValue?: string;
}
+66 -4
View File
@@ -1,12 +1,14 @@
/*
* viperViewProvider.ts
* 左侧活动栏三个视图的 TreeDataProvider 实现。
* 左侧活动栏各视图的 TreeDataProvider 实现。
*/
import { Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from 'vscode';
import { EnvironmentManager } from './environmentManager';
import { EnvironmentTool } from './environmentTool';
import { ProjectManager } from './projectManager';
import { ToolchainManager } from './toolchainManager';
import { ViperTreeNode } from './types';
import { ViperEnvironment, ViperTreeNode } from './types';
/** 通用树视图提供者,按视图 ID 返回不同节点集。 */
export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
@@ -16,7 +18,9 @@ export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
constructor(
private readonly viewId: string,
private readonly projects: ProjectManager,
private readonly toolchain: ToolchainManager
private readonly toolchain: ToolchainManager,
private readonly environments: EnvironmentManager,
private readonly tool: EnvironmentTool
) {}
Refresh(): void {
@@ -31,7 +35,7 @@ export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
item.iconPath = element.Icon;
}
if (element.Command) {
item.command = { command: element.Command, title: element.Label };
item.command = { command: element.Command, title: element.Label, arguments: element.CommandArguments };
}
if (element.ContextValue) {
item.contextValue = element.ContextValue;
@@ -54,6 +58,8 @@ export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
return this.BuildToolchainNodes();
case 'viper.settingsView':
return this.BuildSettingsNodes();
case 'viper.environmentView':
return this.BuildEnvironmentNodes();
default:
return [];
}
@@ -164,6 +170,62 @@ export class ViperViewProvider implements TreeDataProvider<ViperTreeNode> {
];
}
/** 环境视图:按 VET 给出的 source 分组(workspace / global / ...)。 */
private BuildEnvironmentNodes(): ViperTreeNode[] {
const all = this.environments.GetEnvironments();
const selectedId = this.environments.GetSelectedId();
if (all.length === 0) {
// 手动关闭 VET 时不能显示"扫描中…"(那是误导),改为明确告知并引导重新启动。
const stopped = this.tool.GetState() === 'stopped';
return [
{
Label: stopped ? 'VET 已关闭' : '未发现 Viper 编译器',
Description: stopped ? '点击重新启动' : 'VET 扫描中…',
Tooltip: stopped ? 'VET 已被手动停止,点击重新启动扫描' : '点击可重启 VET 扫描',
Icon: stopped ? '$(debug-stop)' : '$(sync~spin)',
Command: 'viper.restartEnvironmentTool',
},
];
}
const groups = new Map<string, ViperEnvironment[]>();
for (const environment of all) {
const source = environment.Source || 'unknown';
const bucket = groups.get(source);
if (bucket) {
bucket.push(environment);
} else {
groups.set(source, [environment]);
}
}
const nodes: ViperTreeNode[] = [];
for (const [source, environments] of groups) {
nodes.push({
Label: source,
Description: `${environments.length}`,
Tooltip: `${environments.length} 个环境(来源:${source})`,
Icon: '$(folder)',
Children: environments.map((environment) => this.ConvertEnvironmentToNode(environment, selectedId)),
});
}
return nodes;
}
private ConvertEnvironmentToNode(environment: ViperEnvironment, selectedId: string | undefined): ViperTreeNode {
const isSelected = environment.Id === selectedId;
return {
Label: environment.DisplayName,
Description: `${environment.Version} · ${environment.Arch}${isSelected ? ' · 已选中' : ''}`,
Tooltip: `${environment.Executable}\n来源: ${environment.Source}\nID: ${environment.Id}`,
Icon: isSelected ? '$(check)' : '$(server-process)',
Command: 'viper.selectEnvironment',
CommandArguments: [environment.Id],
};
}
private ToCollapsibleState(children?: ViperTreeNode[]): TreeItemCollapsibleState {
return children && children.length > 0 ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None;
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "vipeye-internal",
"version": "1.1.411",
"version": "1.1.427",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vipeye-internal",
"version": "1.1.411",
"version": "1.1.427",
"license": "MIT",
"dependencies": {
"@yarnpkg/fslib": "2.10.4",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "vipeye-internal",
"displayName": "vipeye",
"description": "Type checker for the Viper language",
"version": "1.1.412",
"version": "1.1.427",
"license": "MIT",
"private": true,
"files": [
@@ -37,6 +37,26 @@ export interface RequiringAnalysisCount {
cells: number;
}
/**
* Viper 扩展:分析进度的快照,供 LSP 客户端在状态栏显示
* 「C:\xxx\1.vp (4/90) 0.04% loading...」这类进度信息。
*/
export interface AnalysisProgress {
/** 当前正在分析的文件路径(文件系统路径,Windows 下形如 C:\xxx\1.vp)。 */
currentFile: string | undefined;
/** 已分析的用户文件数。 */
analyzed: number;
/** 需要分析的用户文件总数。 */
total: number;
/** 是否已分析完毕(true 表示可以收起状态栏进度项)。 */
done: boolean;
}
export type AnalysisProgressCallback = (progress: AnalysisProgress) => void;
export type AnalysisCompleteCallback = (results: AnalysisResults) => void;
export function analyzeProgram(
@@ -45,7 +65,8 @@ export function analyzeProgram(
configOptions: ConfigOptions,
callback: AnalysisCompleteCallback | undefined,
console: ConsoleInterface,
token: CancellationToken
token: CancellationToken,
progressCallback?: AnalysisProgressCallback
): boolean {
let moreToAnalyze = false;
@@ -55,7 +76,17 @@ export function analyzeProgram(
throwIfCancellationRequested(token);
const duration = new Duration();
moreToAnalyze = program.analyze(maxTime, token);
// 逐文件上报进度(analyze 每检查完一个文件就回一次),
// 让客户端状态栏能实时反映「正在分析哪个文件」。
moreToAnalyze = program.analyze(
maxTime,
token,
progressCallback ? () => progressCallback({ ...program.getAnalysisProgress(), done: false }) : undefined
);
if (progressCallback) {
progressCallback({ ...program.getAnalysisProgress(), done: !moreToAnalyze });
}
const requiringAnalysisCount = program.getFilesToAnalyzeCount();
@@ -17,7 +17,7 @@ import { ServiceProvider } from '../common/serviceProvider';
import '../common/serviceProviderExtensions';
import { Range } from '../common/textRange';
import { Uri } from '../common/uri/uri';
import { AnalysisCompleteCallback, analyzeProgram } from './analysis';
import { AnalysisCompleteCallback, AnalysisProgressCallback, analyzeProgram } from './analysis';
import { ImportResolver } from './importResolver';
import { MaxAnalysisTime, OpenFileOptions, Program } from './program';
import { TypeStubWriter } from './typeStubWriter';
@@ -33,6 +33,7 @@ export class BackgroundAnalysisProgram {
private _program: Program;
private _disposed = false;
private _onAnalysisCompletion: AnalysisCompleteCallback | undefined;
private _onAnalysisProgress: AnalysisProgressCallback | undefined;
private _preEditAnalysis: IBackgroundAnalysis | undefined;
constructor(
@@ -157,6 +158,11 @@ export class BackgroundAnalysisProgram {
this._backgroundAnalysis?.setCompletionCallback(callback);
}
setAnalysisProgressCallback(callback?: AnalysisProgressCallback) {
this._onAnalysisProgress = callback;
this._backgroundAnalysis?.setAnalysisProgressCallback(callback);
}
startAnalysis(token: CancellationToken): boolean {
if (this._backgroundAnalysis) {
this._backgroundAnalysis.startAnalysis(token);
@@ -169,7 +175,8 @@ export class BackgroundAnalysisProgram {
this._configOptions,
this._onAnalysisCompletion,
this._serviceProvider.console(),
token
token,
this._onAnalysisProgress
);
}
@@ -44,6 +44,7 @@ import {
DelNode,
ExceptNode,
ExpressionNode,
FallthroughNode,
ForNode,
FunctionNode,
GlobalNode,
@@ -432,10 +433,25 @@ export class Binder extends ParseTreeWalker {
// 仅当导入的最终目标是目录(而非具体文件)时才标黄。
const lastResolvedUri = importResult.resolvedUris[importResult.resolvedUris.length - 1];
const isFinalTargetDirectory = !lastResolvedUri || lastResolvedUri.isEmpty();
// Viper 扩展:`from . import X` 的请求符号(X)能在目录中经隐式导入解析
//(存在同名 X.vp 文件)时,说明目录作为包已可正常使用,不再报"命名空间包不完整"。
const importFromNode =
node.parent?.nodeType === ParseNodeType.ImportFrom ? (node.parent as ImportFromNode) : undefined;
const allSymbolsResolvable =
importFromNode !== undefined &&
importFromNode.d.imports.length > 0 &&
importFromNode.d.imports.every((importSymbolNode) => {
const importedName = importSymbolNode.d.name.d.value;
return (
importResult.implicitImports?.has(importedName) ||
importResult.filteredImplicitImports?.has(importedName)
);
});
if (
importResult.isImportFound &&
importResult.isNamespacePackage &&
isFinalTargetDirectory &&
!allSymbolsResolvable &&
IsViperDialect(this._fileInfo.executionEnvironment.dialect)
) {
this._fileInfo.diagnosticSink.addDiagnosticWithTextRange(
@@ -532,6 +548,13 @@ export class Binder extends ParseTreeWalker {
this._dunderSlotsEntries = undefined;
if (!this._moduleSymbolOnly) {
// Viper 扩展:`enum X:` 的成员允许只写成员名(裸名 / 逗号分隔的裸名),
// 这不是赋值语句,Python 规则下不会产生声明。
// 这里先补出类作用域声明,使成员访问、语义高亮与枚举语义可用。
if (node.d.isViperEnum) {
this._bindViperEnumMemberNames(node.d.suite);
}
// Analyze the suite.
this.walk(node.d.suite);
}
@@ -1319,7 +1342,11 @@ export class Binder extends ParseTreeWalker {
const postForLabel = this._createBranchLabel();
// Determine if this loop is guaranteed to execute at least once
const isGuaranteedToExecute = this._isNonEmptyListOrTupleLiteral(node.d.iterableExpr);
// Viper 方言(C 风格):for 循环变量在循环后仍视为已定义(保留迭代类型),
// 不产生 Unbound 分支——与 Python 语义(循环可能零次执行 → Unbound)不同。
const isGuaranteedToExecute = IsViperDialect(this._fileInfo.executionEnvironment.dialect)
? true
: this._isNonEmptyListOrTupleLiteral(node.d.iterableExpr);
this._addAntecedent(preForLabel, this._currentFlowNode!);
this._currentFlowNode = preForLabel;
@@ -1422,6 +1449,17 @@ export class Binder extends ParseTreeWalker {
return false;
}
override visitFallthrough(node: FallthroughNode): boolean {
// Viper 扩展:`fallthrough` 结束当前 case 体(控制流交给下一个 case),
// 故本条之后同 case 体内的语句不可达。去向不是「match 之后」,
// 故不像 break 那样挂到 _currentBreakTarget;下一个 case 的入口不建模
// (visitMatch 的 postMatchLabel 依赖「未终止的 case 体」收口)。
this._currentFlowNode = Binder._unreachableStructuralFlowNode;
// Fallthrough nodes don't have any children.
return false;
}
override visitReturn(node: ReturnNode): boolean {
if (this._targetFunctionDeclaration) {
if (!this._targetFunctionDeclaration.returnStatements) {
@@ -3753,6 +3791,53 @@ export class Binder extends ParseTreeWalker {
return undefined;
}
// Viper 扩展:`enum X:` 的成员允许只写成员名:
// enum ASTFlag:
// IsAsync
// Simple
// HasStar: int
// enum X: a, b, c
// 裸名(含逗号分隔的裸名元组)在 Python 规则下只是表达式语句,不产生声明,
// 这里为它们补出类作用域声明;取值由编译期推导(见 checker 的枚举取值校验)。
private _bindViperEnumMemberNames(suite: SuiteNode): void {
suite.d.statements.forEach((statement) => {
if (statement.nodeType !== ParseNodeType.StatementList) {
return;
}
statement.d.statements.forEach((subStatement) => {
this._bindViperEnumMemberNameNode(subStatement);
});
});
}
private _bindViperEnumMemberNameNode(node: ParseNode): void {
if (node.nodeType === ParseNodeType.Name) {
this._addViperEnumMemberDeclaration(node);
} else if (node.nodeType === ParseNodeType.Tuple) {
node.d.items.forEach((item) => this._bindViperEnumMemberNameNode(item));
}
}
private _addViperEnumMemberDeclaration(node: NameNode): void {
const symbol = this._bindNameToScope(this._currentScope, node);
if (!symbol) {
return;
}
const declaration: VariableDeclaration = {
type: DeclarationType.Variable,
node,
isConstant: isConstantName(node.d.value),
uri: this._fileInfo.fileUri,
range: convertTextRangeToRange(node, this._fileInfo.lines),
moduleName: this._fileInfo.moduleName,
isInExceptSuite: this._isInExceptSuite,
isExplicitBinding: this._currentScope.getBindingType(node.d.value) !== undefined,
};
symbol.addDeclaration(declaration);
}
private _bindPossibleTupleNamedTarget(target: ExpressionNode, addedSymbols?: Map<string, Symbol>) {
switch (target.nodeType) {
case ParseNodeType.Name: {
@@ -36,6 +36,7 @@ import {
ClassNode,
ComprehensionIfNode,
ComprehensionNode,
ConstantNode,
DelNode,
DictionaryNode,
ErrorNode,
@@ -89,7 +90,7 @@ import {
} from '../parser/parseNodes';
import { ParserOutput } from '../parser/parser';
import { UnescapeError, UnescapeErrorType, getUnescapedString } from '../parser/stringTokenUtils';
import { OperatorType, StringTokenFlags, TokenType } from '../parser/tokenizerTypes';
import { KeywordType, OperatorType, StringTokenFlags, TokenType } from '../parser/tokenizerTypes';
import { AnalyzerFileInfo } from './analyzerFileInfo';
import * as AnalyzerNodeInfo from './analyzerNodeInfo';
import { ConstraintTracker } from './constraintTracker';
@@ -98,7 +99,13 @@ import { addInheritedDataClassEntries } from './dataClasses';
import { Declaration, DeclarationType, isAliasDeclaration, isVariableDeclaration } from './declaration';
import { getNameNodeForDeclaration } from './declarationUtils';
import { getDeprecatedAliases, getDeprecatedSpecialForms } from './deprecatedSymbols';
import { getEnumDeclaredValueType, isEnumClassWithMembers, transformTypeForEnumMember } from './enums';
import {
getEnumDeclaredValueType,
getViperEnumMemberValues,
getViperIntegerLiteralValue,
isEnumClassWithMembers,
transformTypeForEnumMember,
} from './enums';
import { ImportResolver, createImportedModuleDescriptor } from './importResolver';
import { ImportResult, ImportType } from './importResult';
import { getRelativeModuleName, getTopLevelImports } from './importStatementUtils';
@@ -154,6 +161,7 @@ import {
Variance,
combineTypes,
isAnyOrUnknown,
isCDefineType,
isClass,
isClassInstance,
isFunction,
@@ -217,6 +225,33 @@ interface TypeVarUsageInfo {
// functions to be emitted.
const isPrintCodeComplexityEnabled = false;
// Viper 扩展:extern 函数体允许的语句——只允许 pass 或 None(仅声明原型)。
function isViperExternAllowedBody(stmt: ParseNode): boolean {
if (stmt.nodeType === ParseNodeType.Pass) {
return true;
}
if (stmt.nodeType === ParseNodeType.StatementList) {
const smallStatements = (stmt as StatementListNode).d.statements;
return smallStatements.every((s) => {
if (s.nodeType === ParseNodeType.Pass) {
return true;
}
if (s.nodeType === ParseNodeType.Constant) {
return (s as ConstantNode).d.constType === KeywordType.None;
}
return false;
});
}
return false;
}
function isViperNoneLiteral(expr: ParseNode | undefined): boolean {
if (!expr || expr.nodeType !== ParseNodeType.Constant) {
return false;
}
return (expr as ConstantNode).d.constType === KeywordType.None;
}
export class Checker extends ParseTreeWalker {
private readonly _moduleNode: ModuleNode;
private readonly _fileInfo: AnalyzerFileInfo;
@@ -383,6 +418,11 @@ export class Checker extends ParseTreeWalker {
this._validateEnumMembers(classTypeResult.classType, node);
// Viper 扩展:`enum X:` 枚举成员的编译期推取值需要能存入成员声明的类型。
if (node.d.isViperEnum) {
this._validateViperEnumMemberValues(node);
}
if (ClassType.isTypedDictClass(classTypeResult.classType)) {
this._validateTypedDictClassSuite(node.d.suite);
}
@@ -430,6 +470,22 @@ export class Checker extends ParseTreeWalker {
);
}
// Viper 扩展:extern 声明的函数体必须为 pass 或 None(仅声明原型,不定义实现)。
if (
node.d.viperModifier === 'extern' &&
IsViperDialect(this._fileInfo.executionEnvironment.dialect)
) {
const bodyStatements = node.d.suite.d.statements;
const invalidStatement = bodyStatements.find((stmt) => !isViperExternAllowedBody(stmt));
if (invalidStatement) {
this._evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.viperExternBodyInvalid().format({ name: node.d.name.d.value }),
invalidStatement
);
}
}
if (functionTypeResult) {
// Track whether we have seen a *args: P.args parameter. Named
// parameters after this need to be flagged as an error.
@@ -1048,6 +1104,24 @@ export class Checker extends ParseTreeWalker {
returnTypeResult.expectedTypeDiagAddendum?.getEffectiveTextRange()
);
}
// Viper 扩展:返回类型为 None(无返回值)的函数,return 只能写成
// 裸 return 或 return None 字面量。若 return 的是另一个返回 None
// 的表达式(如 `return g()`,g 的调用结果在 C 语义中无值),必须报错。
if (
returnTypeMatches &&
node.d.expr &&
IsViperDialect(this._fileInfo.executionEnvironment.dialect) &&
isNoneInstance(adjReturnType) &&
isNoneInstance(returnType) &&
!isViperNoneLiteral(node.d.expr)
) {
this._evaluator.addDiagnostic(
DiagnosticRule.reportReturnType,
LocMessage.viperReturnExprInNoneFunction(),
node.d.expr
);
}
}
}
@@ -1172,6 +1246,9 @@ export class Checker extends ParseTreeWalker {
override visitAssignment(node: AssignmentNode): boolean {
this._evaluator.evaluateTypesForStatement(node);
// Viper 扩展:整数字面量必须能存入声明类型的位宽(如 int 为 i32)。
this._validateViperIntegerLiteralType(node);
if (node.d.annotationComment) {
this._evaluator.getType(node.d.annotationComment);
@@ -1567,6 +1644,8 @@ export class Checker extends ParseTreeWalker {
return false;
}
this._validateViperStaticImport(node);
const decls = this._evaluator.getDeclInfoForNameNode(node.d.name)?.decls;
if (!decls) {
return false;
@@ -1609,6 +1688,42 @@ export class Checker extends ParseTreeWalker {
return false;
}
// Viper 扩展:`from mod import name` —— 若 name 是目标模块里的 `static` 符号
// (C 的 internal linkage),禁止跨模块取参。与 TransPyV ImportsGenMixin 的
// `from mod import name` 拦截同语义(同模块自引用放行,按声明文件判定)。
// `mod.name` / `mod.fn()` 形态由 typeEvaluator 的模块成员访问分支负责。
private _validateViperStaticImport(node: ImportFromAsNode) {
if (!IsViperDialect(this._fileInfo.executionEnvironment.dialect)) {
return;
}
const decls = this._evaluator.getDeclInfoForNameNode(node.d.name)?.decls;
if (!decls) {
return;
}
const currentFileUri = this._fileInfo.fileUri.toString();
for (const decl of decls) {
const resolvedDecl = this._evaluator.resolveAliasDeclaration(decl, /* resolveLocalNames */ true);
if (
!resolvedDecl ||
!ParseTreeUtils.isViperStaticDeclarationNode(resolvedDecl.node) ||
resolvedDecl.uri.toString() === currentFileUri
) {
continue;
}
this._evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.viperStaticUsedOutsideOfModule().format({
name: `${_getViperImportFromModuleName(node)}.${node.d.name.d.value}`,
}),
node.d.name
);
return;
}
}
override visitModuleName(node: ModuleNameNode): boolean {
if (this._fileInfo.isStubFile) {
return false;
@@ -1685,6 +1800,11 @@ export class Checker extends ParseTreeWalker {
override visitTypeAnnotation(node: TypeAnnotationNode): boolean {
this._evaluator.getType(node.d.annotation);
// Viper 扩展:函数内用类型注解重新声明模块级同名变量(`x: T` / `x: T = v`)
// → 「重复声明」(TypeError),无论是否声明 global/nonlocal。
this._validateViperGlobalRedeclaration(node);
return true;
}
@@ -1956,6 +2076,12 @@ export class Checker extends ParseTreeWalker {
return;
}
// Viper 扩展:`enum X:` 的成员可以只写成员名(裸名 / 逗号分隔的裸名),
// 它们是成员声明而不是"无效果的表达式",不报未使用表达式。
if (this._isViperEnumMemberNameExpression(node)) {
return;
}
const simpleExpressionTypes = [
ParseNodeType.UnaryOperation,
ParseNodeType.BinaryOperation,
@@ -1998,6 +2124,222 @@ export class Checker extends ParseTreeWalker {
}
}
// Viper 扩展:判断表达式语句是否为 `enum X:` 的成员名声明
// (类体内单独的裸名语句,或逗号分隔的裸名元组)。
private _isViperEnumMemberNameExpression(node: ParseNode): boolean {
if (node.nodeType === ParseNodeType.Name) {
return this._isViperEnumMemberNameNode(node);
}
if (
node.nodeType === ParseNodeType.Tuple &&
node.d.items.length > 0 &&
node.d.items.every((item) => item.nodeType === ParseNodeType.Name)
) {
// 逗号分隔的裸名成员(enum X: a, b, c)。
return ParseTreeUtils.getEnclosingClass(node, /* stopAtFunction */ true)?.d.isViperEnum === true;
}
return false;
}
// Viper 扩展:判断裸名节点是否直接作为 `enum X:` 类体内的成员声明语句出现。
private _isViperEnumMemberNameNode(node: NameNode): boolean {
const parent = node.parent;
const isMemberStatement =
parent?.nodeType === ParseNodeType.StatementList ||
(parent?.nodeType === ParseNodeType.Tuple && parent.parent?.nodeType === ParseNodeType.StatementList);
return (
isMemberStatement &&
ParseTreeUtils.getEnclosingClass(node, /* stopAtFunction */ true)?.d.isViperEnum === true
);
}
// Viper 扩展:校验 `enum X:` 枚举成员的编译期推导取值能否存入成员声明的类型。
// 取值从 0 递增推导(见 enums.ts),故按非负取值给出范围(i1 即 0 ~ 1)。
// 只处理"仅注解无赋值"的成员:带显式赋值的成员(如 `c: t.i1 = 2`)由
// _validateViperIntegerLiteralType 按整数字面量规则统一校验,避免重复报错。
private _validateViperEnumMemberValues(node: ClassNode) {
const memberValues = getViperEnumMemberValues(node);
node.d.suite.d.statements.forEach((statement) => {
if (statement.nodeType !== ParseNodeType.StatementList) {
return;
}
statement.d.statements.forEach((subStatement) => {
// `成员: 类型`(无显式赋值):取值由编译期推导。
if (
subStatement.nodeType !== ParseNodeType.TypeAnnotation ||
subStatement.d.valueExpr.nodeType !== ParseNodeType.Name
) {
return;
}
const nameNode = subStatement.d.valueExpr;
const value = memberValues.get(nameNode.d.value);
if (value === undefined) {
return;
}
const range = this._getViperIntegerTypeRange(
subStatement.d.annotation,
/* allowNegativeBitVector */ false
);
if (!range || (value >= range.min && value <= range.max)) {
return;
}
this._evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.viperEnumValueOutOfRange().format({
value: value.toString(),
type: range.typeName,
min: range.min.toString(),
max: range.max.toString(),
}),
nameNode
);
});
});
}
// Viper 扩展:取枚举成员类型注解对应的整型可表示范围。
// 优先使用注解书写形式(如 `i1` / `t.CInt8T`)解析,别名形式再回退到已解析类型名。
private _getViperIntegerTypeRange(
declaredTypeNode: ExpressionNode,
allowNegativeBitVector: boolean
): ViperIntegerRange | undefined {
const writtenName = getViperWrittenTypeName(declaredTypeNode);
if (writtenName) {
const range = getViperIntegerTypeRangeFromName(writtenName, allowNegativeBitVector);
if (range) {
return range;
}
}
const annotationType = this._evaluator.getTypeOfAnnotation(declaredTypeNode);
if (annotationType && isClass(annotationType) && annotationType.shared.name) {
const range = getViperIntegerTypeRangeFromName(annotationType.shared.name, allowNegativeBitVector);
if (range) {
return range;
}
}
return undefined;
}
// Viper 扩展:`NAME: 类型 = 整数字面量` 时校验字面量能否存入类型的位宽。
// 例如 `define CONST_INT: int = 10000000000000000000000000000000`
// (int 为 i32,无法正确存储该数字,需要扩容);`define` 与普通声明形式同样适用。
private _validateViperIntegerLiteralType(node: AssignmentNode) {
const leftExpr = node.d.leftExpr;
if (leftExpr.nodeType !== ParseNodeType.TypeAnnotation) {
return;
}
const literalValue = getViperIntegerLiteralValue(node.d.rightExpr);
if (literalValue === undefined) {
return;
}
const range = this._getViperIntegerTypeRange(leftExpr.d.annotation, /* allowNegativeBitVector */ true);
if (!range || (literalValue >= range.min && literalValue <= range.max)) {
return;
}
this._evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.viperIntegerLiteralOutOfRange().format({
// 用源码书写形式,超长字面量不丢精度(如 10^31)。
value: ParseTreeUtils.printExpression(node.d.rightExpr),
type: range.typeName,
min: range.min.toString(),
max: range.max.toString(),
}),
node.d.rightExpr
);
}
// Viper 扩展:模块级已有同名**运行期变量**时,函数内不得再用**类型注解**声明同名变量。
// 与 TPV(TransPyV/App/lib/core/Gen/AssignGenMixin.vp 的「重复声明」检测)同口径,
// **无条件**失败(判据不含"类型是否一致":注解类型即便与全局完全相同也是重复声明):
// ① 无 global/nonlocal 声明 → 会把全局遮蔽成局部,属重复声明;
// ② 有 global/nonlocal 声明 → 注解与既有全局声明重复,属重复声明。
// 合法的两种写法:函数内 `global x` + **无注解**赋值 `x = v`;或换用不重名的局部名。
// 范围仅函数内——模块级自身的重复注解、类体成员声明都不检查(模块级由全局变量表登记,
// 本就不构成"遮蔽")。
private _validateViperGlobalRedeclaration(node: TypeAnnotationNode) {
if (!IsViperDialect(this._fileInfo.executionEnvironment.dialect)) {
return;
}
const nameNode = node.d.valueExpr;
if (nameNode.nodeType !== ParseNodeType.Name) {
return;
}
// 仅「变量声明语句」:`x: T`(语句本身)与 `x: T = v`(赋值的左值)。
const parent = node.parent;
if (parent?.nodeType !== ParseNodeType.Assignment && parent?.nodeType !== ParseNodeType.StatementList) {
return;
}
const scope = getScopeForNode(node);
if (scope?.type !== ScopeType.Function) {
return;
}
const globalScope = scope.getGlobalScope().scope;
if (globalScope.type !== ScopeType.Module) {
return;
}
const moduleSymbol = globalScope.lookUpSymbol(nameNode.d.value);
if (!moduleSymbol || !this._isViperGlobalRuntimeVariable(moduleSymbol)) {
return;
}
const symbolWithScope = this._evaluator.lookUpSymbolRecursive(
nameNode,
nameNode.d.value,
/* honorCodeFlow */ false
);
const hasGlobalBinding =
!!symbolWithScope?.usesGlobalBinding || !!symbolWithScope?.usesNonlocalBinding;
this._evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
hasGlobalBinding
? LocMessage.viperGlobalRedeclarationWithGlobal().format({ name: nameNode.d.value })
: LocMessage.viperGlobalRedeclarationWithoutGlobal().format({ name: nameNode.d.value }),
nameNode
);
}
// Viper 扩展:模块级符号是否为「运行期变量」(会占据全局存储、可被同名注解遮蔽)。
// 排除函数/类/导入别名/显式类型别名声明,以及不产生全局存储的编译期元指令:
// - CDefine 宏常量:编译期展开,无全局变量(与 TPV 的模块变量表同口径);
// - CTypedef 别名:仅登记类型,不创建全局符号。
private _isViperGlobalRuntimeVariable(moduleSymbol: Symbol): boolean {
const declarations = moduleSymbol.getDeclarations();
const isVariable = declarations.some(
(decl) =>
decl.type === DeclarationType.Variable &&
!(
decl.typeAnnotationNode?.nodeType === ParseNodeType.MemberAccess &&
decl.typeAnnotationNode.d.member.d.value === 'CTypedef'
)
);
if (!isVariable) {
return false;
}
return !isCDefineType(this._evaluator.getEffectiveTypeOfSymbol(moduleSymbol));
}
// Verifies that the target of a nonlocal statement is not a PEP 695-style
// TypeParameter. This situation results in a runtime exception.
private _validateNonlocalTypeParam(node: NameNode) {
@@ -4446,6 +4788,12 @@ export class Checker extends ParseTreeWalker {
return;
}
// Viper 扩展:`enum X:` 的裸名成员是成员声明(取值由编译期推导),
// 不是在读取一个未绑定的变量。
if (this._isViperEnumMemberNameNode(node)) {
return;
}
// Skip this for keyword argument names.
if (node.parent?.nodeType === ParseNodeType.Argument && node.parent.d.name === node) {
return;
@@ -4793,7 +5141,19 @@ export class Checker extends ParseTreeWalker {
declaredReturnType !== undefined &&
isPureCModifierType(declaredReturnType);
if (declaredReturnType && !functionNeverReturns && implicitlyReturnsNone && !isPureModifierReturn) {
// Viper 扩展:extern/export 关键字声明的函数仅声明原型(extern 函数体必须
// 为 pass/None),不要求返回语句与声明类型匹配。
const isViperExternDecl =
IsViperDialect(this._fileInfo.executionEnvironment.dialect) &&
(node.d.viperModifier === 'extern' || node.d.viperModifier === 'export');
if (
declaredReturnType &&
!functionNeverReturns &&
implicitlyReturnsNone &&
!isPureModifierReturn &&
!isViperExternDecl
) {
if (isNever(declaredReturnType)) {
// If the function consists entirely of "...", assume that it's
// an abstract method or a protocol method and don't require that
@@ -7809,3 +8169,126 @@ export class Checker extends ParseTreeWalker {
});
}
}
// Viper 扩展:整型的可表示范围(用于整数字面量 / 枚举取值的位宽校验)。
interface ViperIntegerRange {
typeName: string;
min: bigint;
max: bigint;
}
// Viper 扩展:取类型注解的书写形式(如 `i1`、`t.CInt8T` 取 `CInt8T`)。
function getViperWrittenTypeName(declaredTypeNode: ExpressionNode): string | undefined {
if (declaredTypeNode.nodeType === ParseNodeType.Name) {
return declaredTypeNode.d.value;
}
if (declaredTypeNode.nodeType === ParseNodeType.MemberAccess) {
return declaredTypeNode.d.member.d.value;
}
return undefined;
}
// Viper 扩展:把 `from <module> import <name>` 里的模块名还原成源码写法
// (相对导入的前导点保留),用于 static 跨模块取参的诊断文案。
function _getViperImportFromModuleName(node: ImportFromAsNode): string {
const importFrom = node.parent;
if (importFrom?.nodeType !== ParseNodeType.ImportFrom) {
return '';
}
const leadingDots = '.'.repeat(importFrom.d.module.d.leadingDots);
const nameParts = importFrom.d.module.d.nameParts.map((part) => part.d.value).join('.');
return leadingDots + nameParts;
}
function viperSignedRange(typeName: string, bits: number): ViperIntegerRange | undefined {
if (!Number.isInteger(bits) || bits < 1 || bits > 64) {
return undefined;
}
// 目标 ES 版本低于 ES2020,不能用 BigInt 字面量(1n),统一用 BigInt()。
const half = BigInt(1) << BigInt(bits - 1);
return { typeName, min: -half, max: half - BigInt(1) };
}
function viperUnsignedRange(typeName: string, bits: number): ViperIntegerRange | undefined {
if (!Number.isInteger(bits) || bits < 1 || bits > 64) {
return undefined;
}
return { typeName, min: BigInt(0), max: (BigInt(1) << BigInt(bits)) - BigInt(1) };
}
// Viper 扩展:按类型名给出可表示范围。仅覆盖宽度明确的整型,其余返回 undefined
// (不做校验,避免误报)。位宽来源见 wiki 02-type-system.md。
// allowNegativeBitVector:LLVM 位向量短名(iN)是否允许负数——位向量的符号由运算决定,
// 枚举成员取值必然非负(从 0 递增),传 false 时范围即 [0, 2^N-1];
// 一般整数字面量可为负,传 true 时取有/无符号并集 [-2^(N-1), 2^N-1]。
function getViperIntegerTypeRangeFromName(
typeName: string,
allowNegativeBitVector: boolean
): ViperIntegerRange | undefined {
let match = /^u(\d+)$/.exec(typeName);
if (match) {
return viperUnsignedRange(typeName, Number(match[1]));
}
match = /^i(\d+)$/.exec(typeName);
if (match) {
const bits = Number(match[1]);
const unsignedRange = viperUnsignedRange(typeName, bits);
if (!allowNegativeBitVector || !unsignedRange) {
return unsignedRange;
}
const signedRange = viperSignedRange(typeName, bits);
return signedRange ? { typeName, min: signedRange.min, max: unsignedRange.max } : unsignedRange;
}
// 定宽整数:CInt{n}T / CUInt{n}T / CInt{Least|Fast}{n}T / CIntMaxT / CUIntMaxT。
match = /^CIntMaxT$/.exec(typeName);
if (match) {
return viperSignedRange(typeName, 64);
}
match = /^CUIntMaxT$/.exec(typeName);
if (match) {
return viperUnsignedRange(typeName, 64);
}
match = /^CInt(?:Least|Fast)?(\d+)T$/.exec(typeName);
if (match) {
return viperSignedRange(typeName, Number(match[1]));
}
match = /^CUInt(?:Least|Fast)?(\d+)T$/.exec(typeName);
if (match) {
return viperUnsignedRange(typeName, Number(match[1]));
}
// 基本 C 类型与内置类型。[位宽, 是否有符号]
const basicRanges: Record<string, [number, boolean]> = {
CChar: [8, true],
CSignedChar: [8, true],
CUnsignedChar: [8, false],
CShort: [16, true],
CUnsignedShort: [16, false],
CInt: [32, true],
CUnsigned: [32, false],
CUnsignedInt: [32, false],
CLong: [64, true],
CUnsignedLong: [64, false],
CChar8T: [8, false],
CChar16T: [16, false],
CChar32T: [32, false],
bool: [1, false],
CBool: [1, false],
int: [32, true],
};
const basicRange = basicRanges[typeName];
if (basicRange) {
return basicRange[1] ? viperSignedRange(typeName, basicRange[0]) : viperUnsignedRange(typeName, basicRange[0]);
}
return undefined;
}
@@ -13,6 +13,7 @@
import { ConsoleInterface } from '../common/console';
import { assert, fail } from '../common/debug';
import { IsViperDialect } from '../common/dialect';
import { convertOffsetToPosition } from '../common/positionUtils';
import { ArgCategory, ExpressionNode, ParseNode, ParseNodeType } from '../parser/parseNodes';
import { getFileInfo, getImportInfo } from './analyzerNodeInfo';
@@ -50,6 +51,7 @@ import {
FunctionType,
isClass,
isClassInstance,
isCTypeSubclassType,
isFunction,
isFunctionOrOverloaded,
isInstantiableClass,
@@ -74,6 +76,7 @@ import {
derivesFromStdlibClass,
doForEachSubtype,
isIncompleteUnknown,
isNoneInstance,
isTypeAliasPlaceholder,
mapSubtypes,
} from './typeUtils';
@@ -1729,6 +1732,7 @@ export function getCodeFlowEngine(
function isCallNoReturn(evaluator: TypeEvaluator, flowNode: FlowCall) {
const node = flowNode.node;
const fileInfo = getFileInfo(node);
const isViperDialect = IsViperDialect(fileInfo.executionEnvironment?.dialect);
// Assume that calls within a pyi file are not "NoReturn" calls.
if (fileInfo.isStubFile) {
@@ -1803,7 +1807,7 @@ export function getCodeFlowEngine(
const isCallAwaited = node.parent?.nodeType === ParseNodeType.Await;
if (isFunction(callSubtype)) {
if (isFunctionNoReturn(callSubtype, isCallAwaited)) {
if (isFunctionNoReturn(evaluator, callSubtype, isCallAwaited, isViperDialect)) {
noReturnTypeCount++;
}
} else if (isOverloaded(callSubtype)) {
@@ -1813,7 +1817,7 @@ export function getCodeFlowEngine(
OverloadedType.getOverloads(callSubtype).forEach((overload) => {
overloadCount++;
if (isFunctionNoReturn(overload, isCallAwaited)) {
if (isFunctionNoReturn(evaluator, overload, isCallAwaited, isViperDialect)) {
noReturnOverloadCount++;
}
});
@@ -1859,9 +1863,45 @@ export function getCodeFlowEngine(
}
}
function isFunctionNoReturn(functionType: FunctionType, isCallAwaited: boolean) {
function isFunctionNoReturn(
evaluator: TypeEvaluator,
functionType: FunctionType,
isCallAwaited: boolean,
isViperDialect: boolean
) {
const returnType = FunctionType.getEffectiveReturnType(functionType, /* includeInferred */ false);
if (returnType) {
if (isViperDialect) {
// Viper 方言:函数声明返回 t.State(C 风格 noreturn 标记,仅声明不定义,
// 如 sys.exit / stdlib.exit 的 `-> t.State`)视为 NoReturn,调用后流程终止。
let hasStateReturn = false;
doForEachSubtype(returnType, (retSubtype) => {
if (
isClass(retSubtype) &&
isCTypeSubclassType(retSubtype) &&
retSubtype.shared.name === 'State'
) {
hasStateReturn = true;
}
});
if (hasStateReturn) {
return true;
}
// Viper 无返回注解的函数被隐式声明为 `-> None`(C 语义 void),
// 掩盖了实际不返回的 fail-fast 函数(如函数体以 sys.exit(1) 结尾)。
// 惰性推断(inferredReturnType)可能尚未就绪,直接通过 flow 分析
// 函数体判定是否从不返回(isAfterNodeReachable 主动触发 flow)。
if (isNoneInstance(returnType)) {
const inferred = functionType.shared.inferredReturnType?.type;
if (inferred && isNever(inferred)) {
return true;
}
if (!isAfterNodeReachable(evaluator, functionType)) {
return true;
}
}
}
if (
isClassInstance(returnType) &&
ClassType.isBuiltIn(returnType, ['Coroutine', 'CoroutineType']) &&
@@ -258,32 +258,62 @@ function validateNewAndInitMethods(
const overloadsUsedForCall: FunctionType[] = [];
let newMethodReturnType: Type | undefined;
// Validate __new__ if it is present.
// Viper 扩展:`__new__` 作为分配器时(返回裸指针或实例|指针联合,如
// stmts.vp 中 FunctionDef 的 `__new__` 返回 `FunctionDef | t.CPtr`),其参数
// 不参与构造调用校验——Viper 中 `__new__` 由编译器按分配逻辑调用,构造调用
// 的用户参数只与 `__init__` 签名匹配(否则 7 参 `__init__` 配 6 参 `__new__`
// 会误报 "Expected 6 positional arguments")。此时仅取 `__new__` 的返回类型
// 用于后续 `__init__` 绑定判断。
let isViperNewAllocator = false;
if (newMethodTypeResult) {
// Use speculative mode for arg expressions because we don't know whether
// we'll need to re-evaluate these expressions later for __init__.
const newCallResult = validateNewMethod(
evaluator,
errorNode,
argList,
type,
skipUnknownArgCheck,
inferenceContext,
newMethodTypeResult,
/* useSpeculativeModeForArgs */ true
);
if (isFunctionOrOverloaded(newMethodTypeResult.type)) {
doForEachSignature(newMethodTypeResult.type, (signature) => {
const allocNewReturnType = FunctionType.getEffectiveReturnType(signature);
if (allocNewReturnType) {
doForEachSubtype(allocNewReturnType, (subtype) => {
if (isCPtrType(subtype)) {
isViperNewAllocator = true;
}
});
}
});
}
if (newCallResult.argumentErrors) {
argumentErrors = true;
if (isViperNewAllocator) {
// 跳过 __new__ 参数校验,直接取其返回类型。
if (isFunctionOrOverloaded(newMethodTypeResult.type)) {
doForEachSignature(newMethodTypeResult.type, (signature) => {
if (!newMethodReturnType) {
newMethodReturnType = FunctionType.getEffectiveReturnType(signature);
}
});
}
} else {
appendArray(overloadsUsedForCall, newCallResult.overloadsUsedForCall ?? []);
}
// Use speculative mode for arg expressions because we don't know whether
// we'll need to re-evaluate these expressions later for __init__.
const newCallResult = validateNewMethod(
evaluator,
errorNode,
argList,
type,
skipUnknownArgCheck,
inferenceContext,
newMethodTypeResult,
/* useSpeculativeModeForArgs */ true
);
if (newCallResult.isTypeIncomplete) {
isTypeIncomplete = true;
}
if (newCallResult.argumentErrors) {
argumentErrors = true;
} else {
appendArray(overloadsUsedForCall, newCallResult.overloadsUsedForCall ?? []);
}
newMethodReturnType = newCallResult.returnType;
if (newCallResult.isTypeIncomplete) {
isTypeIncomplete = true;
}
newMethodReturnType = newCallResult.returnType;
}
}
if (
@@ -325,16 +355,33 @@ function validateNewAndInitMethods(
}
// Validate __init__ if it's present.
//
// Viper 扩展:`__new__` 返回联合类型(如 `FunctionDef | t.CPtr`,实例或裸
// 指针)时,`isClassInstance` 对整个联合返回 false,导致 `__init__` 校验被
// 跳过,构造调用只按 `__new__` 签名校验(误报:FunctionDef 的 6 参 `__new__`
// 报 "Expected 6 positional arguments",而 `__init__` 实际有 7 参含
// type_params)。Viper 中 `__new__` 只负责分配内存,`__init__` 仍会被调用,
// 因此联合返回只要含 CPtr(Viper 分配语义)就继续校验 `__init__`。
let newMethodReturnTypeIsInstance = isClassInstance(newMethodReturnType);
if (!newMethodReturnTypeIsInstance) {
doForEachSubtype(newMethodReturnType, (subtype) => {
if (isCPtrType(subtype)) {
newMethodReturnTypeIsInstance = true;
}
});
}
if (
!isNever(newMethodReturnType) &&
!shouldSkipInitEvaluation(evaluator, type, newMethodReturnType) &&
isClassInstance(newMethodReturnType)
newMethodReturnTypeIsInstance
) {
// If the __new__ method returned the same type as the class it's constructing
// but didn't supply solved type arguments, we'll ignore its specialized return
// type and rely on the __init__ method to supply the type arguments instead.
let initMethodBindToType = newMethodReturnType;
if (
isClassInstance(initMethodBindToType) &&
initMethodBindToType.priv.typeArgs &&
initMethodBindToType.priv.typeArgs.some((typeArg) => isUnknown(typeArg))
) {
@@ -354,6 +401,12 @@ function validateNewAndInitMethods(
initMethodBindToType = ClassType.cloneAsInstance(type);
}
// 纯类实例时保持原类型,联合/CPtr 时已替换为类实例;
// 保证传给 getBoundInitMethod / validateInitMethod 的一定是 ClassType。
if (!isClassInstance(initMethodBindToType)) {
initMethodBindToType = ClassType.cloneAsInstance(type);
}
// Determine whether the class overrides the object.__init__ method.
const initMethodDiag = new DiagnosticAddendum();
initMethodTypeResult = getBoundInitMethod(evaluator, errorNode, initMethodBindToType, initMethodDiag);
@@ -389,7 +442,7 @@ function validateNewAndInitMethods(
}
}
if (!validatedArgExpressions && newMethodTypeResult) {
if (!validatedArgExpressions && newMethodTypeResult && !isViperNewAllocator) {
// If we skipped the __init__ method and the __new__ method was evaluated only
// speculatively, evaluate it non-speculatively now so we can report errors.
if (!evaluator.isSpeculativeModeInUse(errorNode)) {
@@ -409,6 +462,13 @@ function validateNewAndInitMethods(
returnType = newMethodReturnType;
}
// Viper 扩展:分配器 `__new__` 且无 `__init__` 时,参数不做构造校验,
// 返回类型沿用 `__new__` 的返回类型(实例|指针联合或裸指针)。
if (!validatedArgExpressions && isViperNewAllocator) {
validatedArgExpressions = true;
returnType = newMethodReturnType;
}
// If the class doesn't override object.__new__ or object.__init__, use the
// fallback constructor type evaluation for the `object` class.
if (!newMethodTypeResult && !initMethodTypeResult) {
+118 -1
View File
@@ -10,7 +10,8 @@
import { assert } from '../common/debug';
import { IsViperDialect } from '../common/dialect';
import { PythonVersion, pythonVersion3_13 } from '../common/pythonVersion';
import { ArgCategory, ExpressionNode, NameNode, ParseNode, ParseNodeType } from '../parser/parseNodes';
import { ArgCategory, ClassNode, ExpressionNode, NameNode, ParseNode, ParseNodeType } from '../parser/parseNodes';
import { OperatorType } from '../parser/tokenizerTypes';
import { getFileInfo } from './analyzerNodeInfo';
import { VariableDeclaration } from './declaration';
import { getClassFullName, getEnclosingClass, getTypeSourceId } from './parseTreeUtils';
@@ -301,6 +302,118 @@ export function createEnumType(
return classType;
}
// Viper 扩展:按声明顺序推导 `enum X:` 各成员的编译期取值。
// 规则:从 0 递增;中间显式赋值则改用该值并从其 +1 继续;
// 显式赋的不是整数字面量时无法推导,其后成员也不推导(直到再次遇到显式整数字面量)。
// 供枚举成员 hover 显示与取值越界校验共用同一份规则。
export function getViperEnumMemberValues(classNode: ClassNode): Map<string, bigint> {
const values = new Map<string, bigint>();
// 目标 ES 版本低于 ES2020,不能用 BigInt 字面量(0n / 1n),统一用 BigInt()。
let nextValue: bigint | undefined = BigInt(0);
const addMember = (nameNode: NameNode, explicitValueNode: ExpressionNode | undefined) => {
const value = explicitValueNode ? getViperIntegerLiteralValue(explicitValueNode) : nextValue;
nextValue = value === undefined ? undefined : value + BigInt(1);
if (value !== undefined) {
values.set(nameNode.d.value, value);
}
};
classNode.d.suite.d.statements.forEach((statement) => {
if (statement.nodeType !== ParseNodeType.StatementList) {
return;
}
statement.d.statements.forEach((subStatement) => {
switch (subStatement.nodeType) {
case ParseNodeType.Name:
// 裸名成员。
addMember(subStatement, undefined);
break;
case ParseNodeType.Tuple:
// 逗号分隔的裸名成员(enum X: a, b, c)。
subStatement.d.items.forEach((item) => {
if (item.nodeType === ParseNodeType.Name) {
addMember(item, undefined);
}
});
break;
case ParseNodeType.TypeAnnotation:
// `成员: 类型`(无显式赋值)。
if (subStatement.d.valueExpr.nodeType === ParseNodeType.Name) {
addMember(subStatement.d.valueExpr, undefined);
}
break;
case ParseNodeType.Assignment: {
// `成员 = 值` 或 `成员: 类型 = 值`。
const leftExpr = subStatement.d.leftExpr;
if (
leftExpr.nodeType === ParseNodeType.TypeAnnotation &&
leftExpr.d.valueExpr.nodeType === ParseNodeType.Name
) {
addMember(leftExpr.d.valueExpr, subStatement.d.rightExpr);
} else if (leftExpr.nodeType === ParseNodeType.Name) {
addMember(leftExpr, subStatement.d.rightExpr);
}
break;
}
}
});
});
return values;
}
// Viper 扩展:从字面量表达式取整数值(支持一元负号),非整数字面量返回 undefined。
// 用 bigint 返回,超长整数字面量(tokenizer 已按 bigint 保存)不会丢精度。
export function getViperIntegerLiteralValue(node: ExpressionNode): bigint | undefined {
if (node.nodeType === ParseNodeType.Number) {
if (node.d.isImaginary || !node.d.isInteger) {
return undefined;
}
if (typeof node.d.value === 'bigint') {
return node.d.value;
}
return Number.isInteger(node.d.value) ? BigInt(node.d.value) : undefined;
}
if (node.nodeType === ParseNodeType.UnaryOperation && node.d.operator === OperatorType.Subtract) {
const innerValue = getViperIntegerLiteralValue(node.d.expr);
return innerValue === undefined ? undefined : -innerValue;
}
return undefined;
}
// Viper 扩展:判断裸名节点是否为 `enum X:` 的成员声明。
// 形如:
// enum ASTFlag:
// IsAsync —— 类体内单独的裸名语句
// enum X: a, b, c —— 逗号分隔的裸名元组(每个裸名都是成员)
function isViperEnumBareMemberName(nameNode: NameNode): boolean {
const parent = nameNode.parent;
if (!parent) {
return false;
}
if (parent.nodeType === ParseNodeType.StatementList) {
// 类体内单独的裸名语句(`IsAsync`)。
} else if (parent.nodeType === ParseNodeType.Tuple && parent.parent?.nodeType === ParseNodeType.StatementList) {
// 逗号分隔的裸名(`enum X: a, b, c`)。
} else {
return false;
}
// 仅 `enum X:` 语法声明的枚举适用:普通类体内的裸名语句仍是普通表达式。
return getEnclosingClass(nameNode, /* stopAtFunction */ true)?.d.isViperEnum === true;
}
// Performs the "magic" that the Enum metaclass does at runtime when it
// transforms a value into an enum instance. If the specified name isn't
// an enum member, this function returns undefined indicating that the
@@ -399,6 +512,10 @@ export function transformTypeForEnumMember(
) {
valueTypeExprNode = nameNode.parent.parent.d.rightExpr;
}
} else if (IsViperDialect(getFileInfo(nameNode).executionEnvironment.dialect) && isViperEnumBareMemberName(nameNode)) {
// Viper 扩展:`enum X:` 只写成员名(裸名 / 逗号分隔的裸名)时,
// 成员既无注解也无赋值,但仍是枚举成员(取值由编译期推导)。
isMemberOfEnumeration = true;
}
// The spec specifically excludes names that start and end with a single underscore.
@@ -0,0 +1,716 @@
/*
* flowChartBuilder.ts
*
* Viper 扩展:把函数的 AST 转成「语句级控制流图(流程图)」,输出 Mermaid `flowchart TD` 文本,
* 供 Webview(停靠面板)渲染函数跳转图。
*
* 覆盖的语句结构:
* - if / elif / else(elif 串成连续的菱形判断)
* - for / for-else、while / while-else(含回流边;else 只在「非 break 结束」时进入)
* - match / case(每个 case 一个菱形,支持 guard 与不可反驳模式)
* - try / except / else / finally(try 画成「正常/异常」菱形,异常链逐个 except,
* 未匹配的异常画成「异常向上传播」终止节点,finally 汇聚所有路径)
* - break / continue / return / raise(终止节点;break/continue 绑定最内层循环)
* - with:透明展开(只加一个标题块,内部语句顺序展开)
* - 嵌套 def / class:折叠成单个节点,不展开其内部(避免图暴涨)
*
* 粒度:连续的无分支语句合并为一个基本块节点(矩形);分支条件为菱形。
*
* 说明:本模块是纯语法层(AST)的图构建,不做类型分析,因此图是「语法跳转图」而非类型收窄图。
*/
import { convertOffsetToPosition } from '../common/positionUtils';
import { TextRange } from '../common/textRange';
import { TextRangeCollection } from '../common/textRangeCollection';
import {
ForNode,
FunctionNode,
IfNode,
MatchNode,
ParseNode,
ParseNodeType,
StatementNode,
TryNode,
WhileNode,
WithNode,
} from '../parser/parseNodes';
import { Token } from '../parser/tokenizerTypes';
/** 节点 id → 源码起始行号(1 起),供 Webview 点击节点跳转到源码用。 */
export interface FlowChartNodeLines {
[nodeId: string]: number;
}
export interface FlowChartResult {
/** 函数名(用于面板标题)。 */
functionName: string;
/** 完整的 Mermaid 文本。 */
mermaidText: string;
/** 节点 id → 源码行号。 */
nodeLines: FlowChartNodeLines;
}
export function buildFlowChartForFunction(
functionNode: FunctionNode,
sourceText: string,
lines: TextRangeCollection<TextRange>
): FlowChartResult {
return new FlowChartBuilder(sourceText, lines).build(functionNode);
}
const enum ChartShape {
/** 入口节点(胶囊)。 */
Entry,
/** 正常结束节点(胶囊)。 */
Exit,
/** 基本块(矩形)。 */
Block,
/** 分支条件(菱形)。 */
Decision,
/** 终止语句:return / raise / break / continue(圆角矩形)。 */
Terminator,
}
interface ChartNode {
id: string;
shape: ChartShape;
/** 节点上逐行显示的文本(未转义)。 */
lines: string[];
}
interface ChartEdge {
from: string;
to: string;
label: string | undefined;
}
// 循环作用域。break / continue 只绑定最内层循环,故每个循环一个实例并向下传递。
interface LoopScope {
/** break 语句产生的出口,绕过 else 直接成为「循环之后」的入口。 */
breakExits: string[];
/** continue 语句节点,最后统一回连到循环条件。 */
continueTargets: string[];
}
// try-finally 作用域。return/raise/break/continue 必须「穿过」finally 才能继续,
// 故这些出口的边要先连到 finally 入口,再由 finally 入口连出去(见 _routeThroughFinally)。
interface FinallyScope {
/** 本层 finally 体的入口节点 id。 */
entryId: string;
}
/** 单个节点最多展示的源码行数(超出截断,避免个别节点撑爆图)。 */
const MaxLinesPerNode = 12;
/** 单行最多展示的字符数。 */
const MaxCharsPerLine = 96;
class FlowChartBuilder {
private _nodes: ChartNode[] = [];
private _edges: ChartEdge[] = [];
private _nodeMap = new Map<string, ChartNode>();
private _nodeLines: FlowChartNodeLines = {};
private _nextNodeId = 1;
// 「待消费的边标签」:下次从该节点连出的边会带上这个标签。
// 用于给分支出口(True/False、匹配/不匹配)打标签,而无需让 _buildStatements 感知标签。
private _pendingEdgeLabels = new Map<string, string>();
// try-finally 作用域栈(外层在前、内层在后)。为空表示当前不在任何 finally 内。
private _finallyStack: FinallyScope[] = [];
// 已连接过的边(from|to|label),避免「多条出口穿过同一个 finally」时产生重复边。
private _edgeKeys = new Set<string>();
// 函数结束节点,按需创建(正常落到末尾、或终止语句穿过 finally 之后都汇到它)。
private _endNodeId: string | undefined;
private _endNodeOffset = 0;
constructor(private _sourceText: string, private _lines: TextRangeCollection<TextRange>) {}
build(functionNode: FunctionNode): FlowChartResult {
this._endNodeOffset = functionNode.start;
const entryId = this._addNode(ChartShape.Entry, [this._getFunctionHeader(functionNode)], functionNode.start);
const bodyExits = this._buildStatements(functionNode.d.suite.d.statements, [entryId], undefined);
// 只有存在「正常落到函数末尾」的路径时才画结束节点(全 return 的函数就没有)。
if (bodyExits.length > 0) {
this._link(bodyExits, this._getEndNodeId());
}
return {
functionName: functionNode.d.name.d.value,
mermaidText: this._render(),
nodeLines: this._nodeLines,
};
}
private _getFunctionHeader(functionNode: FunctionNode): string {
const params = functionNode.d.params
.map((param) => (param.d.name ? this._getNodeText(param.d.name) : '...'))
.join(', ');
return `def ${functionNode.d.name.d.value}(${params})`;
}
// ===== 图元操作 =====
private _addNode(shape: ChartShape, lines: string[], offset: number, mergeWithNodeId?: string): string {
if (mergeWithNodeId !== undefined) {
const existing = this._nodeMap.get(mergeWithNodeId)!;
existing.lines.push(...lines);
return mergeWithNodeId;
}
const id = `n${this._nextNodeId++}`;
const node: ChartNode = { id, shape, lines: [] };
this._appendNodeLines(node, lines);
this._nodes.push(node);
this._nodeMap.set(id, node);
this._nodeLines[id] = convertOffsetToPosition(offset, this._lines).line + 1;
return id;
}
private _appendNodeLines(node: ChartNode, lines: string[]): void {
node.lines.push(...lines);
// 超长基本块只保留前若干行,末尾换成省略提示(避免单个节点撑爆整张图)。
if (node.lines.length > MaxLinesPerNode) {
const hiddenCount = node.lines.length - MaxLinesPerNode;
node.lines = node.lines.slice(0, MaxLinesPerNode);
node.lines.push(`…(还有 ${hiddenCount} 行)`);
}
}
private _link(fromIds: string[], toId: string, label?: string): void {
for (const fromId of fromIds) {
let edgeLabel = label;
if (edgeLabel === undefined) {
const pending = this._pendingEdgeLabels.get(fromId);
if (pending !== undefined) {
edgeLabel = pending;
this._pendingEdgeLabels.delete(fromId);
}
}
// 去重:多条出口穿过同一个 finally 时会产生同一条边,重复画出来只会让图变脏。
const edgeKey = `${fromId}|${toId}|${edgeLabel ?? ''}`;
if (this._edgeKeys.has(edgeKey)) {
continue;
}
this._edgeKeys.add(edgeKey);
this._edges.push({ from: fromId, to: toId, label: edgeLabel });
}
}
/** 函数结束节点的 id(按需创建)。 */
private _getEndNodeId(): string {
if (!this._endNodeId) {
this._endNodeId = this._addNode(ChartShape.Exit, ['结束'], this._endNodeOffset);
}
return this._endNodeId;
}
/**
* 让「必须等 finally 执行完才能继续」的出口穿过 finally 链。
* 例如 `try: return x finally: f()` 的真实顺序是 return x → f() → 函数返回,
* 因此 return 的边要先连到 finally 入口,再由 finally 入口连到它原本的目标。
*
* @returns 调用方应该继续连出的节点 id:不在 finally 内就是原节点,否则是最外层 finally 入口。
*/
private _routeThroughFinally(sourceId: string): string {
let currentId = sourceId;
for (let i = this._finallyStack.length - 1; i >= 0; i--) {
const finallyEntryId = this._finallyStack[i].entryId;
this._link([currentId], finallyEntryId);
currentId = finallyEntryId;
}
return currentId;
}
/** 给「下一个从 nodeId 连出的边」预设标签(用于分支出口)。 */
private _queueEdgeLabel(nodeId: string, label: string): void {
this._pendingEdgeLabels.set(nodeId, label);
}
private _clearEdgeLabel(nodeId: string): void {
this._pendingEdgeLabels.delete(nodeId);
}
// ===== 语句序列 =====
/**
* 顺序构建一批语句。
* @param incoming 进入本批语句的「悬空出口」节点 id(尚未连出边)
* @param openBlockId 可选的、允许继续追加语句的当前基本块(如 with 的标题块)
* @returns 本批语句执行完后仍悬空的出口节点 id(终止语句不会产生出口)
*/
private _buildStatements(
statements: StatementNode[],
incoming: string[],
loopScope: LoopScope | undefined,
openBlockId?: string
): string[] {
let current = incoming;
for (const statement of statements) {
if (this._isCompoundStatement(statement)) {
openBlockId = undefined;
current = this._buildCompoundStatement(statement, current, loopScope);
continue;
}
const nodeText = this._getNodeText(statement);
if (this._isTerminatorStatement(statement)) {
openBlockId = undefined;
current = this._buildTerminatorStatement(statement, nodeText, current, loopScope);
continue;
}
// 简单语句:能并入当前基本块就并入,否则新建块。
if (openBlockId !== undefined && current.length === 1 && current[0] === openBlockId) {
this._addNode(ChartShape.Block, [nodeText], statement.start, openBlockId);
continue;
}
const blockId = this._addNode(ChartShape.Block, [nodeText], statement.start);
this._link(current, blockId);
current = [blockId];
openBlockId = blockId;
}
return current;
}
private _isCompoundStatement(statement: StatementNode): boolean {
switch (statement.nodeType) {
case ParseNodeType.If:
case ParseNodeType.While:
case ParseNodeType.For:
case ParseNodeType.Try:
case ParseNodeType.With:
case ParseNodeType.Match:
case ParseNodeType.Function:
case ParseNodeType.Class:
return true;
default:
return false;
}
}
/**
* 是否为终止语句(return / raise / break / continue / fallthrough)。
* 它们位于 StatementList 内部。
*/
private _isTerminatorStatement(statement: StatementNode): boolean {
if (statement.nodeType !== ParseNodeType.StatementList) {
return false;
}
return statement.d.statements.some((smallStatement) => this._isTerminatorSmallStatement(smallStatement.nodeType));
}
private _isTerminatorSmallStatement(nodeType: ParseNodeType): boolean {
return (
nodeType === ParseNodeType.Return ||
nodeType === ParseNodeType.Raise ||
nodeType === ParseNodeType.Break ||
nodeType === ParseNodeType.Continue ||
// Viper 扩展:match-case 内的 `fallthrough`(case 穿透)同样终止当前分支。
nodeType === ParseNodeType.Fallthrough
);
}
private _buildTerminatorStatement(
statement: StatementNode,
nodeText: string,
incoming: string[],
loopScope: LoopScope | undefined
): string[] {
const nodeId = this._addNode(ChartShape.Terminator, [nodeText], statement.start);
this._link(incoming, nodeId);
// 终止语句位于 finally 内时,真实顺序是「终止 → finally → 原去向」,
// 因此出口节点换成最外层 finally 入口(不在 finally 内则仍是自身)。
const exitId = this._routeThroughFinally(nodeId);
if (statement.nodeType === ParseNodeType.StatementList) {
for (const smallStatement of statement.d.statements) {
if (
(smallStatement.nodeType === ParseNodeType.Break ||
smallStatement.nodeType === ParseNodeType.Continue) &&
loopScope
) {
// break / continue 的去向由循环收口,这里只登记它的最终出口。
if (smallStatement.nodeType === ParseNodeType.Break) {
loopScope.breakExits.push(exitId);
} else {
loopScope.continueTargets.push(exitId);
}
continue;
}
// return / raise 要等 finally 执行完才真正离开函数。
if (
exitId !== nodeId &&
(smallStatement.nodeType === ParseNodeType.Return ||
smallStatement.nodeType === ParseNodeType.Raise)
) {
this._link([exitId], this._getEndNodeId());
}
}
}
// 终止语句之后不再有后继(return/raise 直接结束;break/continue 由循环收口)。
return [];
}
private _buildCompoundStatement(
statement: StatementNode,
incoming: string[],
loopScope: LoopScope | undefined
): string[] {
switch (statement.nodeType) {
case ParseNodeType.If:
return this._buildIf(statement, incoming, loopScope);
case ParseNodeType.While:
case ParseNodeType.For:
return this._buildLoop(statement, incoming, loopScope);
case ParseNodeType.Match:
return this._buildMatch(statement, incoming, loopScope);
case ParseNodeType.Try:
return this._buildTry(statement, incoming, loopScope);
case ParseNodeType.With:
return this._buildWith(statement, incoming, loopScope);
case ParseNodeType.Function:
case ParseNodeType.Class:
return this._buildNestedDefinition(statement, incoming);
default:
return incoming;
}
}
// ===== if / elif / else =====
private _buildIf(node: IfNode, incoming: string[], loopScope: LoopScope | undefined): string[] {
// firstToken 区分首分支(if)与后续分支(elif),标签据此显示。
const condId = this._addNode(
ChartShape.Decision,
[`${this._getTokenText(node.d.firstToken)} ${this._getNodeText(node.d.testExpr)}`],
node.start
);
this._link(incoming, condId);
this._queueEdgeLabel(condId, 'True');
const thenExits = this._buildStatements(node.d.ifSuite.d.statements, [condId], loopScope);
this._clearEdgeLabel(condId);
const elseSuite = node.d.elseSuite;
if (!elseSuite) {
// 无 else:条件为假的出口直接悬空,由后继语句(或结束节点)接走。
this._queueEdgeLabel(condId, 'False');
return thenExits.concat(condId);
}
if (elseSuite.nodeType === ParseNodeType.If) {
// elif:串成下一个菱形,保证「False」标签落在连向 elif 条件的边上。
this._queueEdgeLabel(condId, 'False');
const elifExits = this._buildIf(elseSuite, [condId], loopScope);
this._clearEdgeLabel(condId);
return thenExits.concat(elifExits);
}
this._queueEdgeLabel(condId, 'False');
const elseExits = this._buildStatements(elseSuite.d.statements, [condId], loopScope);
this._clearEdgeLabel(condId);
return thenExits.concat(elseExits);
}
// ===== for / while(含 else)=====
private _buildLoop(
node: WhileNode | ForNode,
incoming: string[],
loopScope: LoopScope | undefined
): string[] {
const isWhile = node.nodeType === ParseNodeType.While;
const conditionText = isWhile
? `while ${this._getNodeText(node.d.testExpr)}`
: `for ${this._getNodeText(node.d.targetExpr)} in ${this._getNodeText(node.d.iterableExpr)}`;
const trueLabel = isWhile ? 'True' : '还有元素';
const falseLabel = isWhile ? 'False' : '迭代结束';
const condId = this._addNode(ChartShape.Decision, [conditionText], node.start);
this._link(incoming, condId);
const bodySuite = isWhile ? node.d.whileSuite : node.d.forSuite;
const scope: LoopScope = { breakExits: [], continueTargets: [] };
this._queueEdgeLabel(condId, trueLabel);
const bodyExits = this._buildStatements(bodySuite.d.statements, [condId], scope);
this._clearEdgeLabel(condId);
// 回流:循环体正常结束 + continue 都回到条件判断。
this._link(bodyExits.concat(scope.continueTargets), condId);
let normalExits: string[];
if (node.d.elseSuite) {
// for-else / while-else:只有「非 break 结束」才会进入 else。
this._queueEdgeLabel(condId, falseLabel);
normalExits = this._buildStatements(node.d.elseSuite.d.statements, [condId], scope);
this._clearEdgeLabel(condId);
} else {
this._queueEdgeLabel(condId, falseLabel);
normalExits = [condId];
}
// break 出口绕过 else,直接成为整个循环结构的出口。
return normalExits.concat(scope.breakExits);
}
// ===== match / case =====
private _buildMatch(node: MatchNode, incoming: string[], loopScope: LoopScope | undefined): string[] {
const matchId = this._addNode(ChartShape.Block, [`match ${this._getNodeText(node.d.expr)}`], node.start);
this._link(incoming, matchId);
if (node.d.cases.length === 0) {
return [matchId];
}
const exits: string[] = [];
let fromId = matchId;
for (const caseNode of node.d.cases) {
const guardText = caseNode.d.guardExpr ? ` if ${this._getNodeText(caseNode.d.guardExpr)}` : '';
const condId = this._addNode(
ChartShape.Decision,
[`case ${this._getNodeText(caseNode.d.pattern)}${guardText}`],
caseNode.start
);
this._queueEdgeLabel(fromId, fromId === matchId ? '开始匹配' : '不匹配');
this._link([fromId], condId);
this._queueEdgeLabel(condId, '匹配');
exits.push(...this._buildStatements(caseNode.d.suite.d.statements, [condId], loopScope));
this._clearEdgeLabel(condId);
fromId = condId;
}
// 最后一个 case 若不是「无条件匹配」,则「都不匹配」的出口悬空给后继语句。
const lastCase = node.d.cases[node.d.cases.length - 1];
if (!lastCase.d.isIrrefutable) {
this._queueEdgeLabel(fromId, '都不匹配');
exits.push(fromId);
}
return exits;
}
// ===== try / except / else / finally =====
private _buildTry(node: TryNode, incoming: string[], loopScope: LoopScope | undefined): string[] {
const tryId = this._addNode(ChartShape.Decision, ['try'], node.start);
this._link(incoming, tryId);
// finally 入口必须先建:try 体与各 handler 里的 return/raise/break/continue
// 都要穿过它,_routeThroughFinally 才能拿到入口 id。
const finallySuite = node.d.finallySuite;
const finallyEntryId = finallySuite
? this._addNode(ChartShape.Block, ['finally'], finallySuite.start)
: undefined;
if (finallyEntryId) {
this._finallyStack.push({ entryId: finallyEntryId });
}
// 正常路径:try 体 →(可选 else)。
this._queueEdgeLabel(tryId, '正常');
let normalExits = this._buildStatements(node.d.trySuite.d.statements, [tryId], loopScope);
this._clearEdgeLabel(tryId);
if (node.d.elseSuite) {
normalExits = this._buildStatements(node.d.elseSuite.d.statements, normalExits, loopScope);
}
// 异常路径:逐个 except 连成链。
const handlerExits: string[] = [];
let handlerIncomingId = tryId;
let handlerIncomingIsTry = true;
for (const exceptNode of node.d.exceptClauses) {
const typeText = exceptNode.d.typeExpr ? this._getNodeText(exceptNode.d.typeExpr) : '';
const nameText = exceptNode.d.name ? ` as ${exceptNode.d.name.d.value}` : '';
const condId = this._addNode(
ChartShape.Decision,
[`except ${typeText}${nameText}`.trim()],
exceptNode.start
);
this._queueEdgeLabel(handlerIncomingId, handlerIncomingIsTry ? '异常' : '未匹配');
this._link([handlerIncomingId], condId);
this._queueEdgeLabel(condId, '匹配');
handlerExits.push(
...this._buildStatements(exceptNode.d.exceptSuite.d.statements, [condId], loopScope)
);
this._clearEdgeLabel(condId);
handlerIncomingId = condId;
handlerIncomingIsTry = false;
}
// 没有任何 handler 匹配(或无 handler)时异常继续向上传播。
const lastExcept = node.d.exceptClauses[node.d.exceptClauses.length - 1];
const propagationSources: string[] = [];
if (!lastExcept) {
this._queueEdgeLabel(tryId, '异常');
propagationSources.push(tryId);
} else if (lastExcept.d.typeExpr) {
// 裸 except(无类型)能兜住所有异常,故不再有传播路径。
this._queueEdgeLabel(handlerIncomingId, '未匹配');
propagationSources.push(handlerIncomingId);
}
// try 体 / else / handler 都已构建完,最后才轮到 finally 体,
// 故从这里开始不再把出口穿过 finally(否则 finally 自己会被串进去)。
if (finallyEntryId) {
this._finallyStack.pop();
}
// 未匹配的异常也要先执行 finally,再向上传播。
if (propagationSources.length > 0) {
if (finallyEntryId) {
this._link(propagationSources, finallyEntryId);
}
const propagationId = this._addNode(ChartShape.Terminator, ['raise(异常向上传播)'], node.start);
this._link(finallyEntryId ? [finallyEntryId] : propagationSources, propagationId);
}
let exits = normalExits.concat(handlerExits);
// finally:正常路径与各 handler 的出口都汇聚进来,执行完再继续向下。
if (finallySuite && finallyEntryId) {
this._link(exits, finallyEntryId);
exits = this._buildStatements(finallySuite.d.statements, [finallyEntryId], loopScope, finallyEntryId);
}
return exits;
}
// ===== with =====
private _buildWith(node: WithNode, incoming: string[], loopScope: LoopScope | undefined): string[] {
const itemText = node.d.withItems
.map((item) => {
const exprText = this._getNodeText(item.d.expr);
return item.d.target ? `${exprText} as ${this._getNodeText(item.d.target)}` : exprText;
})
.join(', ');
const headerId = this._addNode(ChartShape.Block, [`with ${itemText}`], node.start);
this._link(incoming, headerId);
// with 是透明容器:内部语句顺序展开,并允许首条简单语句并入标题块。
return this._buildStatements(node.d.suite.d.statements, [headerId], loopScope, headerId);
}
// ===== 嵌套 def / class =====
private _buildNestedDefinition(statement: StatementNode, incoming: string[]): string[] {
const headerText = this._getNodeText(statement).split('\n')[0];
const nodeId = this._addNode(ChartShape.Block, [`${headerText.trim()} …(未展开)`], statement.start);
this._link(incoming, nodeId);
// 嵌套定义本身是声明语句,执行流继续向下。
return [nodeId];
}
// ===== 文本与渲染 =====
private _getNodeText(node: ParseNode): string {
const text = this._sourceText.slice(node.start, TextRange.getEnd(node));
return text.replace(/\s+/g, ' ').trim().slice(0, MaxCharsPerLine);
}
private _getTokenText(token: Token): string {
return this._sourceText.slice(token.start, token.start + token.length);
}
private _render(): string {
const output: string[] = ['flowchart TD'];
for (const node of this._nodes) {
const label = node.lines.map((line) => escapeMermaidLabel(line)).join('<br/>');
output.push(` ${node.id}${renderNodeShape(node.shape, label)}`);
}
for (const edge of this._edges) {
const labelText = edge.label ? `|"${escapeMermaidLabel(edge.label)}"| ` : '';
output.push(` ${edge.from} -->${labelText ? ' ' + labelText : ' '}${edge.to}`);
}
output.push('');
output.push(' classDef entry fill:none,stroke:#16a34a,stroke-width:2px');
output.push(' classDef exitNode fill:none,stroke:#dc2626,stroke-width:2px');
output.push(' classDef terminator fill:none,stroke:#dc2626,stroke-width:2px');
output.push(' classDef decision fill:none,stroke:#d97706,stroke-width:2px');
for (const node of this._nodes) {
const className = getClassName(node.shape);
output.push(` class ${node.id} ${className}`);
}
return output.join('\n');
}
}
function renderNodeShape(shape: ChartShape, label: string): string {
switch (shape) {
case ChartShape.Decision:
return `{"${label}"}`;
case ChartShape.Terminator:
return `(["${label}"])`;
case ChartShape.Entry:
case ChartShape.Exit:
return `(["${label}"])`;
default:
return `["${label}"]`;
}
}
function getClassName(shape: ChartShape): string {
switch (shape) {
case ChartShape.Entry:
return 'entry';
case ChartShape.Exit:
return 'exitNode';
case ChartShape.Terminator:
return 'terminator';
case ChartShape.Decision:
return 'decision';
default:
return 'block';
}
}
/**
* 转义 Mermaid 标签。
* Mermaid 在带引号的标签里用 HTML 实体,且 `#` 是实体前缀,故先转义 `#`。
*/
function escapeMermaidLabel(text: string): string {
return text
.replace(/#/g, '#35;')
.replace(/&/g, '#38;')
.replace(/"/g, '#quot;')
.replace(/</g, '#60;')
.replace(/>/g, '#62;');
}
@@ -82,10 +82,11 @@ export function createImportedModuleDescriptor(moduleName: string): ImportedModu
type CachedImportResults = Map<string, ImportResult>;
const supportedNativeLibExtensions = ['.pyd', '.so', '.dylib'];
// Viper 模式下同时支持 .vp/.vpi(主)和 .py/.pyi(兼容回退)。
// Viper 模式下同时支持 .vp/.vpi/.vpnci(主)和 .py/.pyi(兼容回退)。
// .vpi 为可编译注解,.vpnci 为不可编译注解(配置与 .vpi 一致)。
// .py/.pyi 支持用于 TransPyC 源码(TPC/TPV 编译器源码仍是 .py 扩展名),
// 以及用户未迁移到 .vp 的旧代码。
export const supportedSourceFileExtensions = ['.vp', '.vpi', '.py', '.pyi'];
export const supportedSourceFileExtensions = ['.vp', '.vpi', '.vpnci', '.py', '.pyi'];
export const supportedFileExtensions = [...supportedSourceFileExtensions, ...supportedNativeLibExtensions];
// Should we allow partial resolution for third-party packages? Some use tricks
@@ -223,7 +224,11 @@ export class ImportResolver {
result.nonStubImportResult.resolvedUris.length - 1
];
if (nonEmptyUri.hasExtension('.vp') || nonEmptyUri.hasExtension('.vpi')) {
if (
nonEmptyUri.hasExtension('.vp') ||
nonEmptyUri.hasExtension('.vpi') ||
nonEmptyUri.hasExtension('.vpnci')
) {
// We allow pyi in case there are multiple pyi for a compiled module such as
// numpy.random.mtrand
sourceFileUris.push(nonEmptyUri);
@@ -821,7 +826,13 @@ export class ImportResolver {
let strippedFileName: string;
let isNativeLib = false;
if (fileExt === '.vp' || fileExt === '.vpi' || fileExt === '.py' || fileExt === '.pyi') {
if (
fileExt === '.vp' ||
fileExt === '.vpi' ||
fileExt === '.vpnci' ||
fileExt === '.py' ||
fileExt === '.pyi'
) {
strippedFileName = stripFileExtension(filePath.fileName);
} else if (
_isNativeModuleFileExtension(fileExt) &&
@@ -1402,6 +1413,8 @@ export class ImportResolver {
// Viper 模式下同时支持 .vp/.vpi(主)和 .py/.pyi(兼容回退)。
const pyFilePath = dirPath.initPyUri;
const pyiFilePath = dirPath.initPyiUri;
// .vpnci 不可编译注解(与 .vpi 同级)
const vpnciInitFilePath = dirPath.combinePathsUnsafe('__init__.vpnci');
// .py/.pyi 兼容回退路径
const compatPyFilePath = dirPath.combinePathsUnsafe('__init__.py');
const compatPyiFilePath = dirPath.combinePathsUnsafe('__init__.pyi');
@@ -1414,6 +1427,13 @@ export class ImportResolver {
isStubFile = true;
}
isInitFilePresent = true;
} else if (allowPyi && this.fileExistsCached(vpnciInitFilePath)) {
importLogger?.log(`Resolved import with file '${vpnciInitFilePath}'`);
resolvedPaths.push(vpnciInitFilePath);
if (isLastPart) {
isStubFile = true;
}
isInitFilePresent = true;
} else if (this.fileExistsCached(pyFilePath)) {
importLogger?.log(`Resolved import with file '${pyFilePath}'`);
resolvedPaths.push(pyFilePath);
@@ -1440,6 +1460,7 @@ export class ImportResolver {
if (dirName) {
const sameNameVpPath = dirPath.combinePathsUnsafe(dirName).addExtension('.vp');
const sameNameVpiPath = dirPath.combinePathsUnsafe(dirName).addExtension('.vpi');
const sameNameVpnciPath = dirPath.combinePathsUnsafe(dirName).addExtension('.vpnci');
if (allowPyi && this.fileExistsCached(sameNameVpiPath)) {
importLogger?.log(`Resolved import with Viper same-name file '${sameNameVpiPath}'`);
resolvedPaths.push(sameNameVpiPath);
@@ -1447,6 +1468,13 @@ export class ImportResolver {
isStubFile = true;
}
isInitFilePresent = true;
} else if (allowPyi && this.fileExistsCached(sameNameVpnciPath)) {
importLogger?.log(`Resolved import with Viper same-name file '${sameNameVpnciPath}'`);
resolvedPaths.push(sameNameVpnciPath);
if (isLastPart) {
isStubFile = true;
}
isInitFilePresent = true;
} else if (this.fileExistsCached(sameNameVpPath)) {
importLogger?.log(`Resolved import with Viper same-name file '${sameNameVpPath}'`);
resolvedPaths.push(sameNameVpPath);
@@ -1469,6 +1497,7 @@ export class ImportResolver {
implicitImports = this.findImplicitImports(moduleDescriptor.nameParts.join('.'), dirPath, [
pyFilePath,
pyiFilePath,
vpnciInitFilePath,
]);
break;
}
@@ -1481,6 +1510,8 @@ export class ImportResolver {
// 用于 TransPyC 源码(仍是 .py 扩展名)。
const pyFilePath = dirPath.packageUri;
const pyiFilePath = dirPath.packageStubUri;
// .vpnci 不可编译注解(与 .vpi 同级)
const vpnciFilePath = dirPath.addExtension('.vpnci');
// .py/.pyi 兼容回退路径
const compatPyFilePath = dirPath.addExtension('.py');
const compatPyiFilePath = dirPath.addExtension('.pyi');
@@ -1492,6 +1523,12 @@ export class ImportResolver {
if (isLastPart) {
isStubFile = true;
}
} else if (allowPyi && this.fileExistsCached(vpnciFilePath)) {
importLogger?.log(`Resolved import with file '${vpnciFilePath}'`);
resolvedPaths.push(vpnciFilePath);
if (isLastPart) {
isStubFile = true;
}
} else if (this.fileExistsCached(pyFilePath)) {
importLogger?.log(`Resolved import with file '${pyFilePath}'`);
resolvedPaths.push(pyFilePath);
@@ -2117,7 +2154,11 @@ export class ImportResolver {
if (!entry.name.startsWith('_')) {
const packageInit = dirRoot.combinePaths('__init__.vpi');
if (this._fileSystemCache.existsSync(packageInit)) {
const packageInitVpnci = dirRoot.combinePaths('__init__.vpnci');
if (
this._fileSystemCache.existsSync(packageInit) ||
this._fileSystemCache.existsSync(packageInitVpnci)
) {
addDirectoryStdlibModule(moduleName);
}
}
@@ -17,6 +17,7 @@ import {
AugmentedAssignmentNode,
BinaryOperationNode,
ExpressionNode,
ParseNode,
ParseNodeType,
TernaryNode,
UnaryOperationNode,
@@ -30,6 +31,7 @@ import { EvalFlags, MagicMethodDeprecationInfo, TypeEvaluator, TypeResult } from
import {
InferenceContext,
convertToInstantiable,
convertToInstance,
getLiteralTypeClassName,
getTypeCondition,
getUnionSubtypeCount,
@@ -50,6 +52,7 @@ import {
} from './typeUtils';
import {
ClassType,
ModuleType,
NeverType,
Type,
TypeBase,
@@ -60,13 +63,16 @@ import {
isClassInstance,
isCConstType,
isCDefineType,
isCModifierType,
isCPtrType,
isCStaticType,
isCVolatileType,
isCTypeSubclassType,
isCVoidType,
isFunctionOrOverloaded,
isViperIntegerType,
isInstantiableClass,
isModule,
isNever,
isUnion,
} from './types';
@@ -119,6 +125,7 @@ export function validateBinaryOperation(
leftTypeResult: TypeResult,
rightTypeResult: TypeResult,
errorNode: ExpressionNode,
leftExpression: ExpressionNode,
inferenceContext: InferenceContext | undefined,
diag: DiagnosticAddendum,
options: BinaryOperationOptions
@@ -230,6 +237,30 @@ export function validateBinaryOperation(
type = calcLiteralForBinaryOp(operator, leftType, rightType);
}
// Viper 扩展:C 指针算术。str/bytes 是 char*(i8*),`p + n` / `p - n` / `n + p`
// 是合法的指针偏移,结果仍是指针本身(类型不变);Python 的 str 无此语义。
// 与增强赋值 `dest += n` 的处理保持一致(见 getTypeOfAugmentedAssignment)。
if (
!type &&
IsViperDialect(getFileInfo(errorNode).executionEnvironment.dialect) &&
(operator === OperatorType.Add || operator === OperatorType.Subtract)
) {
type = getViperPointerArithmeticResult(operator, leftType, rightType);
}
// Viper 扩展:C 风格强转 `T*(x)`(左操作数是「类型对象」时可实例化类,如 t.CInt)。
// parser 对 `T*(x)` 得到 BinaryOperation(Multiply, T, (x))——'*' 后紧跟 '(',
// 不满足指针后缀的"无右操作数"判据,故被解析为二元乘法。Viper 语义等价于
// `(T*)(x)`,结果类型为指针类型 CPtr[T] 的实例。
// 左操作数为真值类型(int/float 等)时不受影响:`2 * 3`、`offset * 2`
// 等真实乘法仍走正常路径。
if (!type && IsViperDialect(getFileInfo(errorNode).executionEnvironment.dialect) && operator === OperatorType.Multiply) {
const typeArg = getViperCastTypeArg(evaluator, leftExpression, leftType);
if (typeArg) {
type = getViperCPtrTypeForTypeArg(evaluator, errorNode, typeArg);
}
}
if (!type) {
const result = validateArithmeticOperation(
evaluator,
@@ -373,7 +404,10 @@ export function getTypeOfBinaryOperation(
adjustedLeftType = convertToInstantiable(evaluator.getNoneType());
}
if (isUnionableType([adjustedLeftType, adjustedRightType])) {
if (
isUnionableType([adjustedLeftType, adjustedRightType]) ||
isViperTypeDescriptorUnion(node, adjustedLeftType, adjustedRightType)
) {
if (isInstantiableClass(adjustedLeftType)) {
adjustedLeftType = specializeWithDefaultTypeArgs(adjustedLeftType);
}
@@ -398,7 +432,7 @@ export function getTypeOfBinaryOperation(
// Exempt "|" because it might be a union operation involving unknowns.
// Viper 是 AOT 静态语言,类型表达式中的算术/位运算用于编译时常量
// (如 t.CArray[T, MAX_ORDER + 1] 的数组长度),放行不报 binaryOperationNotAllowed。
if (node.d.operator !== OperatorType.BitwiseOr) {
if (node.d.operator !== OperatorType.BitwiseOr && !isViperIntegerConstantArithmetic(node, leftType, rightType)) {
evaluator.addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.binaryOperationNotAllowed(), node);
return { type: UnknownType.create() };
}
@@ -439,6 +473,7 @@ export function getTypeOfBinaryOperation(
{ type: leftType, isIncomplete: leftTypeResult.isIncomplete },
{ type: rightType, isIncomplete: rightTypeResult.isIncomplete },
node,
node.d.leftExpr,
inferenceContext,
diag,
{ isLiteralMathAllowed, isTupleAddAllowed }
@@ -578,12 +613,11 @@ export function getTypeOfAugmentedAssignment(
// Viper 扩展:str/bytes 指针增强赋值(C 指针算术)。
// Viper 中 str/bytes 是 i8* 指针,`dest += 1`(指针递增)是合法的
// C 指针算术,结果类型保持 str/bytes。Python 中 str 不可变故无此语义。
// 仅当运算符为 + 且右操作数是整数时生效。
// 仅当运算符为 + 且右操作数是整数(含 C 整数类型)时生效。
if (IsViperDialect(getFileInfo(node).executionEnvironment.dialect)) {
const leftIsStrBytes = isClassInstance(leftSubtypeUnexpanded) &&
ClassType.isBuiltIn(leftSubtypeUnexpanded, ['str', 'bytes']);
const rightIsInt = isClassInstance(rightSubtypeUnexpanded) &&
ClassType.isBuiltIn(rightSubtypeUnexpanded, 'int');
const rightIsInt = isViperIntegerType(rightSubtypeUnexpanded);
if (leftIsStrBytes && rightIsInt) {
return leftSubtypeUnexpanded;
}
@@ -643,6 +677,7 @@ export function getTypeOfAugmentedAssignment(
{ type: leftSubtypeUnexpanded, isIncomplete: leftTypeResult.isIncomplete },
{ type: rightSubtypeUnexpanded, isIncomplete: rightTypeResult.isIncomplete },
node,
node.d.leftExpr,
inferenceContext,
diag,
{ isLiteralMathAllowed, isTupleAddAllowed }
@@ -689,6 +724,19 @@ export function getTypeOfUnaryOperation(
flags: EvalFlags,
inferenceContext: InferenceContext | undefined
): TypeResult {
const isViperDialect = IsViperDialect(getFileInfo(node).executionEnvironment.dialect);
// Viper 扩展:类型注解上下文中的一元 '*' 表示 C 指针类型(T* = CPtr[T])。
// parser 将 `T*`/`T**`/`T***` 后缀解析为 UnaryOperationNode(Multiply) 嵌套,
// 此处须在 TypeExpression 守卫(unaryOperationNotAllowed)之前处理。
if (
isViperDialect &&
node.d.operator === OperatorType.Multiply &&
(flags & EvalFlags.TypeExpression) !== 0
) {
return getTypeOfViperPointerAnnotation(evaluator, node);
}
if ((flags & EvalFlags.TypeExpression) !== 0) {
evaluator.addDiagnostic(DiagnosticRule.reportInvalidTypeForm, LocMessage.unaryOperationNotAllowed(), node);
return { type: UnknownType.create() };
@@ -703,6 +751,35 @@ export function getTypeOfUnaryOperation(
return { type: NeverType.createNever(), isIncomplete };
}
// Viper 扩展:一元 '*'(解引用)与 '&'(取地址),与 C 语义一致。
// 在 Viper 方言下,前缀 '*'/'&' 被 parser 解析为 UnaryOperation
// (operator = Multiply / BitwiseAnd),此处直接按 C 语义求值,
// 不走魔法方法路径(指针没有 __pos__ 等)。
if (isViperDialect) {
if (node.d.operator === OperatorType.Multiply) {
// Viper 扩展:值上下文里一元 '*' 的操作数若是「类型对象」(可实例化类,
// 如 t.CInt、ast.AST),则 `T*` 是 C 指针类型表达式 CPtr[T],而非对值的解引用。
// 于是 `(T*)x`、`(T*)(x)`、`T*(x)` 中的括号内/实参里的 `T*` 都被求值为指针类型。
// 只有真值(CPtr / str / bytes / 数值等)才走解引用路径。
//
// Viper 扩展:parser 标记的「指针类型后缀」节点(`_parseViperPointerTypeSuffix`
// 打上 isViperPointerTypeSuffix)在值上下文里一律是类型表达式,不做解引用。
// 多级指针 `T**`(AST = *(*T))的外层 '*' 其操作数是内层已求值出的 CPtr[T]
// 实例(非可实例化类),若按上一条判据会落入解引用 → 得到 T,使
// `(T**)(x)` 的括号内类型退化成 T(报"T 的对象不可调用")并丢失二级指针。
if (node.d.isViperPointerTypeSuffix) {
return getTypeOfViperPointerAnnotation(evaluator, node);
}
if (isInstantiableClass(exprType)) {
return getTypeOfViperPointerAnnotation(evaluator, node);
}
return getTypeOfViperDeref(evaluator, node, exprType, isIncomplete);
}
if (node.d.operator === OperatorType.BitwiseAnd) {
return getTypeOfViperAddrOf(evaluator, node, exprType, isIncomplete);
}
}
// Map unary operators to magic functions. Note that the bitwise
// invert has two magic functions that are aliases of each other.
const unaryOperatorMap: { [operator: number]: string } = {
@@ -820,6 +897,308 @@ export function getTypeOfUnaryOperation(
return { type, isIncomplete, magicMethodDeprecationInfo: deprecatedInfo };
}
// Viper 扩展:类型表达式里的**编译期整型常量运算**放行(不报 binaryOperationNotAllowed)。
// Viper 是 AOT 静态语言,数组长度等类型参数就是编译期整型常量表达式:
// `x: t.CArray[t.CUInt8T, MAX_ORDER + 1]`
// `part_types: [MAX_VOLUMES * MAX_PARTITIONS]uint8`(`[SIZE]T` 脱糖为 t.CArray[T, SIZE])
// 判据:两侧都是 Viper 整型(含整型字面量),且运算符属于算术/位运算集合。
// 其他情况(如类名相加、比较运算)仍按原逻辑报错。
function isViperIntegerConstantArithmetic(node: BinaryOperationNode, leftType: Type, rightType: Type): boolean {
if (!IsViperDialect(getFileInfo(node).executionEnvironment.dialect)) {
return false;
}
if (!isViperIntegerType(leftType) || !isViperIntegerType(rightType)) {
return false;
}
switch (node.d.operator) {
case OperatorType.Add:
case OperatorType.Subtract:
case OperatorType.Multiply:
case OperatorType.Power:
case OperatorType.Divide:
case OperatorType.FloorDivide:
case OperatorType.Mod:
case OperatorType.LeftShift:
case OperatorType.RightShift:
case OperatorType.BitwiseAnd:
case OperatorType.BitwiseXor:
return true;
default:
return false;
}
}
// Viper 扩展:C 指针算术的结果类型。
// - `p + n` / `p - n` / `n + p`(p 为 str/bytes,即 char*;n 为整数或 char)
// → 结果仍是指针 p 的类型(类型不变)
// - 其它组合返回 undefined,交由常规路径(可能报"运算符不支持")
function getViperPointerArithmeticResult(
operator: OperatorType,
leftType: Type,
rightType: Type
): Type | undefined {
const isViperPointer = (type: Type) => isClassInstance(type) && ClassType.isBuiltIn(type, ['str', 'bytes']);
const isOffsetOperand = (type: Type) =>
isViperIntegerType(type) || (isClassInstance(type) && ClassType.isBuiltIn(type, 'char'));
if (
(operator === OperatorType.Add || operator === OperatorType.Subtract) &&
isViperPointer(leftType) &&
isOffsetOperand(rightType)
) {
return leftType;
}
// 加法可交换:`n + p` 等价于 `p + n`。
if (operator === OperatorType.Add && isViperPointer(rightType) && isOffsetOperand(leftType)) {
return rightType;
}
return undefined;
}
// Viper 扩展:一元 '*'(解引用)类型求值,与 C 语义一致。
// - CPtr[X](含 `X | t.CPtr` 化简形式)→ 返回目标类型 X(类对象转为实例)
// - str / bytes(C 的 char*,即 i8*)→ 返回 char(i8)
// - Unknown/Any → 透传
// - 非指针类型 → 报错(C 语义:invalid type argument of unary '*')
function getTypeOfViperDeref(
evaluator: TypeEvaluator,
node: UnaryOperationNode,
exprType: Type,
isIncomplete: boolean | undefined
): TypeResult {
const resultTypes: Type[] = [];
let sawNonPointer = false;
const isViperDialect = IsViperDialect(getFileInfo(node).executionEnvironment.dialect);
mapSubtypes(exprType, (subtype) => {
if (isAnyOrUnknown(subtype)) {
resultTypes.push(subtype);
return subtype;
}
// Viper 扩展:类型描述符(const / volatile / static 等修饰符)不是指针,
// 原样保留在结果里,不算「非指针类型」:
// `lfn: const str`(AST = `CConst | str`)→ `*lfn` = `CConst | char`
// 可写性另由赋值侧判定(typeEvaluator.assignTypeToExpression 的
// 「解引用赋值」分支拒绝 const 指针),故这里只放行、不报错。
if (isViperDialect && isClass(subtype) && isCModifierType(subtype)) {
resultTypes.push(subtype);
return subtype;
}
// Viper:str / bytes 都是 C 的 char*(i8*),解引用取首字符,类型为 char(i8)。
if (isClassInstance(subtype) && ClassType.isBuiltIn(subtype, ['str', 'bytes'])) {
resultTypes.push(evaluator.getBuiltInObject(node, 'char'));
return subtype;
}
if (isClassInstance(subtype) && isCPtrType(subtype)) {
const typeArgs = subtype.priv.typeArgs;
if (typeArgs && typeArgs.length > 0) {
const pointee = typeArgs[0];
resultTypes.push(
isClass(pointee) && !isClassInstance(pointee) ? convertToInstance(pointee) : pointee
);
} else {
// 裸 CPtr(无类型参数)无法确定目标类型。
resultTypes.push(UnknownType.create());
}
return subtype;
}
// 非指针类型:C 语义报错。
sawNonPointer = true;
return subtype;
});
if (sawNonPointer) {
evaluator.addDiagnostic(
DiagnosticRule.reportOperatorIssue,
LocMessage.typeNotSupportUnaryOperator().format({
operator: printOperator(node.d.operator),
type: evaluator.printType(exprType),
}),
node
);
}
if (resultTypes.length === 0) {
return { type: UnknownType.create(isIncomplete), isIncomplete };
}
return { type: combineTypes(resultTypes), isIncomplete };
}
// Viper 扩展:类型注解上下文的一元 '*' → C 指针类型 CPtr[T](即 `T*` 后缀语法)。
// 与 `T | t.CPtr` 联合化简(combineTypes → CPtr[T])的表示一致:
// 类对象(instantiable)先转为实例再作为 CPtr 的类型参数;TypeVar 直接作为类型参数
// (泛型中的 `T*` → CPtr[T])。每个嵌套层递归求值,`T**` 自然得到 CPtr[CPtr[T]]。
function getTypeOfViperPointerAnnotation(evaluator: TypeEvaluator, node: UnaryOperationNode): TypeResult {
const innerResult = evaluator.getTypeOfExpression(node.d.expr, EvalFlags.TypeExpression);
const innerType = innerResult.type;
const resultTypes: Type[] = [];
let sawInvalidType = false;
const isViperDialect = IsViperDialect(getFileInfo(node).executionEnvironment.dialect);
mapSubtypes(innerType, (subtype) => {
if (isAnyOrUnknown(subtype)) {
resultTypes.push(subtype);
return subtype;
}
// Viper 扩展:类型描述符(const / volatile / static 等修饰符)不出现在指针
// 的类型参数里,原样保留在结果中:
// `const int*`(AST = `(CConst | int)*`)→ `CConst | CPtr[int]`
// 与 Viper 惯用的 `CConst | CChar | CPtr`(const char*)表示一致,
// 避免产生 `CPtr[CConst] | CPtr[int]` 这类"指针的联合"。
if (isViperDialect && isClass(subtype) && isCModifierType(subtype)) {
resultTypes.push(subtype);
return subtype;
}
// Viper 扩展:`T*` 的 T 是裸模块名、且模块内有同名类时(见
// getViperModuleSameNameClass),取该类作为类型参数:
// `bootinfo*(saved_rdi)` → CPtr[bootinfo](而非 CPtr[Module])。
const sameNameClass = getViperModuleSameNameClass(evaluator, node.d.expr, subtype);
const typeArg = sameNameClass ?? (isClass(subtype) && !isClassInstance(subtype) ? convertToInstance(subtype) : subtype);
const cptrType = getViperCPtrTypeForTypeArg(evaluator, node, typeArg);
if (cptrType) {
resultTypes.push(cptrType);
return subtype;
}
sawInvalidType = true;
return subtype;
});
if (sawInvalidType) {
evaluator.addDiagnostic(
DiagnosticRule.reportInvalidTypeForm,
LocMessage.typeNotSupportUnaryOperator().format({
operator: printOperator(node.d.operator),
type: evaluator.printType(innerType),
}),
node
);
}
if (resultTypes.length === 0) {
return { type: UnknownType.create(innerResult.isIncomplete), isIncomplete: innerResult.isIncomplete };
}
return { type: combineTypes(resultTypes), isIncomplete: innerResult.isIncomplete };
}
// Viper 扩展:一元 '&'(取地址)类型求值,与 C 语义一致。
// - x 类型为 X → 返回 CPtr[X](即 `X | t.CPtr` 的 Viper 指针表示)
function getTypeOfViperAddrOf(
evaluator: TypeEvaluator,
node: UnaryOperationNode,
exprType: Type,
isIncomplete: boolean | undefined
): TypeResult {
const cptrType = getViperCPtrTypeForTypeArg(evaluator, node, exprType);
if (!cptrType) {
return { type: UnknownType.create(isIncomplete), isIncomplete };
}
return { type: cptrType, isIncomplete };
}
// 构造 CPtr[typeArg](Viper 指针类型),通过 t 模块的 CPtr 泛型类特化。
function getViperCPtrTypeForTypeArg(evaluator: TypeEvaluator, node: ParseNode, typeArg: Type): Type | undefined {
const cptrClass = getViperCPtrClass(evaluator, node);
if (cptrClass) {
return convertToInstance(ClassType.specialize(cptrClass, [typeArg]));
}
return undefined;
}
// 解析 Viper C 风格强转 `T*(x)` 左操作数 `T` 的被指向类型。
// - 常规:`T` 是可实例化类(t.CInt / bootinfo.bootinfo 等)→ 取其**实例**形态作为被指向类型;
// - 模块同名类:见 getViperModuleSameNameClass;
// - 其余(真值类型)返回 undefined,不兜底:交由常规算术校验给出真实报错。
function getViperCastTypeArg(evaluator: TypeEvaluator, leftExpression: ExpressionNode, leftType: Type): Type | undefined {
if (isInstantiableClass(leftType)) {
return isClass(leftType) && !isClassInstance(leftType) ? convertToInstance(leftType) : leftType;
}
return getViperModuleSameNameClass(evaluator, leftExpression, leftType);
}
// Viper 扩展:裸名同时是「模块名」与「模块内同名类」时,取该类(实例形态)。
// 典型场景(ViperOS VKernel/Kernel/main.vp):`import bootinfo`(模块)与
// bootinfo.vp 的 `class bootinfo` 同名,代码写作 `BootInfo = bootinfo*(saved_rdi)`。
// TPC 的 struct 注册表按短名全局查找,故裸名 bootinfo 取到该结构体类;LSP 按
// 「模块内同名类」等价还原。模块内没有同名类时返回 undefined(不兜底)。
function getViperModuleSameNameClass(
evaluator: TypeEvaluator,
exprNode: ExpressionNode,
type: Type
): Type | undefined {
if (!isModule(type) || exprNode.nodeType !== ParseNodeType.Name) {
return undefined;
}
const sameNameSymbol = ModuleType.getField(type, exprNode.d.value);
if (!sameNameSymbol) {
return undefined;
}
const memberType = evaluator.getEffectiveTypeOfSymbolForUsage(
sameNameSymbol,
/* usageNode */ undefined,
/* useLastDecl */ true
).type;
return isInstantiableClass(memberType) ? convertToInstance(memberType) : undefined;
}
// 取 Viper 的 CPtr 泛型类(可实例化形式)。
// - 首选源码 `import t` 可见的 `t.CPtr`;
// - 回退到 builtins stub 的编译器内置 `rawptr`(等价 t.CPtr,见 builtins.vpi),
// 使 `T*` / `&x` 指针语法在未 `import t` 的文件中同样可用
// (与 T* 注解、typedef、[SIZE]T 一致:不强制源码 import t)。
function getViperCPtrClass(evaluator: TypeEvaluator, node: ParseNode): ClassType | undefined {
const tSymbolWithScope = evaluator.lookUpSymbolRecursive(node, 't', /* honorCodeFlow */ true);
if (tSymbolWithScope) {
const tEffectiveType = evaluator.getEffectiveTypeOfSymbolForUsage(
tSymbolWithScope.symbol,
/* usageNode */ undefined,
/* useLastDecl */ true
).type;
if (isModule(tEffectiveType)) {
const cptrSymbol = ModuleType.getField(tEffectiveType, 'CPtr');
if (cptrSymbol) {
const cptrType = evaluator.getEffectiveTypeOfSymbolForUsage(
cptrSymbol,
/* usageNode */ undefined,
/* useLastDecl */ true
).type;
if (isInstantiableClass(cptrType)) {
return cptrType;
}
}
}
}
// 回退:builtins 中的 rawptr(= t.CPtr)。
const rawptrSymbolWithScope = evaluator.lookUpSymbolRecursive(node, 'rawptr', /* honorCodeFlow */ true);
if (rawptrSymbolWithScope) {
const rawptrType = evaluator.getEffectiveTypeOfSymbolForUsage(
rawptrSymbolWithScope.symbol,
/* usageNode */ undefined,
/* useLastDecl */ true
).type;
if (isInstantiableClass(rawptrType)) {
return rawptrType;
}
}
return undefined;
}
export function getTypeOfTernaryOperation(
evaluator: TypeEvaluator,
node: TernaryNode,
@@ -870,6 +1249,57 @@ export function getTypeOfTernaryOperation(
return { type: combineTypes(typesToCombine), isIncomplete, typeErrors };
}
// Viper 扩展:`X | t.CPtr` 的 Viper 语义是"指向 X 的指针"(X*)。combineTypes 里的
// Struct | CPtr 折叠覆盖了 X 不是指针的情形;但当 X 自身就是指针时(如
// `HANDLE = VOIDPTR = t.CPtr`,两侧在类型层完全同形,都是 type[CPtr[Unknown]]),
// 类型层已无法区分"外层指针模板"与"被指向类型",两者会被并成一级指针,使
// `HANDLE | t.CPtr` 从 void**(void 的指针的指针)退化成 void*,于是取值
// `&handle`(void**)传参/返回会被报"跨级指针隐式转换不允许"(误报)。
// 此处按语法分辨:右操作数就是裸 CPtr 类(`t.CPtr` / `rawptr`)且左操作数已是指针
// 形态时,直接把左操作数包一层指针(保持输入的实例 / 可实例化形态)。
function maybeWrapViperCPtrUnion(
node: BinaryOperationNode,
flags: EvalFlags,
leftType: Type,
rightType: Type
): Type | undefined {
// 只在类型表达式位置生效,值上下文的 `a | b` 不受影响。
if ((flags & EvalFlags.TypeExpression) === 0) {
return undefined;
}
if (!isViperBareCPtrClassReference(node.d.rightExpr)) {
return undefined;
}
// 右操作数须确实解析为 CPtr 类(挡住用户自定义的同名成员,如 `Foo.CPtr`)。
if (!isClass(rightType) || !isCPtrType(rightType)) {
return undefined;
}
// 左操作数须已是指针;否则交给 combineTypes 的 Struct | CPtr 折叠
// (如 `Scheduler | t.CPtr` → CPtr[Scheduler]、`CConst | CChar | CPtr` → const char*)。
if (!someSubtypes(leftType, (subtype) => isClass(subtype) && isCPtrType(subtype))) {
return undefined;
}
const isInstantiableForm = TypeBase.isInstantiable(leftType);
const pointee = isClass(leftType) && isInstantiableForm ? ClassType.cloneAsInstance(leftType) : leftType;
const template = TypeBase.isInstantiable(rightType) ? rightType : ClassType.cloneAsInstantiable(rightType);
const wrapped = ClassType.specialize(template, [pointee]);
return isInstantiableForm ? wrapped : ClassType.cloneAsInstance(wrapped);
}
/** Viper:右操作数是否是裸 CPtr 类引用(`t.CPtr` / `CPtr` / `rawptr`,不带下标实参)。 */
function isViperBareCPtrClassReference(node: ExpressionNode): boolean {
const isCPtrName = (name: string) => name === 'CPtr' || name === 'rawptr';
if (node.nodeType === ParseNodeType.Name) {
return isCPtrName(node.d.value);
}
if (node.nodeType === ParseNodeType.MemberAccess) {
return node.d.member.nodeType === ParseNodeType.Name && isCPtrName(node.d.member.d.value);
}
return false;
}
function createUnionType(
evaluator: TypeEvaluator,
node: BinaryOperationNode,
@@ -918,6 +1348,13 @@ function createUnionType(
flags | EvalFlags.InstantiableType
);
// Viper 扩展:`X | t.CPtr` 且 X 已是指针时,按"包一层指针"处理
// (见 maybeWrapViperCPtrUnion),此时结果不是联合类型,直接返回。
const wrappedPointerType = maybeWrapViperCPtrUnion(node, flags, adjustedLeftType, adjustedRightType);
if (wrappedPointerType) {
return { type: wrappedPointerType };
}
let newUnion = combineTypes([adjustedLeftType, adjustedRightType], { skipElideRedundantLiterals: true });
// Viper 扩展:若 combineTypes 已将 Struct | CPtr 折叠为 CPtr[Struct],
@@ -1144,6 +1581,29 @@ function calcLiteralForBinaryOp(operator: OperatorType, leftType: Type, rightTyp
return undefined;
}
// Viper 扩展:判断 `|` 是否为「类型描述符组合」(TypeSet),如
// `CConst | int`(const int)、`CConst | int*`(const int*,右侧求值为 CPtr[int] 实例)。
// Python 的 `|` 走联合要求两侧同为可实例化类型;而 `T*` 求值出的是**实例**
// (CPtr[int]),故 `CConst | int*` 会落到 `__or__` 分支并退化为 Unknown。
// 描述符(const / volatile / static 等)本身只是编译期修饰,不含运行时 `__or__`,
// 这里把「任一侧含描述符」的组合按联合处理,交由 combineTypes 归一。
// 注:两侧同为可实例化类型的组合(如 CConst | CChar | CPtr)本就走 isUnionableType,
// 不受此处影响。
function isViperTypeDescriptorUnion(node: ParseNode, leftType: Type, rightType: Type): boolean {
if (!IsViperDialect(getFileInfo(node).executionEnvironment.dialect)) {
return false;
}
const containsDescriptor = (type: Type): boolean => {
if (isUnion(type)) {
return type.priv.subtypes.some((subtype) => isCModifierType(subtype));
}
return isCModifierType(type);
};
return containsDescriptor(leftType) || containsDescriptor(rightType);
}
function customMetaclassSupportsMethod(type: Type, methodName: string): boolean {
if (!isInstantiableClass(type)) {
return false;
@@ -1346,7 +1806,8 @@ function validateArithmeticOperation(
if (isViperDialect) {
if (
isCStaticType(leftSubtypeUnexpanded) || isCStaticType(rightSubtypeUnexpanded) ||
isCConstType(leftSubtypeUnexpanded) || isCConstType(rightSubtypeUnexpanded)
isCConstType(leftSubtypeUnexpanded) || isCConstType(rightSubtypeUnexpanded) ||
isCVolatileType(leftSubtypeUnexpanded) || isCVolatileType(rightSubtypeUnexpanded)
) {
return UnknownType.create(isIncomplete);
}
@@ -1382,9 +1843,26 @@ function validateArithmeticOperation(
ClassType.isBuiltIn(leftSubtypeExpanded, ['int', 'float', 'bool', 'bytes', 'str']);
const rightIsBuiltinNum = isClassInstance(rightSubtypeExpanded) &&
ClassType.isBuiltIn(rightSubtypeExpanded, ['int', 'float', 'bool', 'bytes', 'str']);
// str/bytes 指针算术优先:一方是 str/bytes(i8* 指针)、另一方是
// 整数(含 C 整数类型)且运算符为 + 或 - 时,不走 CType 分支
// (否则 `str + CInt` 会误返回 CInt),由下方指针算术分支处理。
// 注意:`int - str` 不是指针算术(指针不能减整数的反向),
// 如 `CChar - '0'` 数字字符转换仍走 CType 分支。
const leftIsStrBytes = isClassInstance(leftSubtypeExpanded) &&
ClassType.isBuiltIn(leftSubtypeExpanded, ['str', 'bytes']);
const rightIsStrBytes = isClassInstance(rightSubtypeExpanded) &&
ClassType.isBuiltIn(rightSubtypeExpanded, ['str', 'bytes']);
const isPointerArith =
(operator === OperatorType.Add &&
((leftIsStrBytes && isViperIntegerType(rightSubtypeExpanded)) ||
(rightIsStrBytes && isViperIntegerType(leftSubtypeExpanded)))) ||
(operator === OperatorType.Subtract &&
leftIsStrBytes &&
(isViperIntegerType(rightSubtypeExpanded) || rightIsStrBytes));
// 任一方是 CType 子类,另一方是 CType 子类或 Python 内置数值/字符串类型
if ((leftIsCType && (rightIsCType || rightIsBuiltinNum)) ||
(rightIsCType && (leftIsCType || leftIsBuiltinNum))) {
if (!isPointerArith &&
((leftIsCType && (rightIsCType || rightIsBuiltinNum)) ||
(rightIsCType && (leftIsCType || leftIsBuiltinNum)))) {
// 算术/位运算结果类型:优先返回 CType 子类(保持类型语义)
if (leftIsCType) {
return leftSubtypeExpanded;
@@ -1405,10 +1883,11 @@ function validateArithmeticOperation(
ClassType.isBuiltIn(leftSubtypeExpanded, ['str', 'bytes']);
const rightIsStrBytes = isClassInstance(rightSubtypeExpanded) &&
ClassType.isBuiltIn(rightSubtypeExpanded, ['str', 'bytes']);
const leftIsInt = isClassInstance(leftSubtypeExpanded) &&
ClassType.isBuiltIn(leftSubtypeExpanded, 'int');
const rightIsInt = isClassInstance(rightSubtypeExpanded) &&
ClassType.isBuiltIn(rightSubtypeExpanded, 'int');
// 整数操作数须涵盖 Viper C 整数类型(CInt 等),否则
// `str + CInt`(如 resp_buf + total)会落入下方 CType 算术分支
// 误返回整数类型,而指针偏移应保持 str/bytes(i8*)指针类型。
const leftIsInt = isViperIntegerType(leftSubtypeExpanded);
const rightIsInt = isViperIntegerType(rightSubtypeExpanded);
if (operator === OperatorType.Add) {
if ((leftIsStrBytes && rightIsInt) || (leftIsInt && rightIsStrBytes)) {
// 返回指针类型(str/bytes)
@@ -462,7 +462,11 @@ export class PackageTypeVerifier {
if (isFile) {
const fileExtension = getFileExtension(entry.name);
if (fileExtension === '.vp' || fileExtension === '.vpi') {
if (
fileExtension === '.vp' ||
fileExtension === '.vpi' ||
fileExtension === '.vpnci'
) {
const nameWithoutExtension = stripFileExtension(entry.name);
if (nameWithoutExtension === '__init__') {
@@ -83,6 +83,21 @@ export function getNodeDepth(node: ParseNode): number {
return depth;
}
// Viper 扩展:判定一条声明的「离节点」是否带 `static` 修饰(C 的 internal linkage,
// 语义为禁止跨模块取参)。两种来源:
// - 函数:parser 把 `static` 记到 FunctionNode.d.viperModifier(与 virtual 同法);
// - 变量:`static x: T` 的变量声明离节点是目标 NameNode(parser 打 isViperStatic 标记),
// 注解节点(TypeAnnotationNode)上也有兜底标记。
// 供 typeEvaluator(`mod.name` 取参)与 checker(`from mod import name`)共用,
// 保证两处判据一致。
export function isViperStaticDeclarationNode(node: ParseNode): boolean {
const declarationFields = node.d as { isViperStatic?: boolean; viperModifier?: string };
if (declarationFields.isViperStatic === true) {
return true;
}
return node.nodeType === ParseNodeType.Function && declarationFields.viperModifier === 'static';
}
// Returns the deepest node that contains the specified position.
export function findNodeByPosition(
node: ParseNode,
@@ -230,6 +245,20 @@ export function printExpression(node: ExpressionNode, flags = PrintExpressionFla
}
case ParseNodeType.Index: {
// Viper 扩展:`[SIZE]T` 数组语法脱糖出的下标节点,按源码原形打印
// (items 为 [长度, 元素类型],省略长度时只有 [元素类型]),
// 使诊断/悬停显示用户实际写下的数组类型而非脱糖后的 t.CArray[...]。
if (node.d.isViperBracketArray === true) {
const sizeArg = node.d.items.length > 1 ? node.d.items[0] : undefined;
const elementArg = node.d.items[node.d.items.length - 1];
return (
'[' +
(sizeArg ? printArg(sizeArg, flags) : '') +
']' +
printExpression(elementArg.d.valueExpr, flags)
);
}
return (
printExpression(node.d.leftExpr, flags) +
'[' +
@@ -33,6 +33,7 @@ import {
EllipsisNode,
ErrorNode,
ExceptNode,
FallthroughNode,
ForNode,
FormatStringNode,
FunctionAnnotationNode,
@@ -185,6 +186,9 @@ export function getChildNodes(node: ParseNode): (ParseNode | undefined)[] {
case ParseNodeType.Except:
return [node.d.typeExpr, node.d.name, node.d.exceptSuite];
case ParseNodeType.Fallthrough:
return [];
case ParseNodeType.For:
return [node.d.targetExpr, node.d.iterableExpr, node.d.forSuite, node.d.elseSuite];
@@ -441,6 +445,9 @@ export class ParseTreeVisitor<T> {
case ParseNodeType.Except:
return this.visitExcept(node);
case ParseNodeType.Fallthrough:
return this.visitFallthrough(node);
case ParseNodeType.For:
return this.visitFor(node);
@@ -711,6 +718,11 @@ export class ParseTreeVisitor<T> {
return this._default;
}
// Viper 扩展:match-case 内的裸 `fallthrough` 语句(case 穿透)。
visitFallthrough(node: FallthroughNode) {
return this._default;
}
visitFor(node: ForNode) {
return this._default;
}
@@ -12,6 +12,7 @@
import { appendArray } from '../common/collectionUtils';
import { assert } from '../common/debug';
import { DiagnosticAddendum } from '../common/diagnostic';
import { IsViperDialect } from '../common/dialect';
import { DiagnosticRule } from '../common/diagnosticRules';
import { LocAddendum, LocMessage } from '../localization/localize';
import {
@@ -29,6 +30,7 @@ import {
PatternValueNode,
} from '../parser/parseNodes';
import { CodeFlowReferenceExpressionNode } from './codeFlowTypes';
import { getFileInfo } from './analyzerNodeInfo';
import { addConstraintsForExpectedType } from './constraintSolver';
import { ConstraintTracker } from './constraintTracker';
import { DeclarationType } from './declaration';
@@ -833,6 +835,7 @@ function narrowTypeBasedOnClassPattern(
pattern: PatternClassNode,
isPositiveTest: boolean
): Type {
const isViperDialect = IsViperDialect(getFileInfo(pattern).executionEnvironment?.dialect);
let exprType = evaluator.getTypeOfExpression(pattern.d.className, EvalFlags.CallBaseDefaults).type;
// If this is a class (but not a type alias that refers to a class),
@@ -1058,6 +1061,33 @@ function narrowTypeBasedOnClassPattern(
}
if (isClassInstance(subjectSubtypeExpanded)) {
// Viper 扩展:指针 subject 的变体模式收窄。
// `match x: case LLVMType.Struct(...)` 中 subject 为 CPtr[LLVMType](指针)时,
// 收窄结果应是 CPtr[LLVMType.Struct](指向变体的指针),而非普通变体值 Struct。
// 否则 case 体内继续把 x 传给 CPtr 参数(如 build_alloca(builder, x))会误报
// "Struct 不可分配给 CPtr[LLVMType]"。
// 判定用 fullName 前缀(LLVMType.Struct ⊂ LLVMType):REnum 变体类与
// 基类之间 assignType 不一定成立(变体不直接继承基类),故不能用 assignType。
if (
isViperDialect &&
isCPtrType(subjectSubtypeExpanded) &&
isInstantiableClass(expandedSubtype)
) {
const subjectTypeArgs = subjectSubtypeExpanded.priv.typeArgs;
const subjectPointee =
subjectTypeArgs && subjectTypeArgs.length > 0 ? subjectTypeArgs[0] : undefined;
if (
subjectPointee &&
isClassInstance(subjectPointee) &&
!isAnyOrUnknown(subjectPointee) &&
expandedSubtype.shared.fullName.startsWith(subjectPointee.shared.fullName + '.')
) {
const patternInstance = ClassType.cloneAsInstance(expandedSubtype);
const cptrResult = ClassType.specialize(subjectSubtypeExpanded, [patternInstance]);
return addConditionToType(cptrResult, getTypeCondition(subjectSubtypeExpanded));
}
}
let resultType: Type;
if (evaluator.assignType(ClassType.cloneAsInstance(expandedSubtype), subjectSubtypeExpanded)) {
@@ -15,6 +15,7 @@ import { OperationCanceledException, throwIfCancellationRequested } from '../com
import { ConfigOptions, ExecutionEnvironment, matchFileSpecs } from '../common/configOptions';
import { ConsoleInterface, StandardConsole } from '../common/console';
import { isThenable } from '../common/core';
import { appendArray } from '../common/collectionUtils';
import * as debug from '../common/debug';
import { assert } from '../common/debug';
import { Diagnostic, DiagnosticCategory } from '../common/diagnostic';
@@ -30,6 +31,8 @@ import { Range, TextRange, doRangesIntersect } from '../common/textRange';
import { Duration, timingStats } from '../common/timing';
import { Uri } from '../common/uri/uri';
import { ParseFileResults, ParserOutput } from '../parser/parser';
import { MemberAccessNode, ParseNodeType } from '../parser/parseNodes';
import { DeclarationType } from './declaration';
import { RequiringAnalysisCount } from './analysis';
import { AbsoluteModuleDescriptor, ImportLookupResult, LookupImportOptions } from './analyzerFileInfo';
import { CellChainIndex, CellChainIndexProvider } from './cellChainIndex';
@@ -157,6 +160,20 @@ export class Program {
private _editModeTracker = new EditModeTracker();
private _sourceFileFactory: ISourceFileFactory;
// Viper 扩展:LSP 状态栏进度所需的「当前正在分析的文件路径」。
// 在 analyze() 的循环里逐文件更新,见 getAnalysisProgress()。
private _currentAnalysisFilePath: string | undefined;
// Viper 扩展:includes/builtins.vp(真实源码,非 .vpi 存根)的 typedef 符号
// 合并状态。key =(typeshed builtins 存根 + 执行环境 + 被分析文件目录),
// 避免对同一环境/目录下的文件重复扫描/合并;不同目录或不同执行环境会各自尝试。
private _viperBuiltinsVpMergedKeys = new Set<string>();
// Viper 扩展:合并过程中的递归保护。查找并绑定 builtins.vp 时会重新进入
// _bindFile,若此时再次走到本方法会对同一文件重复 bind(触发
// "Bind called unnecessarily" 断言),故用该标志中断重入。
private _isMergingViperBuiltinsVp = false;
constructor(
initialImportResolver: ImportResolver,
initialConfigOptions: ConfigOptions,
@@ -287,6 +304,9 @@ export class Program {
this._configOptions = configOptions;
this._importResolver.setConfigOptions(configOptions);
// Viper 扩展:配置变化(includes 等搜索路径调整)后重置 builtins.vp 合并状态。
this._viperBuiltinsVpMergedKeys.clear();
// Create a new evaluator with the updated config options.
this._createNewEvaluator();
}
@@ -698,7 +718,11 @@ export class Program {
// whether the method needs to be called again to complete the
// analysis. In interactive mode, the timeout is always limited
// to the smaller value to maintain responsiveness.
analyze(maxTime?: MaxAnalysisTime, token: CancellationToken = CancellationToken.None): boolean {
analyze(
maxTime?: MaxAnalysisTime,
token: CancellationToken = CancellationToken.None,
onProgress?: () => void
): boolean {
return this._runEvaluatorWithCancellationToken(token, () => {
const elapsedTime = new Duration();
@@ -711,6 +735,8 @@ export class Program {
// Check the open files.
for (const sourceFileInfo of openFiles) {
this._currentAnalysisFilePath = sourceFileInfo.uri.getFilePath();
onProgress?.();
if (this._checkTypes(sourceFileInfo)) {
if (elapsedTime.getDurationInMilliseconds() > effectiveMaxTime) {
return true;
@@ -735,6 +761,8 @@ export class Program {
continue;
}
this._currentAnalysisFilePath = sourceFileInfo.uri.getFilePath();
onProgress?.();
if (this._checkTypes(sourceFileInfo)) {
if (elapsedTime.getDurationInMilliseconds() > effectiveMaxTime) {
return true;
@@ -747,6 +775,22 @@ export class Program {
});
}
/**
* Viper 扩展:返回分析进度的快照,供 LSP 客户端在状态栏显示
* 「正在分析哪个文件 / 已分析多少 / 共多少个用户文件」。
* 计数与 AnalysisResults.requiringAnalysisCount 同源(都走 getFilesToAnalyzeCount),
* 因此百分比不会与实际分析进度脱节。
*/
getAnalysisProgress(): { currentFile: string | undefined; analyzed: number; total: number } {
const total = this.getUserFileCount();
const pending = this.getFilesToAnalyzeCount().files;
return {
currentFile: this._currentAnalysisFilePath,
analyzed: Math.min(total, Math.max(0, total - pending)),
total,
};
}
// Performs parsing and analysis of a single file in the program. If the file is not part of
// the program returns false to indicate analysis was not performed.
analyzeFile(fileUri: Uri, token: CancellationToken = CancellationToken.None): boolean {
@@ -1879,6 +1923,13 @@ export class Program {
// the builtin module.
builtinsScope =
getScopeIfAvailable(fileToBind.chainedSourceFile) ?? getScopeIfAvailable(fileToBind.builtinsImport);
// Viper 扩展:将项目 includes 中 builtins.vp(真实源码,非 .vpi 存根)
// 的 t.CTypedef 类型别名(rawptr/size_t 等)合并进 builtin 作用域。
// 对齐 TransPyC TypeAnnotationResolver / TPV _lookup_builtins_typedef 的
// "builtins 最后匹配"语义:builtins 始终纳入编译链但不在用户文件的
// import 闭包内,其 typedef 声明需作为注解名解析的兜底。
this._mergeViperBuiltinsVpSymbols(fileToBind, builtinsScope);
}
if (fileToBind.sourceFile.isParseRequired()) {
@@ -1902,6 +1953,153 @@ export class Program {
return true;
}
// Viper 扩展:将项目 includes 中 builtins.vp(真实源码,非 .vpi 存根)的
// t.CTypedef 类型别名(rawptr/size_t 等)合并进 builtin 作用域。
//
// 背景:TransPyC/TPV 的"builtins 最后匹配"——builtins 模块始终纳入编译链,
// 但不在用户文件的 import 闭包内,其 `NAME: t.CTypedef = TYPE` 声明(如
// builtins.vp 中的 rawptr = t.CPtr、size_t = __stdint.size_t)不随
// from_imports 注入,编译器按源文件扫描 builtins.vp 作为注解名解析兜底
// (见 TransPyC lib/core/TypeAnnotationResolver.py)。
//
// LSP 侧 builtin 作用域仍以 builtins.vpi(typeshed 存根,提供 int/str/Asm 等)
// 为基准,这里仅补充 builtins.vp 中 builtin 作用域缺失的 typedef 符号,
// 不覆盖既有内建语义。
private _mergeViperBuiltinsVpSymbols(fileToBind: SourceFileInfo, builtinsScope: Scope | undefined) {
if (!builtinsScope || !fileToBind.builtinsImport) {
return;
}
// 在 workspace root / 项目 includes(extraPaths)中定位 builtins.vp。
// 注意:用 fileToBind.uri 而非 builtinsImport.uri(后者位于 typeshed,
// findExecEnvironment 会落到默认环境而丢失 extraPaths)。
const execEnv = this._configOptions.findExecEnvironment(fileToBind.uri);
const seeds: Uri[] = [];
if (fileToBind.uri && !fileToBind.uri.isEmpty()) {
seeds.push(fileToBind.uri.getDirectory());
}
if (execEnv?.root) {
seeds.push(execEnv.root);
}
// Viper 扩展:候选位置只取决于"被分析文件所在目录 + 执行环境(root/extraPaths)",
// 因此缓存键必须包含这两者,不能只用 fileToBind.builtinsImport.uri.key ——
// 该 uri 是 typeshed 的 builtins 存根,整个 Program 共用同一个键:若第一个
// 绑定的文件所在环境找不到 builtins.vp(工程外的散落文件、多根工作区的另一个
// 根、`.tpv_build` 下的生成物等),标记后其它文件/工程会被永久跳过合并,
// 表现为 uint32/int8/sizet 等 typedef 集体报"未定义"(同一工程在 VSCode 与
// Trae 因打开的文件集合/顺序不同而结果不同)。
// 仍在查找前标记(防止绑定 builtins.vp 时递归进入本方法):查找失败只跳过
// "同目录 + 同环境"的后续文件,其它目录/环境会各自尝试。
const key = [
fileToBind.builtinsImport.uri.key,
execEnv?.root?.key ?? '',
...(execEnv?.extraPaths ?? []).map((extraPath) => extraPath.key),
...seeds.map((seed) => seed.key),
].join('|');
if (this._viperBuiltinsVpMergedKeys.has(key)) {
return;
}
// 合并过程中的重入(绑定 builtins.vp 自身时)直接跳过:
// 外层调用已在执行同一次合并,符号表无需重复写入。
if (this._isMergingViperBuiltinsVp) {
return;
}
this._viperBuiltinsVpMergedKeys.add(key);
const candidates: Uri[] = [];
if (execEnv?.root) {
candidates.push(execEnv.root);
}
if (execEnv) {
appendArray(candidates, execEnv.extraPaths);
}
// Viper 扩展:真实编译器始终把 stdlib 的 includes 目录纳入编译链,
// 与工程是否在 .vpj / viperconfig 中声明 includes 无关。这里额外沿
// "被分析文件" 与 "执行环境根" 的祖先目录上溯(最多 6 层),把每个
// 祖先目录自身及其 includes/ 子目录也纳入 builtins.vp 的候选位置,
// 使 `<root>/includes/builtins.vp` 对 `<root>/Test/.../App/main.vp`
// 这类未显式配置 includes 的工程同样可用(对齐编译器行为)。
const ancestorKeys = new Set<string>(candidates.map((c) => c.key));
for (const seed of seeds) {
let dir: Uri | undefined = seed;
for (let depth = 0; depth < 6 && dir && !dir.isEmpty(); depth++) {
for (const candidateDir of [dir, dir.combinePathsUnsafe('includes')]) {
if (ancestorKeys.has(candidateDir.key)) {
continue;
}
ancestorKeys.add(candidateDir.key);
candidates.push(candidateDir);
}
const parent = dir.getDirectory();
if (parent.equals(dir)) {
break;
}
dir = parent;
}
}
let builtinsVpUri: Uri | undefined;
for (const dir of candidates) {
const candidate = dir.combinePathsUnsafe('builtins').addExtension('.vp');
if (this.fileSystem.existsSync(candidate)) {
builtinsVpUri = candidate;
break;
}
}
if (!builtinsVpUri) {
return;
}
// 加载并绑定 builtins.vp(_lookUpImport 会按需绑定)。
// 绑定过程中会重新进入 _bindFile → 本方法(builtins.vp 自身也要合并),
// 此时必须中断重入,否则会对同一文件重复 bind("Bind called unnecessarily")。
this.addTrackedFile(builtinsVpUri);
this._isMergingViperBuiltinsVp = true;
try {
this._lookUpImport(builtinsVpUri);
} finally {
this._isMergingViperBuiltinsVp = false;
}
const builtinsVpInfo = this.getSourceFileInfo(builtinsVpUri);
const parseResults = builtinsVpInfo?.sourceFile.getParserOutput();
if (!builtinsVpInfo || !parseResults) {
return;
}
const builtinsVpScope = AnalyzerNodeInfo.getScope(parseResults.parseTree);
if (!builtinsVpScope) {
return;
}
// 合并:builtins.vp 的声明优先覆盖 builtin 作用域(完全对齐真实编译器,
// 如 ord/chr 的 t.CChar 签名、min/max 的 double 签名、rawptr/size_t 等 typedef)。
// 覆盖范围:t.CTypedef 类型别名声明 + 函数声明;跳过模块别名(__stdint/t)
// 与隐式 dunder 符号,避免引入不必要的隐式全局。
for (const [name, symbol] of builtinsVpScope.symbolTable) {
const decls = symbol.getDeclarations();
const isViperDecl = decls.some(
(decl) =>
(decl.type === DeclarationType.Variable &&
!!decl.typeAnnotationNode &&
decl.typeAnnotationNode.nodeType === ParseNodeType.MemberAccess &&
(decl.typeAnnotationNode as MemberAccessNode).d.member.d.value === 'CTypedef') ||
decl.type === DeclarationType.Function
);
if (!isViperDecl) {
continue;
}
builtinsScope.symbolTable.set(name, symbol);
}
}
private _getEffectiveFutureImports(futureImports: Set<string>, chainedSourceFile: SourceFileInfo): Set<string> {
const effectiveFutureImports = new Set<string>(futureImports);
@@ -8,7 +8,7 @@
*/
import { FileSystem } from '../common/fileSystem';
import { IsViperDialect, LanguageDialect } from '../common/dialect';
import { LanguageDialect } from '../common/dialect';
import { Uri } from '../common/uri/uri';
import { isDirectory, isFile } from '../common/uri/uriUtils';
@@ -20,16 +20,16 @@ export interface PyTypedInfo {
//
// Retrieves information about a py.typed file, if it exists, under the given path.
//
// 注:本模块只做纯文件系统探测,不做方言分支。Viper 工程同样需要该探测:
// `foo-stubs/` 存根包(PEP 561 式布局)的虚拟映射(PartialStubService)依据
// py.typed 的 `partial` 标记决定是否把存根合并进已安装目录;此前按方言短路
// 会让映射永不生效(viperFileSystem 用例全部失败)。
// dialect 参数保留是为了兼容既有调用点,当前实现不依赖它。
export function getPyTypedInfo(
fileSystem: FileSystem,
dirPath: Uri,
dialect?: LanguageDialect
_dialect?: LanguageDialect
): PyTypedInfo | undefined {
// Viper 模式短路:Viper 无 PEP 561 py.typed 标记机制,跳过探测。
if (IsViperDialect(dialect)) {
return undefined;
}
if (!fileSystem.existsSync(dirPath) || !isDirectory(fileSystem, dirPath)) {
return undefined;
}
@@ -39,7 +39,7 @@ export function getPyTypedInfo(
return undefined;
}
return getPyTypedInfoForPyTypedFile(fileSystem, pyTypedPath, dialect);
return getPyTypedInfoForPyTypedFile(fileSystem, pyTypedPath, _dialect);
}
//
@@ -48,13 +48,8 @@ export function getPyTypedInfo(
export function getPyTypedInfoForPyTypedFile(
fileSystem: FileSystem,
pyTypedPath: Uri,
dialect?: LanguageDialect
_dialect?: LanguageDialect
) {
// Viper 模式短路:Viper 无 PEP 561 py.typed 标记机制,跳过解析。
if (IsViperDialect(dialect)) {
return undefined;
}
// This function intentionally doesn't check whether the given py.typed path exists or not,
// as filesystem access is expensive if done repeatedly.
// The caller should verify the file's validity before calling this method and use a cache if possible
@@ -52,7 +52,7 @@ import {
isFile,
tryStat,
} from '../common/uri/uriUtils';
import { AnalysisCompleteCallback } from './analysis';
import { AnalysisCompleteCallback, AnalysisProgressCallback } from './analysis';
import {
BackgroundAnalysisProgram,
BackgroundAnalysisProgramFactory,
@@ -340,6 +340,14 @@ export class AnalyzerService {
this._backgroundAnalysisProgram.setCompletionCallback(callback);
}
/**
* Viper 扩展:注册分析进度回调(逐文件上报「正在分析哪个文件」),
* 由 languageServerBase 转成客户端状态栏进度通知。
*/
setAnalysisProgressCallback(callback: AnalysisProgressCallback | undefined): void {
this._backgroundAnalysisProgram.setAnalysisProgressCallback(callback);
}
setOptions(commandLineOptions: CommandLineOptions): void {
this._commandLineOptions = commandLineOptions;
@@ -1459,9 +1467,19 @@ export class AnalyzerService {
});
}
// 写入 defaultExtraPaths;_ensureDefaultOptions 检测到已设置时不会覆盖。
// 累加写入 defaultExtraPaths(多个子项目配置时必须累加而非覆盖,
// 否则后一个配置会挤掉前一个的 source_dir/includes);
// _ensureDefaultOptions 检测到已设置时不会覆盖。
if (extraPaths.length > 0) {
configOptions.defaultExtraPaths = extraPaths;
const existing = configOptions.defaultExtraPaths ?? [];
const seen = new Set(existing.map((u) => u.key));
for (const p of extraPaths) {
if (!seen.has(p.key)) {
existing.push(p);
seen.add(p.key);
}
}
configOptions.defaultExtraPaths = existing;
}
}
@@ -69,24 +69,37 @@ export function findVpjFileInDir(fs: ReadOnlyFileSystem, dirUri: Uri): Uri | und
return undefined;
}
// Viper 扩展:在目录的一级子目录中查找所有 Viper 项目配置文件(*.vpj / project.json)。
// Viper 扩展:递归查找工作区下所有 Viper 项目配置文件(*.vpj / project.json)。
// 用于 LSP 模式下工作区根(如 TransPyC)未直接包含配置文件,
// 但子目录(如 TransPyV/)包含 Viper 项目配置的场景。
// 但子目录(如 TransPyV/、Test/G4/)包含 Viper 项目配置的场景。
// 每个子项目的 source_dir(如 ./App)都应作为全局导入根,
// includes 作为额外搜索路径 —— App 根级全局可导入。
// 仅扫描一级子目录(非递归),避免深层遍历开销。
// 递归深度限制 + 跳过常见非项目目录,避免深层遍历开销。
// 失败或未找到时返回空数组。
const _viperConfigScanMaxDepth = 6;
const _viperConfigSkipDirs = new Set(['.git', 'node_modules', '__pycache__', '.tpv_build', 'build', 'dist']);
export function findViperProjectConfigsInChildDirs(fs: ReadOnlyFileSystem, dirUri: Uri): Uri[] {
return _findViperProjectConfigsRecursive(fs, dirUri, 1);
}
function _findViperProjectConfigsRecursive(fs: ReadOnlyFileSystem, dirUri: Uri, depth: number): Uri[] {
const configs: Uri[] = [];
try {
if (!fs.existsSync(dirUri)) {
if (!fs.existsSync(dirUri) || depth > _viperConfigScanMaxDepth) {
return configs;
}
const entries = fs.readdirEntriesSync(dirUri);
// 第一轮:只收集直接子目录中的项目配置(保持原有主配置优先级,
// 即 configs[0] 仍是工作区根下第一个含配置的直接子目录)。
const nonProjectDirs: Uri[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
if (_viperConfigSkipDirs.has(entry.name)) {
continue;
}
const childDir = dirUri.resolvePaths(entry.name);
// 优先 .vpj(Viper Project):当目录同时存在 project.vpj 与 project.json 时,优先使用 .vpj。
const vpjFile = findVpjFileInDir(fs, childDir);
@@ -98,7 +111,13 @@ export function findViperProjectConfigsInChildDirs(fs: ReadOnlyFileSystem, dirUr
const projectFileName = childDir.resolvePaths(viperProjectFileName);
if (fs.existsSync(projectFileName) && isViperProjectConfig(fs, projectFileName)) {
configs.push(fs.realCasePath(projectFileName));
continue;
}
nonProjectDirs.push(childDir);
}
// 第二轮:对非项目目录递归深入,查找嵌套项目(如 TransPyC/Test/G4)。
for (const childDir of nonProjectDirs) {
configs.push(..._findViperProjectConfigsRecursive(fs, childDir, depth + 1));
}
} catch {
// 读取目录失败:跳过
@@ -284,7 +284,7 @@ export class SourceFile {
this._uri = uri;
this._fileId = this._makeFileId(uri);
this._moduleNameGetter = moduleNameGetter;
this._isStubFile = uri.hasExtension('.vpi');
this._isStubFile = uri.hasExtension('.vpi') || uri.hasExtension('.vpnci');
this._isThirdPartyImport = isThirdPartyImport;
this._isThirdPartyPyTypedPresent = isThirdPartyPyTypedPresent;
const fileName = uri.fileName;
@@ -1128,7 +1128,7 @@ export class SourceMapper {
}
export function isStubFile(uri: Uri): boolean {
return uri.lastExtension === '.vpi';
return uri.lastExtension === '.vpi' || uri.lastExtension === '.vpnci';
}
export function isAliasFactoryCallee(callTarget: ExpressionNode, allowMemberAccess = false): boolean {
File diff suppressed because it is too large. Load diff
@@ -527,6 +527,11 @@ export enum Reachability {
export interface PrintTypeOptions {
expandTypeAlias?: boolean;
enforcePythonSyntax?: boolean;
// Viper 方言:类名保留源码中书写时的完整 module 路径(如 `ast.AST`)。
// 与 useFullyQualifiedNames 不同:后者用类型声明所在的 fullName
// (如 `ast.base.AST`、容器 `_list.list`),这里用源码里实际书写的路径,
// 未写限定路径的裸名(`list`/`str`)保持短名。
sourceQualifiedNames?: boolean;
useFullyQualifiedNames?: boolean;
useTypingUnpack?: boolean;
printUnknownWithAny?: boolean;
@@ -878,7 +883,11 @@ export interface TypeEvaluator {
printType: (type: Type, options?: PrintTypeOptions) => string;
printSrcDestTypes: (srcType: Type, destType: Type) => { sourceType: string; destType: string };
printFunctionParts: (type: FunctionType, extraFlags?: PrintTypeFlags) => [string[], string];
printFunctionParts: (
type: FunctionType,
extraFlags?: PrintTypeFlags,
options?: PrintTypeOptions
) => [string[], string];
getTypeCacheEntryCount: () => number;
disposeEvaluator: () => void;
@@ -10,6 +10,7 @@
*/
import { assert } from '../common/debug';
import { IsViperDialect } from '../common/dialect';
import {
ArgCategory,
AssignmentExpressionNode,
@@ -45,6 +46,7 @@ import {
isAnyOrUnknown,
isClass,
isClassInstance,
isCPtrType,
isFunction,
isFunctionOrOverloaded,
isInstantiableClass,
@@ -175,7 +177,11 @@ export function getTypeNarrowingCallback(
)
) {
return (type: Type) => {
return { type: narrowTypeForIsNone(evaluator, type, adjIsPositiveTest), isIncomplete: false };
const isViperDialect = IsViperDialect(getFileInfo(testExpression).executionEnvironment?.dialect);
return {
type: narrowTypeForIsNone(evaluator, type, adjIsPositiveTest, isViperDialect),
isIncomplete: false,
};
};
}
@@ -1108,7 +1114,12 @@ function getInnermostNewTypeBaseInstance(subtype: Type): Type | undefined {
}
// Handle type narrowing for expressions of the form "x is None" and "x is not None".
function narrowTypeForIsNone(evaluator: TypeEvaluator, type: Type, isPositiveTest: boolean) {
function narrowTypeForIsNone(
evaluator: TypeEvaluator,
type: Type,
isPositiveTest: boolean,
isViperDialect = false
) {
const expandedType = mapSubtypes(type, (subtype) => {
return transformPossibleRecursiveTypeAlias(subtype);
});
@@ -1172,6 +1183,15 @@ function narrowTypeForIsNone(evaluator: TypeEvaluator, type: Type, isPositiveTes
return isPositiveTest ? adjustedSubtype : undefined;
}
// Viper 扩展:None 即 C 空指针 NULL,是 CPtr 指针类型的一个取值,而非独立类型。
// `p is None` 的正测试只是空指针判断,不改变指针类型本身(与 C 语义一致)。
// 若不拦截,正分支会把 CPtr[T] 窄化为 NoneType,随后与负分支合并时
// combineTypes 会把 None 误当作结构体候选折叠成 CPtr[None],
// 导致 `if p is None: fatal_error(...)` 守卫之后 `.Pointee` 等成员访问误报。
if (isViperDialect && isClassInstance(subtype) && isCPtrType(subtype)) {
return adjustedSubtype;
}
// Is it potentially None?
if (evaluator.assignType(subtype, evaluator.getNoneType())) {
resultIncludesNoneSubtype = true;
@@ -30,6 +30,7 @@ import {
isTypeVarTuple,
isUnknown,
isUnpacked,
isViperAttrMarkerType,
maxTypeRecursionCount,
OverloadedType,
SentinelLiteral,
@@ -102,10 +103,36 @@ export const enum PrintTypeFlags {
// Viper 方言:将 NoneType 显示为 CVoid(C 语义 void)。
ViperDialect = 1 << 14,
// Viper 方言:类名保留源码中书写时的完整 module 路径(如 `ast.AST`、`memhub.MemBuddy`)。
// Viper 源码注解习惯写完整 module 路径,而类型的 fullName 是「声明所在模块」
// (如 `ast.base.AST`、容器类 `_list.list`),两者并不一致,故只能按源码书写形式展示:
// 写了限定路径的保留路径,写裸名的(`list`/`str`)仍显示短名。
ViperSourceQualifiedNames = 1 << 15,
}
export type FunctionReturnTypeCallback = (type: FunctionType) => Type;
// 计算类的显示名(不含 aliasName 与 rawptr 等特殊覆盖)。
// 该函数必须被 printObjectTypeForClassInternal 与 UniqueNameMap 同时使用,
// 否则「名字唯一性判定」与「实际打印名」不一致会导致名字被错误地降级/保留。
function getPrintableClassName(type: ClassType, printTypeFlags: PrintTypeFlags): string {
// Viper 方言(展示模式):按源码书写的完整 module 路径显示,
// 未写限定路径的裸名(`list`/`str` 等)保持短名。
if (
(printTypeFlags & PrintTypeFlags.ViperSourceQualifiedNames) !== 0 &&
(printTypeFlags & PrintTypeFlags.ViperDialect) !== 0
) {
return type.priv.displayName ?? type.shared.name;
}
if ((printTypeFlags & PrintTypeFlags.UseFullyQualifiedNames) !== 0) {
return type.shared.fullName;
}
return type.shared.name;
}
export function printType(
type: Type,
printTypeFlags: PrintTypeFlags,
@@ -323,9 +350,19 @@ function printTypeInternal(
}
}
// Viper 扩展:别名自身没有类型参数(如 `rawptr: t.CTypedef = t.CPtr`),
// 但类型被下标特化(`rawptr[T]` → 底层 `CPtr[T]`)。这种别名不能只显示
// 别名本身,否则会丢掉类型实参(`rawptr[T]` 会显示成裸 `rawptr`)。
// 跳过别名显示,交给常规路径输出 `rawptr[T]`。
const isSpecializedNonGenericViperAlias =
(printTypeFlags & PrintTypeFlags.ViperDialect) !== 0 &&
type.category === TypeCategory.Class &&
(type.priv.typeArgs?.length ?? 0) > 0 &&
(!typeParams || typeParams.length === 0);
// If it's a TypeVar, don't use the alias name. Instead, use the full
// name, which may have a scope associated with it.
if (type.category !== TypeCategory.TypeVar) {
if (!isSpecializedNonGenericViperAlias && type.category !== TypeCategory.TypeVar) {
return aliasName;
}
} finally {
@@ -536,6 +573,21 @@ function printTypeInternal(
return specialFormText;
}
// Viper 扩展:联合类型中的类型描述符按**前缀限定符**渲染,而不是
// 联合成员:`CConst | CPtr[int]` → `const rawptr[int]`、
// `CVolatile | int` → `volatile int`。见 printViperModifierPrefix。
const modifierPrefixText = printViperModifierPrefix(
type,
printTypeFlags,
returnTypeCallback,
uniqueNameMap,
recursionTypes,
recursionCount
);
if (modifierPrefixText !== undefined) {
return modifierPrefixText;
}
// If we're using "|" notation, enclose callable subtypes in parens.
const updatedPrintTypeFlags =
printTypeFlags & PrintTypeFlags.PEP604
@@ -670,6 +722,120 @@ function printTypeInternal(
}
}
// Viper 扩展:联合类型中修饰符(类型描述符)对应的展示关键字。
// Viper 用联合(TypeSet)表示「类型 + 描述符」,如 `CConst | CPtr[int]`(const int*)、
// `CVolatile | int`(volatile int)。展示时须把它们当作**前缀限定符**,
// 而不是联合成员,否则会暴露出内部表示("CConst | rawptr[int]")。
// `CExport`(`export x: T`)/ `State`(`extern x: T` = CExtern | CExport)是本
// 语言软关键字 `export` / `extern` 声明变量的脱糖产物(与 TPC
// `_parse_export_extern_var` 同形),同属存储类修饰,故与 static/register 等一样
// 按前缀渲染(`export int` / `extern int`)。
// 其余声明标记 / 编译期元指令(CPass / CDefine)保持原样。
function getViperModifierKeyword(type: Type): string | undefined {
if (!isClass(type)) {
return undefined;
}
switch (type.shared.name) {
case 'CConst':
return 'const';
case 'CVolatile':
return 'volatile';
case 'CStatic':
return 'static';
case 'CRegister':
return 'register';
case 'CAuto':
return 'auto';
case 'CInline':
return 'inline';
case 'CExtern':
return 'extern';
case 'CExport':
return 'export';
case 'State':
return 'extern';
default:
return undefined;
}
}
// Viper 扩展:把联合类型渲染为「修饰符前缀 + 其余类型」。
// `CConst | CPtr[int]` → `const rawptr[int]`
// `CVolatile | int` → `volatile int`
// `CConst | CVolatile | int` → `const volatile int`(修饰符全部提到最前)
// `CConst | int | str` → `const (int | str)`(其余多于一个时加括号,
// 避免前缀限定范围歧义)
// 无修饰符(或全是修饰符)时返回 undefined,交回常规联合渲染。
function printViperModifierPrefix(
type: UnionType,
printTypeFlags: PrintTypeFlags,
returnTypeCallback: FunctionReturnTypeCallback,
uniqueNameMap: UniqueNameMap,
recursionTypes: Type[],
recursionCount: number
): string | undefined {
const modifierKeywords: string[] = [];
const remainingSubtypes: Type[] = [];
let sawAttrMarker = false;
for (const subtype of type.priv.subtypes) {
// Viper 扩展:`__attribute__` 属性标记(t.CAttr)在源码中没有对应关键字,
// 展示时直接丢弃:`CInt | t.attr.weak` → `int`。
if (isViperAttrMarkerType(subtype)) {
sawAttrMarker = true;
continue;
}
const keyword = getViperModifierKeyword(subtype);
if (keyword !== undefined) {
modifierKeywords.push(keyword);
} else {
remainingSubtypes.push(subtype);
}
}
if ((modifierKeywords.length === 0 && !sawAttrMarker) || remainingSubtypes.length === 0) {
return undefined;
}
if (recursionCount >= maxTypeRecursionCount) {
return undefined;
}
recursionTypes.push(type);
try {
let remainingText: string;
if (remainingSubtypes.length === 1) {
remainingText = printTypeInternal(
remainingSubtypes[0],
printTypeFlags,
returnTypeCallback,
uniqueNameMap,
recursionTypes,
recursionCount + 1
);
} else {
const parts = remainingSubtypes.map((subtype) =>
printTypeInternal(
subtype,
printTypeFlags,
returnTypeCallback,
uniqueNameMap,
recursionTypes,
recursionCount + 1
)
);
remainingText = `(${parts.join(' | ')})`;
}
const modifierText = modifierKeywords.join(' ');
return modifierText ? `${modifierText} ${remainingText}` : remainingText;
} finally {
recursionTypes.pop();
}
}
function printUnionType(
type: UnionType,
printTypeFlags: PrintTypeFlags,
@@ -1000,8 +1166,7 @@ function printObjectTypeForClassInternal(
): string {
let objName = type.priv.aliasName;
if (!objName) {
objName =
(printTypeFlags & PrintTypeFlags.UseFullyQualifiedNames) !== 0 ? type.shared.fullName : type.shared.name;
objName = getPrintableClassName(type, printTypeFlags);
}
// Special-case NoneType to convert it to None.
@@ -1009,6 +1174,17 @@ function printObjectTypeForClassInternal(
objName = 'None';
}
// Viper 方言:t.CPtr 等价编译器内置裸指针 rawptr,统一显示为 rawptr
// (如 `elts: CPtr[list[CPtr[AST]]]` → `rawptr[list[rawptr[AST]]]`)。
// 类型实参照常展开,仅替换指针类型的显示名。
if (
(printTypeFlags & PrintTypeFlags.ViperDialect) !== 0 &&
type.shared.name === 'CPtr' &&
(type.shared.fullName === 't.CPtr' || type.shared.moduleName === 't')
) {
objName = 'rawptr';
}
// Use the fully-qualified name if the name isn't unique.
if (!uniqueNameMap.isUnique(objName)) {
objName = type.shared.fullName;
@@ -1502,10 +1678,7 @@ class UniqueNameMap {
let className = type.priv.aliasName;
if (!className) {
className =
(this._printTypeFlags & PrintTypeFlags.UseFullyQualifiedNames) !== 0
? type.shared.fullName
: type.shared.name;
className = getPrintableClassName(type, this._printTypeFlags);
}
this._addIfUnique(className, type);
+221 -17
View File
@@ -670,6 +670,11 @@ export const enum ClassTypeFlags {
// This class is rejected when used as the second argument to
// an isinstance or issubclass call.
IllegalIsinstanceClass = 1 << 24,
// Viper 扩展:由 `enum X:` 语法声明的枚举类(等价 `class X(t.CEnum):`)。
// 与 EnumClass 同时设置。用于把 Viper 枚举视作 CEnum 子类
// (如 enum → int 的隐式转换)。
ViperEnumClass = 1 << 25,
}
export interface DataClassBehaviors {
@@ -810,6 +815,11 @@ export interface ClassDetailsPriv {
// name.
aliasName?: string | undefined;
// Viper 扩展:该类型在源码中被引用时书写的完整路径(如 `ast.AST`、`memhub.MemBuddy`)。
// Viper 源码注解习惯写完整 module 路径,类型展示需要保留它而不是退化为短名。
// 该字段**仅用于展示**(typePrinter 读取),不参与任何类型判定/兼容性比较。
displayName?: string | undefined;
// Used for "narrowing" of typed dicts where some entries
// that are not required have been confirmed to be present
// through the use of a guard expression.
@@ -985,6 +995,17 @@ export namespace ClassType {
return newClassType;
}
// Viper 扩展:把「源码中书写类型时使用的完整路径」记到类型上,仅供展示用。
// 已经记录过同一路径时直接返回原类型,避免无意义的克隆。
export function cloneForDisplayName(classType: ClassType, displayName: string): ClassType {
if (classType.priv.displayName === displayName) {
return classType;
}
const newClassType = TypeBase.cloneType(classType);
newClassType.priv.displayName = displayName;
return newClassType;
}
export function cloneForNarrowedTypedDictEntries(
classType: ClassType,
narrowedEntries?: Map<string, TypedDictEntry>
@@ -1239,6 +1260,11 @@ export namespace ClassType {
return !!(classType.shared.flags & ClassTypeFlags.EnumClass);
}
// Viper 扩展:是否为 `enum X:` 语法声明的枚举类。
export function isViperEnumClass(classType: ClassType) {
return !!(classType.shared.flags & ClassTypeFlags.ViperEnumClass);
}
export function isPropertyClass(classType: ClassType) {
return !!(classType.shared.flags & ClassTypeFlags.PropertyClass);
}
@@ -3273,13 +3299,35 @@ export function isCStaticType(type: Type): boolean {
return type.shared.name === 'CStatic' || type.shared.fullName === 't.CStatic';
}
// Viper 扩展:检测类型是否为 t.CVolatile(volatile 类型限定符)。
// CVolatile 在联合类型中用于表示 volatile 修饰(如 CVolatile | CInt 表示 volatile int)。
// 与 isCConstType 对称:语义上不影响数据类型,仅在 TypeSet 联合中充当描述符。
export function isCVolatileType(type: Type): boolean {
if (!isClass(type)) {
return false;
}
return type.shared.name === 'CVolatile' || type.shared.fullName === 't.CVolatile';
}
// Viper 扩展:检测类型是否为 `t.CAttr`(`__attribute__` 属性标记)。
// 注解里的属性标记(`T | t.attr.section(".x")`)与 `__attribute__(...) T` 修饰器
// 都求值为 CAttr 实例:与 CConst / CVolatile 同族的修饰符——只携带编译期元数据,
// 不参与类型检查(运算/下标/赋值按被修饰的基础类型处理),展示时也不作为联合成员。
export function isViperAttrMarkerType(type: Type): boolean {
if (!isClass(type)) {
return false;
}
return type.shared.name === 'CAttr' || type.shared.fullName === 't.CAttr';
}
// Viper 扩展:检测类型是否为 C 修饰符/描述符(编译时元指令,非数据类型)。
// 这些类型在联合类型中应被过滤,只保留实际数据类型进行类型检查。
// 依据 t.py 中 position 属性:含 PREFIX 的为修饰符(存储类/类型限定符/前缀)。
// 特例:CDefine/CPass 虽 position=BASE,但语义是编译时元指令,也归为修饰符。
// 注意:CUnsigned/CUnsignedChar 等是数据类型(position=BASE),不是修饰符。
// 修饰符列表:CInline, CStatic, CConst, CExtern, CVolatile, CRegister,
// CExport, CAuto, CPass, CDefine, State(State 等价 CExport | CExtern)
// CExport, CAuto, CPass, CDefine, State(State 等价 CExport | CExtern),
// CAttr(`__attribute__` 属性标记,见 isViperAttrMarkerType)
export function isCModifierType(type: Type): boolean {
if (!isClass(type)) {
return false;
@@ -3296,7 +3344,8 @@ export function isCModifierType(type: Type): boolean {
name === 'CAuto' ||
name === 'CPass' ||
name === 'CDefine' ||
name === 'State'
name === 'State' ||
name === 'CAttr'
);
}
@@ -3466,11 +3515,12 @@ const _viperCTypeSizeMap = new Map<string, number>([
// Viper 扩展:判断 CType 子类之间的隐式转换是否合法。
// 规则:
// 1. 整数 → 整数:srcSize <= destSize 允许(低向高或同级),否则不允许
// 2. 浮点 → 浮点:srcSize <= destSize 允许
// 1. 整数 → 整数:允许所有方向(C 语义,高向低可安全降级)
// 2. 浮点 → 浮点:允许所有方向
// 3. 整数 → 浮点:destSize >= srcSize 允许(无损)
// 4. 浮点 → 整数:不允许(有损)
// 5. 同级指针(都是 CPtr 子类):允许不同类型转换
// 5. 指针 → 指针(都是 CPtr 子类):级数必须相同,且 **pointee 必须兼容**
// (见 isViperCPtrPointeeAssignable)——不再无条件允许
// 6. 不同级(一个是指针,另一个不是):不允许
// 7. 未知位宽:保守允许(避免误报)
export function canImplicitlyConvertCType(srcType: Type, destType: Type): boolean {
@@ -3481,18 +3531,30 @@ export function canImplicitlyConvertCType(srcType: Type, destType: Type): boolea
const srcName = srcType.shared.name;
const destName = destType.shared.name;
// Viper 扩展:指针 → 指针(都是 CPtr 子类)的 pointee 兼容性检查。
// 必须放在下面的「同名即允许」快捷判断**之前**:CPtr↔CPtr 的 shared.name
// 都是 'CPtr',否则会因同名而无条件通过(pointee 类型完全不看)。
if (isCPtrType(srcType) && isCPtrType(destType)) {
return isViperCPtrPointeeAssignable(destType, srcType);
}
// 同类型,允许
if (srcName === destName) {
return true;
}
// 指针规则:同级指针(都是 CPtr 子类)允许,不同级不允许
const srcIsPtr = isCPtrType(srcType);
const destIsPtr = isCPtrType(destType);
if (srcIsPtr && destIsPtr) {
// Viper 扩展:单级指针 ↔ void*(CVoid)语义(C 惯例)。
// 任意对象指针可隐式转为 void*,void* 也可隐式转回任意对象指针。
// 多级指针(如 Node** → void*)仍禁止,与跨级指针守卫口径一致。
if (isCPtrType(srcType) && getCPtrLevel(srcType) === 1 && isCVoidType(destType)) {
return true;
}
if (srcIsPtr !== destIsPtr) {
if (isCVoidType(srcType) && isCPtrType(destType) && getCPtrLevel(destType) === 1) {
return true;
}
// 指针规则(剩余情况只剩「一侧指针、一侧非指针」):不允许
if (isCPtrType(srcType) !== isCPtrType(destType)) {
return false;
}
@@ -3535,6 +3597,143 @@ export function canImplicitlyConvertCType(srcType: Type, destType: Type): boolea
return true;
}
// Viper 扩展:pointee 是否为「数值标量」——C 整数 / C 浮点 / 内置 int|float|
// double|char|bool,即用户口径的「基本类型」。
// 非基本类型(结构体、类等)的指针**禁止**隐式互转(见 isViperCPtrPointeeAssignable)。
function isViperNumericScalarPointee(type: Type): boolean {
if (!isClassInstance(type)) {
return false;
}
if (ClassType.isBuiltIn(type, ['int', 'float', 'double', 'char', 'bool'])) {
return true;
}
const name = type.shared.name;
return _viperCIntegerTypeNames.has(name) || _viperCFloatTypeNames.has(name);
}
// Viper 扩展:把 pointee 规范化为可比较的「核心类型」:
// 1. 去掉联合里的 C 修饰符(CConst / CVolatile / CStatic / ...):
// 如 `volatile int`(= CVolatile | int)→ int;
// 2. 统一为**实例形态**:`type[ParamNode]`(类对象)与 ParamNode(实例)
// 是同一个类,须可比。
function normalizeViperPointeeType(type: Type): Type {
let core: Type = type;
if (isUnion(core)) {
const nonModifiers = core.priv.subtypes.filter((subtype) => !(isClass(subtype) && isCModifierType(subtype)));
if (nonModifiers.length === 1) {
core = nonModifiers[0];
}
}
if (isInstantiableClass(core)) {
core = ClassType.cloneAsInstance(core);
}
return core;
}
// Viper 扩展:两个 pointee 是否属于同一个 REnum(t.REnum tagged-union)家族——
// 其一是继承自 t.REnum 的**根类**,另一个是声明时**嵌套**在根类 suite 里的
// **变体类**(如 class LLVMType 内的 class Ptr / Int / Struct)。
//
// 变体类不是普通子类(不继承根类),故 isDerivedFrom 判不出来;但 TransPyV 对
// 「变体指针 ↔ 根指针」只 emit bitcast(tagged-union 的负载地址与联合地址),
// 两侧可自由互转。此外 match 的变体模式收窄会把 CPtr[LLVMType] 收窄成
// CPtr[LLVMType.Ptr](见 patternMatching.narrowTypeBasedOnClassPattern),
// 若不认这一家族关系,收窄后的指针传回 rawptr[LLVMType] 形参会误报。
//
// 判定用 fullName 前缀('LLVMType.Ptr' ⊂ 'LLVMType'),与 patternMatching 的
// 收窄判定口径保持一致;同级的不同变体(Ptr vs Int)互不为前缀,仍被拒绝。
function isViperREnumFamilyPointee(firstPointee: Type, secondPointee: Type): boolean {
if (!isClass(firstPointee) || !isClass(secondPointee)) {
return false;
}
const firstFullName = firstPointee.shared.fullName;
const secondFullName = secondPointee.shared.fullName;
if (!firstFullName || !secondFullName) {
return false;
}
if (firstFullName.startsWith(secondFullName + '.')) {
return isViperREnumRootClass(secondPointee);
}
if (secondFullName.startsWith(firstFullName + '.')) {
return isViperREnumRootClass(firstPointee);
}
return false;
}
// Viper 扩展:类是否继承自 t.REnum(REnum tagged-union 根类)。
function isViperREnumRootClass(classType: ClassType): boolean {
return classType.shared.mro.some(
(baseClass) => isClass(baseClass) && !!baseClass.shared.fullName?.endsWith('.REnum')
);
}
// Viper 扩展:两个 CPtr 之间是否允许隐式转换(指针 → 指针)。
// 规则(2026-09-28 收紧,原先「同级即无条件允许」):
// 1. 级数必须相同(getCPtrLevel)——不同级一律拒绝;
// 2. 任一侧 pointee 是 void*(CVoid)或未定(rawptr = CPtr[未特化],其实参是
// 类自身的 TypeVar)→ 允许(void* 通用指针语义,malloc 等场景依赖它);
// 3. pointee 规范化后完全相同 → 允许;
// 3.5. 同一 REnum 家族(根 ↔ 变体,见 isViperREnumFamilyPointee)→ 允许;
// 4. 两侧 pointee 都是数值标量(基本类型)→ 按 T 的隐式转换规则
// (整数/浮点全方向、整数→浮点需位宽≥、浮点→整数禁止);
// 5. 结构体(非基本类型):仅允许**同一继承体系内**的转换 —— 上转型
// (MemBuddy* → MemManager*,真实代码 memhub/requests 依赖)与下转型
// (AST* → Call*,CallGenDispatch.vp:824 依赖)都放行;
// 不相关的两个结构体(Node* → Other*)、结构体 ↔ 基本类型一律拒绝。
function isViperCPtrPointeeAssignable(destPtr: ClassType, srcPtr: ClassType): boolean {
if (getCPtrLevel(destPtr) !== getCPtrLevel(srcPtr)) {
return false;
}
const rawDestPointee =
destPtr.priv.typeArgs && destPtr.priv.typeArgs.length > 0 ? destPtr.priv.typeArgs[0] : undefined;
const rawSrcPointee = srcPtr.priv.typeArgs && srcPtr.priv.typeArgs.length > 0 ? srcPtr.priv.typeArgs[0] : undefined;
// 未定 pointee(裸 rawptr / CPtr 无实参)→ 通用指针,允许。
// 注意:裸 `rawptr`(= t.CPtr 未特化)的实参是**类自身的 TypeVar**,不是
// UnknownType(getCPtrLevel 视其为 1 级),故这里必须一并认 TypeVar。
if (!rawDestPointee || !rawSrcPointee || isTypeVar(rawDestPointee) || isTypeVar(rawSrcPointee)) {
return true;
}
const destPointee = normalizeViperPointeeType(rawDestPointee);
const srcPointee = normalizeViperPointeeType(rawSrcPointee);
// void* 通用指针(任一侧)→ 允许。
if (isCVoidType(destPointee) || isCVoidType(srcPointee)) {
return true;
}
if (isUnknown(destPointee) || isUnknown(srcPointee)) {
return true;
}
// pointee 完全相同 → 允许。
if (isTypeSame(destPointee, srcPointee)) {
return true;
}
// REnum(t.REnum tagged-union)家族:变体指针 ↔ 根指针 双向允许。
if (isViperREnumFamilyPointee(destPointee, srcPointee)) {
return true;
}
// 仅「基本类型(数值标量)」之间按 T 的隐式转换规则放行。
if (isViperNumericScalarPointee(destPointee) && isViperNumericScalarPointee(srcPointee)) {
return canImplicitlyConvertCType(srcPointee, destPointee);
}
// 结构体等非基本类型:仅允许**同一继承体系内**的转换(上转型 + 下转型)。
// 注意:isDerivedFrom 的比较必须在**可实例化形态**下进行(类的 baseClasses
// 存的是 instantiable 基类,与实例形态混比会因 instance/instantiable 差异误判)。
if (isClassInstance(destPointee) && isClassInstance(srcPointee)) {
const srcForCompare = ClassType.cloneAsInstantiable(srcPointee);
const destForCompare = ClassType.cloneAsInstantiable(destPointee);
const inheritanceChain: InheritanceChain = [];
return (
ClassType.isDerivedFrom(srcForCompare, destForCompare, inheritanceChain) ||
ClassType.isDerivedFrom(destForCompare, srcForCompare, inheritanceChain)
);
}
// 其余(如联合 pointee)→ 禁止隐式转换。
return false;
}
export function isAny(type: Type): type is AnyType {
return type.category === TypeCategory.Any;
}
@@ -4153,7 +4352,12 @@ export function combineTypes(subtypes: Type[], options?: CombineTypesOptions): T
// Viper 扩展:将 Struct | CPtr 自动映射为 CPtr[Struct]。
// 模拟 C 语言指针语义:结构体与指针的联合类型自动转换为指向该结构体的指针类型。
// 例如:Scheduler | t.CPtr → CPtr[Scheduler],bootinfo.bootinfo | t.CPtr → CPtr[bootinfo.bootinfo]。
// 识别条件:联合类型中同时存在 CPtr 子类型和"结构体"子类型(非 CPtr/CVoid/CArray/CConst/CDefine/整数/str/bytes)。
// 识别条件:联合类型中同时存在 CPtr 子类型和"结构体"子类型(非 CPtr/CVoid/CArray/CConst/CDefine)。
// int 允许作为结构体候选:`int | t.CPtr` → CPtr[int](C 的 int*),与 `t.CInt | t.CPtr` 一致。
// str/bytes 也允许作为 CPtr 的被指向类型:Viper 中 str/bytes 本身就是 C 的 char*(i8*),
// 因此 `str | t.CPtr` 折叠为 CPtr[str](i8**,即 char**,如 argv),
// `bytes | t.CPtr` 折叠为 CPtr[bytes](同一 i8** 语义)。这与 TransPyC 的契约一致。
// 注:无 CPtr 子类型时(如 `CConst | str` = const char*)不会折叠,保持联合类型语义。
if (subtypes.length > 1) {
let cptrSubtype: ClassType | undefined;
const structSubtypes: ClassType[] = [];
@@ -4167,14 +4371,14 @@ export function combineTypes(subtypes: Type[], options?: CombineTypesOptions): T
}
} else if (
isClass(subtype) &&
// None 是空指针取值而非结构体,不能作为 CPtr 的类型参数(否则 None | CPtr[T] 会误折叠成 CPtr[None])
!ClassType.isBuiltIn(subtype, 'NoneType') &&
// 修饰符(CConst/CStatic/CVolatile/CExport/State/CExtern/CInline/CDefine/CAttr 等)
// 只作修饰,不能当被指向类型:`t.CPtr | t.State`(extern 变量的存储类标记)保持联合,
// 否则会折叠成 CPtr[State] 这种把修饰符当结构体的错误类型。
!isCModifierType(subtype) &&
!isCVoidType(subtype) &&
!isCArrayType(subtype) &&
!isCConstType(subtype) &&
!isCStaticType(subtype) &&
!isCDefineType(subtype) &&
!ClassType.isBuiltIn(subtype, 'int') &&
!ClassType.isBuiltIn(subtype, 'str') &&
!ClassType.isBuiltIn(subtype, 'bytes') &&
!ClassType.isBuiltIn(subtype, 'object')
) {
structSubtypes.push(subtype);
@@ -118,7 +118,7 @@ class DefaultTypeshedInfoProvider implements TypeshedInfoProvider {
typeshedThirdPartyPackagePaths.set(innerEntry.name, [innerDirPath]);
}
} else if (innerEntry.isFile()) {
if (innerEntry.name.endsWith('.vpi')) {
if (innerEntry.name.endsWith('.vpi') || innerEntry.name.endsWith('.vpnci')) {
const strippedFileName = stripFileExtension(innerEntry.name);
const pathList = typeshedThirdPartyPackagePaths.get(strippedFileName);
if (pathList) {
@@ -11,6 +11,8 @@ import { MessageChannel, MessagePort, Worker, parentPort, threadId, workerData }
import {
AnalysisCompleteCallback,
AnalysisProgress,
AnalysisProgressCallback,
AnalysisResults,
RequiringAnalysisCount,
analyzeProgram,
@@ -51,6 +53,7 @@ import { ProgramView } from './common/extensibility';
export interface IBackgroundAnalysis extends Disposable {
setProgramView(program: Program): void;
setCompletionCallback(callback?: AnalysisCompleteCallback): void;
setAnalysisProgressCallback(callback?: AnalysisProgressCallback): void;
setImportResolver(importResolver: ImportResolver): void;
setConfigOptions(configOptions: ConfigOptions): void;
setTrackedFiles(fileUris: Uri[]): void;
@@ -84,6 +87,9 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis {
private _worker: Worker | undefined;
private _onAnalysisCompletion: AnalysisCompleteCallback = nullCallback;
private _onAnalysisProgress: AnalysisProgressCallback = () => {
/* empty */
};
private _messageChannel: MessageChannel;
protected program: ProgramView | undefined;
@@ -115,6 +121,14 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis {
this._onAnalysisCompletion = callback ?? nullCallback;
}
setAnalysisProgressCallback(callback?: AnalysisProgressCallback) {
this._onAnalysisProgress =
callback ??
(() => {
/* empty */
});
}
setImportResolver(importResolver: ImportResolver) {
this.enqueueRequest({ requestType: 'setImportResolver', data: serialize(importResolver.host.kind) });
}
@@ -322,6 +336,11 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis {
break;
}
case 'analysisProgress': {
this._onAnalysisProgress(deserialize<AnalysisProgress>(msg.data));
break;
}
default:
debug.fail(`${msg.requestType} is not expected. Message structure: ${JSON.stringify(msg)}`);
}
@@ -344,6 +363,11 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis {
break;
}
case 'analysisProgress': {
this._onAnalysisProgress(deserialize<AnalysisProgress>(msg.data));
break;
}
case 'analysisPaused': {
// Analysis request has completed, but there is more to
// analyze, so queue another message to resume later.
@@ -607,13 +631,25 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase
protected handleResumeAnalysis(port: MessagePort, token: CancellationToken) {
// Report results at the interval of the max analysis time.
const maxTime = { openFilesTimeInMs: 50, noOpenFilesTimeInMs: 200 };
// Viper 扩展:把逐文件的分析进度回传前台(节流到 50ms 一次,避免
// 大量文件时 IPC 消息过密)。收尾那次(done)一定要发。
let lastProgressSent = 0;
const moreToAnalyze = analyzeProgram(
this.program,
maxTime,
this._configOptions,
(result) => this.onAnalysisCompletion(port, result),
this.getConsole(),
token
token,
(progress) => {
const now = Date.now();
if (!progress.done && now - lastProgressSent < 50) {
return;
}
lastProgressSent = now;
this.onAnalysisProgress(port, progress);
}
);
if (moreToAnalyze) {
@@ -770,6 +806,14 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase
port.postMessage({ requestType: 'analysisResult', data: serialize(result) });
}
/**
* Viper 扩展:把「正在分析哪个文件 / 已分析多少」回传前台线程,
* 由 languageServerBase 转成 `viper/analysisStatus` 通知给客户端状态栏。
*/
protected onAnalysisProgress(port: MessagePort, progress: AnalysisProgress) {
port.postMessage({ requestType: 'analysisProgress', data: serialize(progress) });
}
private _onMessageWrapper(msg: BackgroundRequest) {
try {
return this.onMessage(msg);
@@ -881,7 +925,7 @@ export interface BackgroundRequest {
sharedUsageBuffer?: SharedArrayBuffer;
}
export type BackgroundResponseKind = 'log' | 'analysisResult' | 'analysisPaused' | 'analysisDone';
export type BackgroundResponseKind = 'log' | 'analysisResult' | 'analysisProgress' | 'analysisPaused' | 'analysisDone';
export interface BackgroundResponse {
requestType: BackgroundResponseKind;
@@ -12,6 +12,7 @@ import { LanguageServerInterface } from '../common/languageServerInterface';
import { Commands } from './commands';
import { CreateTypeStubCommand } from './createTypeStub';
import { DumpFileDebugInfoCommand } from './dumpFileDebugInfoCommand';
import { FlowChartCommand } from './flowChartCommand';
import { QuickActionCommand } from './quickActionCommand';
import { RestartServerCommand } from './restartServer';
@@ -24,12 +25,14 @@ export class CommandController implements ServerCommand {
private _restartServer: RestartServerCommand;
private _quickAction: QuickActionCommand;
private _dumpFileDebugInfo: DumpFileDebugInfoCommand;
private _flowChart: FlowChartCommand;
constructor(ls: LanguageServerInterface) {
this._createStub = new CreateTypeStubCommand(ls);
this._restartServer = new RestartServerCommand(ls);
this._quickAction = new QuickActionCommand(ls);
this._dumpFileDebugInfo = new DumpFileDebugInfoCommand(ls);
this._flowChart = new FlowChartCommand(ls);
}
async execute(cmdParams: ExecuteCommandParams, token: CancellationToken): Promise<any> {
@@ -50,6 +53,10 @@ export class CommandController implements ServerCommand {
return this._dumpFileDebugInfo.execute(cmdParams, token);
}
case Commands.showFunctionFlowChart: {
return this._flowChart.execute(cmdParams, token);
}
default: {
return new ResponseError<string>(1, 'Unsupported command');
}
@@ -10,6 +10,7 @@
export const enum Commands {
createTypeStub = 'viper.createtypestub',
restartServer = 'viper.restartserver',
cleanupAndDeactivate = 'viper.cleanupAndDeactivate',
orderImports = 'viper.organizeimports',
unusedImport = 'viper.unusedImport',
dumpFileDebugInfo = 'viper.dumpFileDebugInfo',
@@ -18,4 +19,5 @@ export const enum Commands {
dumpTypes = 'viper.dumpTypes',
dumpCachedTypes = 'viper.dumpCachedTypes',
dumpCodeFlowGraph = 'viper.dumpCodeFlowGraph',
showFunctionFlowChart = 'viper.showFunctionFlowChart',
}
@@ -0,0 +1,100 @@
/*
* flowChartCommand.ts
*
* Viper 扩展:为「函数跳转图」提供 LSP 服务端命令。
*
* 客户端(vscode-vipeye)把「文件 uri + 光标 offset」发过来,本命令:
* 1) 按 offset 找到最内层节点;
* 2) 从该节点向上找到最近的函数(方法优先于其所在类,故光标在方法内即取方法);
* 3) 用 AST 构建控制流图(见 analyzer/flowChartBuilder.ts),返回 Mermaid 文本与节点行号映射。
*
* 光标不在任何函数内时返回 undefined,由客户端提示用户。
*/
import { CancellationToken, ExecuteCommandParams } from 'vscode-languageserver';
import { buildFlowChartForFunction, FlowChartNodeLines } from '../analyzer/flowChartBuilder';
import { findNodeByOffset } from '../analyzer/parseTreeUtils';
import { throwIfCancellationRequested } from '../common/cancellationUtils';
import { LanguageServerInterface } from '../common/languageServerInterface';
import { convertOffsetToPosition } from '../common/positionUtils';
import { Uri } from '../common/uri/uri';
import { FunctionNode, ParseNode, ParseNodeType } from '../parser/parseNodes';
import { ServerCommand } from './commandController';
/** 服务端返回给客户端的流程图数据(跨进程传输)。 */
export interface FlowChartResponse {
/** 函数名(面板标题用)。 */
functionName: string;
/** 完整 Mermaid 文本。 */
mermaidText: string;
/** 图节点 id → 源码行号(1 起),供点击节点跳转。 */
nodeLines: FlowChartNodeLines;
/** 函数在文档中的起始 / 结束行号(1 起)。 */
startLine: number;
endLine: number;
}
export class FlowChartCommand implements ServerCommand {
constructor(private _ls: LanguageServerInterface) {}
async execute(params: ExecuteCommandParams, token: CancellationToken): Promise<FlowChartResponse | undefined> {
throwIfCancellationRequested(token);
if (!params.arguments || params.arguments.length < 2) {
return undefined;
}
const fileUri = Uri.parse(params.arguments[0] as string, this._ls.serviceProvider);
const offset = params.arguments[1] as number;
const workspace = await this._ls.getWorkspaceForFile(fileUri);
return workspace.service.run((_program) => {
const parseResults = workspace.service.getParseResults(workspace.service.fs.realCasePath(fileUri));
if (!parseResults || offset === undefined) {
return undefined;
}
const node = findNodeByOffset(parseResults.parserOutput.parseTree, offset);
if (!node) {
return undefined;
}
const functionNode = findInnermostFunction(node);
if (!functionNode) {
return undefined;
}
const chart = buildFlowChartForFunction(functionNode, parseResults.text, parseResults.parserOutput.lines);
const lines = parseResults.parserOutput.lines;
return {
functionName: chart.functionName,
mermaidText: chart.mermaidText,
nodeLines: chart.nodeLines,
startLine: convertOffsetToPosition(functionNode.start, lines).line + 1,
endLine: convertOffsetToPosition(functionNode.start + functionNode.length - 1, lines).line + 1,
};
}, token);
}
}
/**
* 从指定节点向上找最近的函数定义。
* 注意不能用 parseTreeUtils 的 getEnclosingFunction:它从 parent 起找,节点本身是函数时取不到;
* 且它在类体内直接返回 undefined,而 Viper 的方法都写在类体里,那会导致方法全都取不到。
* 本函数遇到 Function 即返回,因此光标在方法内时取到的是方法而不是类。
*/
function findInnermostFunction(node: ParseNode): FunctionNode | undefined {
let curNode: ParseNode | undefined = node;
while (curNode) {
if (curNode.nodeType === ParseNodeType.Function) {
return curNode;
}
curNode = curNode.parent;
}
return undefined;
}
@@ -1229,6 +1229,8 @@ function getKeywordTypeString(type: KeywordType) {
return 'Debug';
case KeywordType.Def:
return 'Def';
case KeywordType.Define:
return 'Define';
case KeywordType.Del:
return 'Del';
case KeywordType.Elif:
@@ -7,6 +7,7 @@
* Pathname utility functions.
*/
import * as os from 'os';
import * as path from 'path';
import { Char } from './charCodes';
@@ -606,7 +607,7 @@ export function getWildcardRoot(rootPath: string, fileSpec: string): string {
}
export function hasPythonExtension(path: string) {
return path.endsWith('.vp') || path.endsWith('.vpi');
return path.endsWith('.vp') || path.endsWith('.vpi') || path.endsWith('.vpnci');
}
export function getRegexEscapedSeparator(pathSep: string = path.sep) {
@@ -630,6 +631,24 @@ export function isDiskPathRoot(path: string) {
return rootLength > 0 && rootLength === path.length;
}
/**
* Viper 全局目录:`~/.vipeye/`。
*
* 该目录跨会话、跨实例共享(同一台机器上多个编辑器窗口共用一份)。
* 语言服务端写入、扩展客户端清理,两侧必须共用这里的定义,避免各写一份路径。
*/
export function getViperGlobalDir(): string {
return path.join(os.homedir(), '.vipeye');
}
/**
* 语义高亮磁盘缓存目录:`~/.vipeye/cache/semantic-tokens/`。
* 服务端在启动时创建并读写,客户端在执行「清理并临时关停(含清缓存)」时整体删除。
*/
export function getSemanticTokenCacheDir(): string {
return path.join(getViperGlobalDir(), 'cache', 'semantic-tokens');
}
function getAnyExtensionFromPathWorker(
path: string,
extensions: string | readonly string[],
@@ -334,7 +334,7 @@ export function getWildcardRoot(root: Uri, fileSpec: string): Uri {
}
export function hasPythonExtension(uri: Uri) {
return uri.hasExtension('.vp') || uri.hasExtension('.vpi');
return uri.hasExtension('.vp') || uri.hasExtension('.vpi') || uri.hasExtension('.vpnci');
}
export function getFileSpec(root: Uri, fileSpec: string): FileSpec {
@@ -12,8 +12,6 @@
import './common/extensions';
// Node.js 内置模块(用于语义高亮磁盘缓存目录确定)
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import {
@@ -86,7 +84,7 @@ import {
} from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { AnalysisResults } from './analyzer/analysis';
import { AnalysisProgress, AnalysisResults } from './analyzer/analysis';
import { BackgroundAnalysisProgram, InvalidatedReason } from './analyzer/backgroundAnalysisProgram';
import { ImportResolver } from './analyzer/importResolver';
import { MaxAnalysisTime } from './analyzer/program';
@@ -115,6 +113,7 @@ import {
WorkspaceServices,
} from './common/languageServerInterface';
import { fromLSPAny, isNullProgressReporter } from './common/lspUtils';
import { getSemanticTokenCacheDir } from './common/pathUtils';
import { ProgressReportTracker, ProgressReporter } from './common/progressReporter';
import { ServiceKeys } from './common/serviceKeys';
import { ServiceProvider } from './common/serviceProvider';
@@ -317,7 +316,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis
*/
private _getSemanticTokenCacheDir(): string | undefined {
try {
const cacheDir = path.join(os.homedir(), '.vipeye', 'cache', 'semantic-tokens');
const cacheDir = getSemanticTokenCacheDir();
fs.mkdirSync(cacheDir, { recursive: true });
return cacheDir;
} catch {
@@ -370,9 +369,19 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis
});
service.setCompletionCallback((results) => this.onAnalysisCompletedHandler(service.fs, results));
service.setAnalysisProgressCallback((progress) => this.sendAnalysisStatus(progress));
return service;
}
/**
* Viper 扩展:把「正在分析哪个文件 / 已分析多少 / 共多少」通知给客户端,
* 供其在左下角状态栏显示 `C:\xxx\1.vp (4/90) 0.04% loading...`,
* 让用户能判断语言服务是在工作还是卡死。
*/
protected sendAnalysisStatus(progress: AnalysisProgress) {
this.connection.sendNotification('viper/analysisStatus', progress);
}
async getWorkspaces(): Promise<Workspace[]> {
const workspaces = this.workspaceFactory.items();
for (const workspace of workspaces) {
@@ -479,8 +479,9 @@ export class AutoImporter {
const fileDir = uri.getDirectory();
const initPathPy = fileDir.initPyUri;
const initPathPyi = fileDir.initPyiUri;
const isStub = uri.hasExtension('.vpi');
const hasInit = map.has(initPathPy.key) || map.has(initPathPyi.key);
const initPathVpnci = fileDir.combinePathsUnsafe('__init__.vpnci');
const isStub = uri.hasExtension('.vpi') || uri.hasExtension('.vpnci');
const hasInit = map.has(initPathPy.key) || map.has(initPathPyi.key) || map.has(initPathVpnci.key);
const sourceFileInfo = this.program.getSourceFileInfo(uri);
return { isStub, hasInit, isUserCode: isUserCode(sourceFileInfo) };
}
@@ -79,6 +79,7 @@ import { appendArray } from '../common/collectionUtils';
import { ExecutionEnvironment } from '../common/configOptions';
import * as debug from '../common/debug';
import { fail } from '../common/debug';
import { IsViperDialect } from '../common/dialect';
import { ProgramView } from '../common/extensibility';
import { fromLSPAny, toLSPAny } from '../common/lspUtils';
import { convertOffsetToPosition, convertPositionToOffset } from '../common/positionUtils';
@@ -137,6 +138,11 @@ import {
import { DocumentSymbolCollector } from './documentSymbolCollector';
import { getImportFromTarget, getModuleNameCompletionSuggestions } from './importStatementCandidates';
import { getAutoImportText, getDocumentationPartsForTypeAndDecl } from './tooltipUtils';
import {
findViperAttributeContext,
getViperAttributeSymbols,
getViperUndeclaredAttributeNames,
} from './viperAttributeUtils';
namespace Keywords {
const base: string[] = [
@@ -184,7 +190,41 @@ namespace Keywords {
const python3_15: string[] = [...python3_10, 'lazy'];
export function forVersion(version: PythonVersion): string[] {
// Viper 扩展:Viper 方言专有关键字(Vipeye 仅支持 Viper,故一律并入补全)。
// CPython 的关键字表里没有这些词,但它们在 Viper 里都是合法的语句/声明前缀,
// 不并入就会"看不见"(如 define/typedef/const 等):
// - 硬关键字:define(宏函数,等价 def)、const / volatile(类型描述符前缀);
// - 软关键字:typedef(类型别名);inline / export / extern / virtual /
// novirtual / static(C 风格修饰;static 意为禁止跨模块取参);enum /
// rnum / union / struct(类/枚举声明前缀);fn / fun / func / function(def 别名)。
// - `__attribute__`:宏属性装饰器/注解修饰器(`@__attribute__(...)` /
// `a: __attribute__(...) T`),非关键字(普通标识符),但写法特殊,
// 单独键入 `__a` 时若不列出就"看不见"。
const viper: string[] = [
'define',
'const',
'volatile',
'typedef',
'fallthrough',
'inline',
'export',
'extern',
'virtual',
'novirtual',
'static',
'enum',
'rnum',
'union',
'struct',
'fn',
'fun',
'func',
'function',
'__attribute__',
];
/** 版本对应的 CPython 关键字。 */
function versionKeywords(version: PythonVersion): string[] {
if (PythonVersion.isGreaterOrEqualTo(version, pythonVersion3_15)) {
return python3_15;
}
@@ -197,6 +237,10 @@ namespace Keywords {
return base;
}
export function forVersion(version: PythonVersion): string[] {
return [...versionKeywords(version), ...viper];
}
// Keywords that can only begin a statement and can never begin an expression,
// so they are suppressed in the expression-only slots recognized here. (Some
// of these, like `from`, can still appear *within* an expression form such as
@@ -1146,6 +1190,11 @@ export class CompletionProvider {
}
if (detail?.typeDetail) {
// 围栏语言保持 `python`:宿主渲染时按 languageService.getLanguageIdByLanguageName()
// 解析围栏语言并取该语言的 TM tokenization。本扩展的 `viper` 语法虽能被取到,
// 但只有注释/字符串/控制流关键字/数字等通用 scope 能命中主题规则
// (`def`/`class` 的 storage.type.*.viper 与按 Python 精调的主题规则不匹配,
// 裸类型名语法里没有规则),着色不完整;`python`(MagicPython)齐全,故用它。
markdownString += '```python\n' + detail.typeDetail + '\n```\n';
}
@@ -1295,6 +1344,15 @@ export class CompletionProvider {
return undefined;
}
// Viper 扩展:`__attribute__(...)` 括号内提示属性名(t.attr 的属性方法)。
// 必须在常规 parse 树分派之前:`__attribute__(` 前缀会脱糖为
// `t.attr.X | 内层类型`,空实参时该节点不保留在 parse 树里,
// 光标位置没有可命中的节点,走常规流程会返回无关补全。
const attributeCompletions = this._getViperAttributeCompletions(offset);
if (attributeCompletions) {
return attributeCompletions;
}
let node = ParseTreeUtils.findNodeByOffset(this.parseResults.parserOutput.parseTree, offset);
// See if we're inside a string literal or an f-string statement.
@@ -3466,6 +3524,58 @@ export class CompletionProvider {
});
}
// Viper 扩展:光标位于 `__attribute__(...)` 括号内时,提示 `t.attr` 的属性名
// (section / aligned / always_inline 等;与 TPC/TPV 的 `_MakeTAttrMarker` 一致,
// 实参只用无前缀属性名,故这里只列 t.attr 直接的属性方法,不含 llvm 描述符类)。
// 上下文识别与 t.attr 查找见 viperAttributeUtils(与 hoverProvider 共用)。
private _getViperAttributeCompletions(offset: number): CompletionMap | undefined {
const moduleNode = this.parseResults.parserOutput.parseTree;
if (!IsViperDialect(AnalyzerNodeInfo.getFileInfo(moduleNode).executionEnvironment.dialect)) {
return undefined;
}
if (!findViperAttributeContext(this._fileContents, offset)) {
return undefined;
}
const attributeInfo = getViperAttributeSymbols(this.evaluator, moduleNode);
if (!attributeInfo) {
return undefined;
}
// 光标所在行的光标前文本,用于过滤已输入的部分。
const lineTextRange = this.parseResults.tokenizerOutput.lines.getItemAt(this.position.line);
const textOnLine = this._fileContents.substr(lineTextRange.start, lineTextRange.length);
const priorText = textOnLine.substr(0, this.position.character);
const priorWordIndex = priorText.search(/[\p{L}\p{N}\p{Pc}\p{Mn}\p{Mc}]+$/u);
const priorWord = priorWordIndex >= 0 ? priorText.substr(priorWordIndex) : '';
const completionMap = new CompletionMap();
attributeInfo.symbolTable.forEach((symbol, name) => {
if (completionMap.has(name)) {
return;
}
this.addSymbol(name, symbol, priorWord, completionMap, {
boundObjectOrClass: attributeInfo.attrClass,
declaredOnBoundObjectOrClass: true,
funcParensDisabled: false,
extraCommitChars: !!priorWord,
});
});
// 属性名是编译器级开放集合:`t.attr` 未声明、但 codegen 支持的名字
// (如 stackrealign)也要列出(见 viperAttributeUtils)。
getViperUndeclaredAttributeNames(attributeInfo).forEach((name) => {
this.addNameToCompletions(name, CompletionItemKind.Function, priorWord, completionMap, {
funcParensDisabled: false,
extraCommitChars: !!priorWord,
itemDetail: '__attribute__ 属性名(编译器 codegen 支持)',
});
});
return completionMap;
}
private _addSymbols(node: ParseNode, priorWord: string, completionMap: CompletionMap) {
let curNode: ParseNode | undefined = node;
@@ -160,6 +160,11 @@ export function getCompletionItemDocumentation(
declaration: Declaration | undefined
): MarkupContent | undefined {
if (markupKind === MarkupKind.Markdown) {
// 围栏语言保持 `python`:宿主渲染时按 languageService.getLanguageIdByLanguageName()
// 解析围栏语言并取该语言的 TM tokenization。本扩展的 `viper` 语法虽能被取到,
// 但只有注释/字符串/控制流关键字/数字等通用 scope 能命中主题规则
// (`def`/`class` 的 storage.type.*.viper 与按 Python 精调的主题规则不匹配,
// 裸类型名语法里没有规则),着色不完整;`python`(MagicPython)齐全,故用它。
let markdownString = '```python\n' + typeDetail + '\n```\n';
if (documentation) {
@@ -19,6 +19,7 @@ import {
isUnresolvedAliasDeclaration,
} from '../analyzer/declaration';
import { getFileInfo } from '../analyzer/analyzerNodeInfo';
import { getViperEnumMemberValues } from '../analyzer/enums';
import * as ParseTreeUtils from '../analyzer/parseTreeUtils';
import { SourceMapper } from '../analyzer/sourceMapper';
import { SynthesizedTypeInfo } from '../analyzer/symbol';
@@ -47,7 +48,15 @@ import { convertOffsetToPosition, convertPositionToOffset } from '../common/posi
import { ServiceProvider } from '../common/serviceProvider';
import { Position, Range, TextRange } from '../common/textRange';
import { Uri } from '../common/uri/uri';
import { ExpressionNode, FunctionNode, NameNode, ParseNode, ParseNodeType, StringNode } from '../parser/parseNodes';
import {
ExpressionNode,
FunctionNode,
IndexNode,
NameNode,
ParseNode,
ParseNodeType,
StringNode,
} from '../parser/parseNodes';
import { ParseFileResults } from '../parser/parser';
import { TokenType } from '../parser/tokenizerTypes';
import {
@@ -57,9 +66,11 @@ import {
getToolTipForType,
getTypeForToolTip,
getWrappedFunctionType,
} from './tooltipUtils';
} from './tooltipUtils';
import { findViperAttributeContext, getViperAttributeNames, getViperAttributeSymbols } from './viperAttributeUtils';
export interface HoverTextPart {
// 该部分按代码块渲染(markdown 下加代码围栏,见 convertHoverResults)。
python?: boolean;
text: string;
}
@@ -78,6 +89,16 @@ export function convertHoverResults(hoverResults: HoverResults | null, format: M
.map((part) => {
if (part.python) {
if (format === MarkupKind.Markdown) {
// 围栏语言保持 `python`:hover 代码块由宿主 markdown 渲染器着色,它先用
// languageService.getLanguageIdByLanguageName() 把围栏语言名解析成语言 id,
// 再取该语言的 TM tokenization,取不到才整块退化为纯文本。
// 本扩展贡献的 `viper` 语法在该路径下能取到 tokenization,但只有通用 scope
// (comment/string/keyword.control.flow/constant.numeric)能命中主题规则;
// `def`/`class` 用的 storage.type.*.viper 匹配不到主题里按 Python 精调的
// 规则,裸类型名(int/str)语法里也没有规则 —— 实测 ```viper 只部分着色,
// 而 ```python 走 MagicPython,关键字/类型/装饰器齐全,故用它。
// Viper 语法本就是 CPython 派生,Python 语法对关键字/装饰器/字符串/数字的
// 着色是正确的。
return '```python\n' + part.text + '\n```\n';
} else if (format === MarkupKind.PlainText) {
return part.text + '\n\n';
@@ -201,6 +222,43 @@ export function addDocumentationResultsPart(
fail(`Unsupported markup type: ${format}`);
}
// Viper 扩展:构造 `enum X:` 枚举成员的 hover 类型文本(枚举类型 + 编译期推导值),
// 例如 `ASTFlag = 0`。非 `enum X:` 的成员返回 undefined,走常规显示逻辑。
function getViperEnumMemberTypeText(declaration: VariableDeclaration | undefined, name: string): string | undefined {
if (!declaration || declaration.node.nodeType !== ParseNodeType.Name) {
return undefined;
}
const classNode = ParseTreeUtils.getEnclosingClass(declaration.node, /* stopAtFunction */ true);
if (!classNode?.d.isViperEnum) {
return undefined;
}
const className = classNode.d.name.d.value;
const value = getViperEnumMemberValues(classNode).get(name);
return value === undefined ? `${name}: ${className}` : `${name}: ${className} = ${value}`;
}
// Viper 扩展:判断类型注解是否是/包含 `[SIZE]T` 数组语法(脱糖后的下标节点,
// 见 parser._parseViperBracketArrayType)。用于让 hover 按源码原形显示
// (如 `[4]t.CInt`)而非脱糖后的 `CArray[CInt, 4]`。联合类型逐操作数判断。
function hasViperBracketArrayAnnotation(node: ExpressionNode | undefined): boolean {
if (!node) {
return false;
}
if (node.nodeType === ParseNodeType.Index && (node as IndexNode).d.isViperBracketArray === true) {
return true;
}
if (node.nodeType === ParseNodeType.BinaryOperation) {
return (
hasViperBracketArrayAnnotation(node.d.leftExpr) ||
hasViperBracketArrayAnnotation(node.d.rightExpr)
);
}
return false;
}
export function getVariableTypeText(
evaluator: TypeEvaluator,
declaration: VariableDeclaration | undefined,
@@ -208,13 +266,28 @@ export function getVariableTypeText(
type: Type,
typeNode: ExpressionNode,
functionSignatureDisplay: SignatureDisplayType,
sourceMapper?: SourceMapper
sourceMapper?: SourceMapper,
printOptions?: PrintTypeOptions
) {
let label = 'variable';
if (declaration) {
label = declaration.isConstant || evaluator.isFinalVariableDeclaration(declaration) ? 'constant' : 'variable';
}
// Viper 扩展:`enum X:` 枚举成员显示枚举类型与编译期推导值(如 `ASTFlag = 0`)。
// 裸名成员没有赋值也没有注解,常规路径只会给出 Unbound。
const viperEnumMemberText = getViperEnumMemberTypeText(declaration, name);
if (viperEnumMemberText) {
return `(${label}) ` + viperEnumMemberText;
}
// Viper 扩展:`[SIZE]T` 数组语法注解按源码原形显示(如 `x: [4]t.CInt`),
// 而不是脱糖后的 `CArray[CInt, 4]`,与用户书写形态保持一致。
const bracketArrayAnnotation = declaration?.typeAnnotationNode;
if (bracketArrayAnnotation && hasViperBracketArrayAnnotation(bracketArrayAnnotation)) {
return `(${label}) ${name}: ` + ParseTreeUtils.printExpression(bracketArrayAnnotation);
}
let typeVarName: string | undefined;
if (type.props?.typeAliasInfo && typeNode.nodeType === ParseNodeType.Name) {
@@ -226,6 +299,7 @@ export function getVariableTypeText(
} else {
// Handle type aliases specially.
const typeText = evaluator.printType(convertToInstance(getTypeForToolTip(evaluator, typeNode)), {
...printOptions,
expandTypeAlias: true,
});
@@ -245,6 +319,7 @@ export function getVariableTypeText(
(declaration.typeAnnotationNode as any).d?.member?.d?.value === 'CTypedef'
) {
const typeText = evaluator.printType(convertToInstance(getTypeForToolTip(evaluator, typeNode)), {
...printOptions,
expandTypeAlias: false,
});
return `(type) ${name} = ` + typeText;
@@ -263,11 +338,13 @@ export function getVariableTypeText(
/* isProperty */ false,
functionSignatureDisplay,
typeNode,
sourceMapper
sourceMapper,
printOptions
);
}
const typeText = typeVarName ?? name + ': ' + evaluator.printType(getTypeForToolTip(evaluator, typeNode));
const typeText =
typeVarName ?? name + ': ' + evaluator.printType(getTypeForToolTip(evaluator, typeNode), printOptions);
// Viper 扩展:变量类型是 typedef 别名时,附加 typedef 信息换行显示。
// 例如 `k: UINT16PTR` 悬浮显示:
@@ -284,10 +361,10 @@ export function getVariableTypeText(
const fileInfo = getFileInfo(typeNode);
if (IsViperDialect(fileInfo.executionEnvironment.dialect)) {
const aliasName = aliasInfo.shared.name;
const actualType = evaluator.printType(
convertToInstance(getTypeForToolTip(evaluator, typeNode)),
{ expandTypeAlias: false }
);
const actualType = evaluator.printType(convertToInstance(getTypeForToolTip(evaluator, typeNode)), {
...printOptions,
expandTypeAlias: false,
});
return `(${label}) ` + typeText + `\n(typedef) ${aliasName} = ` + actualType;
}
}
@@ -347,6 +424,59 @@ export class HoverProvider {
return this._program.configOptions.functionSignatureDisplay;
}
// Viper 方言:hover 的类型展示保留源码中书写时的完整 module 路径(如 `ast.AST`)。
// 未写限定路径的裸名(`list`/`str`)仍显示短名,具体规则见 typePrinter。
private _getPrintTypeOptions(options?: PrintTypeOptions): PrintTypeOptions {
return { ...options, sourceQualifiedNames: true };
}
private _printType(type: Type, options?: PrintTypeOptions): string {
return this._evaluator.printType(type, this._getPrintTypeOptions(options));
}
// Viper 扩展:光标位于 `__attribute__(...)` 实参括号内时,列出该修饰器可用的
// 全部属性名(section / aligned / always_inline / weak 等)。
// 上下文识别与 `t.attr` 成员查找复用 viperAttributeUtils,与括号内补全同源。
private _getViperAttributeHover(offset: number): HoverResults | null {
if (!this._parseResults) {
return null;
}
const moduleNode = this._parseResults.parserOutput.parseTree;
if (!IsViperDialect(getFileInfo(moduleNode).executionEnvironment.dialect)) {
return null;
}
const context = findViperAttributeContext(this._parseResults.text, offset);
if (!context) {
return null;
}
const attributeInfo = getViperAttributeSymbols(this._evaluator, moduleNode);
if (!attributeInfo) {
return null;
}
const names = getViperAttributeNames(attributeInfo);
return {
parts: [
{ text: '`__attribute__(...)` 可用属性名:' },
{ text: names.join(' '), python: true },
{
text:
'\n\n带参数者写成调用,如 `section(".text.wo")` / `aligned(16)` / ' +
'`format(printf, 1, 2)`;无参数者可直接写属性名(`packed`)或写调用(`packed()`)。' +
'\n\n属性清单来自 `t.attr`(TransPyC 的 `lib/includes/t.py`)与编译器 codegen 支持的属性名。',
},
],
range: {
start: convertOffsetToPosition(context.nameStart, this._parseResults.tokenizerOutput.lines),
end: convertOffsetToPosition(context.openParen + 1, this._parseResults.tokenizerOutput.lines),
},
};
}
private _getHoverResult(): HoverResults | null {
throwIfCancellationRequested(this._token);
@@ -359,6 +489,14 @@ export class HoverProvider {
return null;
}
// Viper 扩展:光标位于 `__attribute__(...)` 实参括号内时,提示可用的全部属性名。
// 必须在常规 parse 树分派之前:`__attribute__(...)` 脱糖后原始调用节点不保留在
// parse 树中,光标位置(尤其实参为空/未写完时)没有可命中的节点。
const attributeHover = this._getViperAttributeHover(offset);
if (attributeHover) {
return attributeHover;
}
const node = ParseTreeUtils.findNodeByOffset(this._parseResults.parserOutput.parseTree, offset);
if (node === undefined) {
return null;
@@ -413,7 +551,10 @@ export class HoverProvider {
node.d.value,
this._evaluator,
isProperty,
this._functionSignatureDisplay
this._functionSignatureDisplay,
/* typeNode */ undefined,
/* sourceMapper */ undefined,
this._getPrintTypeOptions()
);
}
@@ -506,7 +647,8 @@ export class HoverProvider {
type,
typeNode,
this._functionSignatureDisplay,
this._sourceMapper
this._sourceMapper,
this._getPrintTypeOptions()
);
this._addResultsPart(parts, typeText, /* python */ true);
@@ -520,13 +662,21 @@ export class HoverProvider {
// (parameter) fb: UINT32PTR
// (typedef) UINT32PTR = CPtr[CUInt32T]
const paramType = this._getType(node);
let paramLine = '(parameter) ' + node.d.value + this._getTypeText(node);
// Viper 扩展:`[SIZE]T` 数组语法注解按源码原形显示(如 `[4]t.CInt`)。
const paramAnnotation = resolvedDecl.node.d.annotation;
let paramLine =
paramAnnotation && hasViperBracketArrayAnnotation(paramAnnotation)
? '(parameter) ' +
node.d.value +
': ' +
ParseTreeUtils.printExpression(paramAnnotation)
: '(parameter) ' + node.d.value + this._getTypeText(node);
if (paramType?.props?.typeAliasInfo && node.nodeType === ParseNodeType.Name) {
const aliasInfo = getTypeAliasInfo(paramType);
if (aliasInfo && aliasInfo.shared.name !== node.d.value) {
const fileInfo = getFileInfo(node);
if (IsViperDialect(fileInfo.executionEnvironment.dialect)) {
const actualType = this._evaluator.printType(
const actualType = this._printType(
convertToInstance(getTypeForToolTip(this._evaluator, node)),
{ expandTypeAlias: false }
);
@@ -596,10 +746,12 @@ export class HoverProvider {
isProperty,
this._functionSignatureDisplay,
/* typeNode */ undefined,
this._sourceMapper
this._sourceMapper,
this._getPrintTypeOptions()
);
this._addResultsPart(parts, signatureString, /* python */ true);
this._addDocumentationPart(parts, node, resolvedDecl);
break;
}
@@ -613,7 +765,7 @@ export class HoverProvider {
case DeclarationType.TypeAlias: {
const type = convertToInstance(this._getType(node));
const typeText = this._evaluator.printType(type, { expandTypeAlias: true });
const typeText = this._printType(type, { expandTypeAlias: true });
this._addResultsPart(parts, `(type) ${node.d.value} = ${typeText}`, /* python */ true);
this._addDocumentationPart(parts, node, resolvedDecl);
break;
@@ -640,7 +792,8 @@ export class HoverProvider {
type,
node,
this._functionSignatureDisplay,
this._sourceMapper
this._sourceMapper,
this._getPrintTypeOptions()
);
}
@@ -658,7 +811,7 @@ export class HoverProvider {
const entry = subtype.shared.typedDictEntries?.knownItems.get(node.d.value);
if (entry) {
// e.g. (key) name: str
const text = '(key) ' + node.d.value + ': ' + this._evaluator.printType(entry.valueType);
const text = '(key) ' + node.d.value + ': ' + this._printType(entry.valueType);
const declarations = ClassType.getSymbolTable(subtype).get(node.d.value)?.getDeclarations();
let declarationWithDoc: Declaration | undefined;
@@ -706,7 +859,13 @@ export class HoverProvider {
if (result.methodType && isFunctionOrOverloaded(result.methodType)) {
this._addResultsPart(
parts,
getConstructorTooltip(node.d.value, result.methodType, this._evaluator, this._functionSignatureDisplay),
getConstructorTooltip(
node.d.value,
result.methodType,
this._evaluator,
this._functionSignatureDisplay,
this._getPrintTypeOptions()
),
/* python */ true
);
@@ -728,7 +887,7 @@ export class HoverProvider {
private _getTypeText(node: ExpressionNode, options?: PrintTypeOptions): string {
const type = this._getType(node);
return ': ' + this._evaluator.printType(type, options);
return ': ' + this._printType(type, options);
}
private _addDocumentationPart(parts: HoverTextPart[], node: NameNode, resolvedDecl: Declaration | undefined) {
@@ -15,7 +15,7 @@
*/
import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo';
import { Declaration, DeclarationType, VariableDeclaration } from '../analyzer/declaration';
import { Declaration, DeclarationType, FunctionDeclaration, VariableDeclaration } from '../analyzer/declaration';
import { ParseTreeWalker } from '../analyzer/parseTreeWalker';
import { isWithinTypeAnnotation } from '../analyzer/parseTreeUtils';
import { TypeCategory, isCDefineType } from '../analyzer/types';
@@ -605,12 +605,42 @@ class SemanticTokenWalker extends ParseTreeWalker {
// 注意:必须在 declInfo 查询之前优先处理,因为 CPtr 解引用转发
// 可能让 getDeclInfoForNameNode 返回 CPtr.__getattr__ 的 variable
// 声明(VariableDeclaration),导致误显示为变量颜色。
if (nameText === '__before_init__' && this._isViperMemberAccess(node)) {
// Viper 扩展:`__attribute__(name)` 脱糖出的属性名(`t.attr.<name>`,
// 见 parser._makeViperAttrMarker)。属性名是编译器级的**开放集合**,未必声明在
// t.attr 里(如 stackrealign),故不能依赖符号解析(解析不到会落到 undefined
// 白色),直接按已声明属性同款(function)着色。
if (node.d.isViperAttrMarkerName === true) {
tokenType = SemanticTokenTypes.function;
tokenModifiers = 0;
} else if (nameText === '__before_init__' && this._isViperMemberAccess(node)) {
tokenType = SemanticTokenTypes.function;
tokenModifiers = 0;
} else if (!declInfo || !declInfo.decls || declInfo.decls.length === 0) {
// 未定义符号 → 白色(默认前景)
tokenType = SemanticTokenTypes.undefined;
// Viper 扩展:REnum 变体字段(如 x.Pointee,字段定义在嵌套类变体
// class Ptr 内)常规声明查找找不到,且 getType 对成员裸名称返回 Unknown;
// 改用成员访问表达式求值获取类型,可求值时显示为 property 而非未定义白色。
if (this._isViperMemberAccess(node)) {
const memberAccessType = this._evaluator.getTypeOfExpression(node.parent as MemberAccessNode).type;
if (
memberAccessType !== undefined &&
memberAccessType.category !== TypeCategory.Unknown &&
memberAccessType.category !== TypeCategory.Unbound
) {
// Viper 扩展:成员访问可求值时,若为函数类型(如 REnum 自带
// kind()、CPtr 解引用转发的方法)显示为方法(函数颜色);
// 数据字段显示为 property。
tokenType =
memberAccessType.category === TypeCategory.Function ||
memberAccessType.category === TypeCategory.Overloaded
? SemanticTokenTypes.function
: SemanticTokenTypes.property;
} else {
tokenType = SemanticTokenTypes.undefined;
}
} else {
// 未定义符号 → 白色(默认前景)
tokenType = SemanticTokenTypes.undefined;
}
} else {
const decl: Declaration | undefined = declInfo.decls[0];
const declType = decl?.type;
@@ -631,7 +661,12 @@ class SemanticTokenWalker extends ParseTreeWalker {
tokenModifiers |= defMod;
break;
case DeclarationType.Function:
tokenType = SemanticTokenTypes.function;
// Viper 扩展:define 宏函数名(定义处与调用处)显示为 macro(深蓝色)。
if ((decl as FunctionDeclaration).node.d.isDefine) {
tokenType = SemanticTokenTypes.macro;
} else {
tokenType = SemanticTokenTypes.function;
}
tokenModifiers |= defMod;
break;
case DeclarationType.Param:
@@ -38,6 +38,8 @@ export const enum SemanticTokenTypes {
decorator = 12,
/** 属性 */
property = 13,
/** 宏函数(define 定义的编译期宏,显示为深蓝色) */
macro = 14,
}
/** 语义 token 修饰符(位掩码) */
@@ -70,6 +72,7 @@ export const semanticTokenTypesLegend: string[] = [
'undefined',
'decorator',
'property',
'macro',
];
/** LSP legend 中的 tokenModifiers 字符串列表(顺序与 bit 位对应) */
@@ -24,7 +24,7 @@ import {
getPropertyDocStringInherited,
getVariableDocString,
} from '../analyzer/typeDocStringUtils';
import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes';
import { PrintTypeOptions, TypeEvaluator } from '../analyzer/typeEvaluatorTypes';
import { MemberAccessFlags, lookUpClassMember } from '../analyzer/typeUtils';
import {
ClassType,
@@ -67,7 +67,8 @@ export function getToolTipForType(
isProperty: boolean,
functionSignatureDisplay: SignatureDisplayType,
typeNode?: ExpressionNode,
sourceMapper?: SourceMapper
sourceMapper?: SourceMapper,
printOptions?: PrintTypeOptions
): string {
// Support __call__ method for class instances to show the signature of the method
if (type.category === TypeCategory.Class && isClassInstance(type) && typeNode) {
@@ -91,7 +92,8 @@ export function getToolTipForType(
evaluator,
functionSignatureDisplay,
/* columnThreshold */ 70,
sourceMapper
sourceMapper,
printOptions
)}`;
} else if (isFunction(type)) {
signatureString = `${getFunctionTooltip(
@@ -101,11 +103,12 @@ export function getToolTipForType(
evaluator,
isProperty,
functionSignatureDisplay,
sourceMapper
sourceMapper,
printOptions
)}`;
} else {
signatureString = label.length > 0 ? `(${label}) ` : '';
signatureString += `${name}: ${evaluator.printType(type)}`;
signatureString += `${name}: ${evaluator.printType(type, printOptions)}`;
}
return signatureString;
@@ -117,7 +120,8 @@ export function getOverloadedTooltip(
evaluator: TypeEvaluator,
functionSignatureDisplay: SignatureDisplayType,
columnThreshold = 70,
sourceMapper?: SourceMapper
sourceMapper?: SourceMapper,
printOptions?: PrintTypeOptions
) {
let content = '';
const overloads = OverloadedType.getOverloads(type).map((o) =>
@@ -128,7 +132,8 @@ export function getOverloadedTooltip(
evaluator,
/* isProperty */ false,
functionSignatureDisplay,
sourceMapper
sourceMapper,
printOptions
)
);
@@ -157,12 +162,13 @@ export function getFunctionTooltip(
evaluator: TypeEvaluator,
isProperty = false,
functionSignatureDisplay: SignatureDisplayType,
sourceMapper?: SourceMapper
sourceMapper?: SourceMapper,
printOptions?: PrintTypeOptions
) {
const labelFormatted = label.length === 0 ? '' : `(${label}) `;
const indentStr =
functionSignatureDisplay === SignatureDisplayType.formatted ? '\n' + ' '.repeat(functionParamIndentOffset) : '';
let funcParts = evaluator.printFunctionParts(type);
let funcParts = evaluator.printFunctionParts(type, /* extraFlags */ undefined, printOptions);
if (sourceMapper) {
funcParts = replaceStubEllipsisDefaultValues(type, funcParts, sourceMapper);
}
@@ -285,14 +291,15 @@ export function getConstructorTooltip(
constructorName: string,
type: Type,
evaluator: TypeEvaluator,
functionSignatureDisplay: SignatureDisplayType
functionSignatureDisplay: SignatureDisplayType,
printOptions?: PrintTypeOptions
) {
const classText = `class `;
let signature = '';
if (isOverloaded(type)) {
const overloads = OverloadedType.getOverloads(type).map((overload) =>
getConstructorTooltip(constructorName, overload, evaluator, functionSignatureDisplay)
getConstructorTooltip(constructorName, overload, evaluator, functionSignatureDisplay, printOptions)
);
overloads.forEach((overload, index) => {
signature += overload + ': ...' + '\n\n';
@@ -302,7 +309,7 @@ export function getConstructorTooltip(
functionSignatureDisplay === SignatureDisplayType.formatted
? '\n' + ' '.repeat(functionParamIndentOffset)
: ' ';
const funcParts = evaluator.printFunctionParts(type);
const funcParts = evaluator.printFunctionParts(type, /* extraFlags */ undefined, printOptions);
const paramSignature = formatSignature(funcParts, indentStr, functionSignatureDisplay);
signature += `${classText}${constructorName}${paramSignature}`;
}
@@ -0,0 +1,142 @@
/*
* viperAttributeUtils.ts
*
* Viper `__attribute__(...)` 的共享工具:文本上下文识别 + `t.attr` 属性成员查找。
*
* 由 completionProvider(括号内属性名补全)与 hoverProvider(括号内属性名提示)共用,
* 避免两处各写一份 `t.attr` 查找逻辑导致口径漂移。
*
* 属性名是编译器级的**开放集合**,故清单 = `t.attr` 已声明的名字 ∪ 编译器 codegen
* 支持但未声明的名字(见 VIPER_UNDECLARED_ATTRIBUTE_NAMES),不能只依赖存根声明。
*
* 为什么用文本扫描定位上下文:`__attribute__(...)` 在 parser 里会被脱糖
* (装饰器 → `@c.Attribute(t.attr.X, ...)`;注解修饰器 → `t.attr.X | 内层类型`),
* 原始调用节点不保留在 parse 树中,光标位置(尤其实参为空或拼写未完成时)
* 没有可命中的节点。
*/
import { getScopeForNode } from '../analyzer/scopeUtils';
import { Symbol, SymbolTable } from '../analyzer/symbol';
import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes';
import { ClassType, isFunctionOrOverloaded, isInstantiableClass, isModule, ModuleType } from '../analyzer/types';
import { ParseNode } from '../parser/parseNodes';
export interface ViperAttributeContext {
/** `__attribute__` 名字的起始 offset(用于 hover 范围锚点)。 */
nameStart: number;
/** `__attribute__` 名字之后 `(` 的 offset(光标必须在其右侧才算在括号内)。 */
openParen: number;
}
// 编译器(TransPyV)codegen 支持、但 `class attr` 里**未声明**的属性名。
//
// 属性名是编译器级的**开放集合**:TransPyV 的 codegen 映射里支持的名字不一定都
// 声明在 `t.attr`(TransPyC 的 `lib/includes/t.py` / LSP 存根 `t.vpi` 的
// `class attr` 两边一致,都没有 `stackrealign`)。补全/悬停若只枚举 `t.attr`,
// 这类名字就看不到,故在此显式补齐。
// 来源:TransPyC/TransPyV/App/lib/core/Gen/Func/FuncGenBase.vp(`attr_name ==
// "stackrealign"` → LLVM `"stackrealign"` 属性);
// 真实用例:ViperOS `VKernel/Kernel/main.vp`
// `@__attribute__(section(".text.startup"), aligned(16), stackrealign)`。
export const VIPER_UNDECLARED_ATTRIBUTE_NAMES: readonly string[] = ['stackrealign'];
// 补全/悬停用的**属性名全集**:`t.attr` 已声明的名字 ∪ 上面补齐的未声明名字,
// 排序去重。属性名是开放集合,不能只依赖 `t.attr` 的声明。
export function getViperAttributeNames(attributeInfo: { symbolTable: SymbolTable }): string[] {
const names = new Set<string>(attributeInfo.symbolTable.keys());
VIPER_UNDECLARED_ATTRIBUTE_NAMES.forEach((name) => names.add(name));
return Array.from(names).sort();
}
// `t.attr` 里**未声明**、只在编译器 codegen 里支持的属性名(补全时单独注入,
// 已声明的走符号表,避免重复)。
export function getViperUndeclaredAttributeNames(attributeInfo: { symbolTable: SymbolTable }): string[] {
return VIPER_UNDECLARED_ATTRIBUTE_NAMES.filter((name) => !attributeInfo.symbolTable.has(name));
}
// 判断 `offset` 是否位于 `__attribute__(...)` 的实参括号内,是则返回上下文。
// 扫描方式:从光标向前找最近的未闭合 `(`(跳过已输入的属性名/实参),
// 该 `(` 左侧(允许空白分隔)的标识符必须是 `__attribute__`。
// 遇到行尾 / `;`(深度为 0 时)即停止,避免跨语句误判。
export function findViperAttributeContext(fileContents: string, offset: number): ViperAttributeContext | undefined {
let depth = 0;
let openParen = -1;
for (let i = offset - 1; i >= 0; i--) {
const curChar = fileContents[i];
if (curChar === ')') {
depth++;
} else if (curChar === '(') {
if (depth === 0) {
openParen = i;
break;
}
depth--;
} else if ((curChar === '\n' || curChar === ';') && depth === 0) {
break;
}
}
if (openParen < 0) {
return undefined;
}
// `(` 左侧(允许空白分隔)必须是 `__attribute__`。
let nameEnd = openParen;
while (nameEnd > 0 && /\s/.test(fileContents[nameEnd - 1])) {
nameEnd--;
}
let nameStart = nameEnd;
while (nameStart > 0 && /[\p{L}\p{N}_]/u.test(fileContents[nameStart - 1])) {
nameStart--;
}
if (fileContents.slice(nameStart, nameEnd) !== '__attribute__') {
return undefined;
}
return { nameStart, openParen };
}
// 取 `t.attr` 类**自身声明**的属性方法(只保留函数成员,排除嵌套的 llvm 描述符类等)。
// 返回 undefined 表示没有可用的 `t` 模块 / `attr` 类(如源码未 `import t`)。
// 只用 attr 类自身的符号表(不合并 MRO),避免把 CType/object 的继承成员
// (__init__ / __or__ 等)当成属性名。
export function getViperAttributeSymbols(
evaluator: TypeEvaluator,
scopeNode: ParseNode
): { symbolTable: SymbolTable; attrClass: ClassType } | undefined {
// 取 `t` 模块:优先本文件的 `import t`,否则沿 scope 链回退到 builtins 作用域
// (builtins.vpi 通过 `from t import ...` 绑定了 `t`)—— 与 `__attribute__`
// 脱糖出的隐式 `t` 一致(源码不 import t 时属性名补全/悬停仍应可用)。
let scope = getScopeForNode(scopeNode);
let tSymbol: Symbol | undefined;
while (scope && !tSymbol) {
tSymbol = scope.lookUpSymbol('t');
scope = scope.parent;
}
if (!tSymbol) {
return undefined;
}
const tType = evaluator.getEffectiveTypeOfSymbol(tSymbol);
if (!isModule(tType)) {
return undefined;
}
const attrSymbol = ModuleType.getField(tType, 'attr');
if (!attrSymbol) {
return undefined;
}
const attrType = evaluator.getEffectiveTypeOfSymbol(attrSymbol);
if (!isInstantiableClass(attrType)) {
return undefined;
}
const symbolTable: SymbolTable = new Map<string, Symbol>();
ClassType.getSymbolTable(attrType).forEach((symbol, name) => {
if (isFunctionOrOverloaded(evaluator.getEffectiveTypeOfSymbol(symbol))) {
symbolTable.set(name, symbol);
}
});
return { symbolTable, attrClass: attrType };
}
@@ -463,6 +463,14 @@ export namespace Localizer {
export const enumMemberSet = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.enumMemberSet'));
export const enumMemberTypeAnnotation = () => getRawString('Diagnostic.enumMemberTypeAnnotation');
export const viperEnumValueOutOfRange = () =>
new ParameterizedString<{ value: string; type: string; min: string; max: string }>(
getRawString('Diagnostic.viperEnumValueOutOfRange')
);
export const viperIntegerLiteralOutOfRange = () =>
new ParameterizedString<{ value: string; type: string; min: string; max: string }>(
getRawString('Diagnostic.viperIntegerLiteralOutOfRange')
);
export const viperBeforeInitReserved = () => getRawString('Diagnostic.viperBeforeInitReserved');
export const exceptionGroupIncompatible = () => getRawString('Diagnostic.exceptionGroupIncompatible');
export const exceptGroupMismatch = () => getRawString('Diagnostic.exceptGroupMismatch');
@@ -611,6 +619,14 @@ export namespace Localizer {
new ParameterizedString<{ name: string }>(
getRawString('Diagnostic.viperGlobalAssignmentWithoutGlobal')
);
export const viperGlobalRedeclarationWithoutGlobal = () =>
new ParameterizedString<{ name: string }>(
getRawString('Diagnostic.viperGlobalRedeclarationWithoutGlobal')
);
export const viperGlobalRedeclarationWithGlobal = () =>
new ParameterizedString<{ name: string }>(
getRawString('Diagnostic.viperGlobalRedeclarationWithGlobal')
);
export const importSymbolUnknown = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.importSymbolUnknown'));
export const incompatibleMethodOverride = () =>
@@ -914,6 +930,7 @@ export namespace Localizer {
export const returnTypeUnknown = () => getRawString('Diagnostic.returnTypeUnknown');
export const returnTypePartiallyUnknown = () =>
new ParameterizedString<{ returnType: string }>(getRawString('Diagnostic.returnTypePartiallyUnknown'));
export const viperReturnExprInNoneFunction = () => getRawString('Diagnostic.viperReturnExprInNoneFunction');
export const revealLocalsArgs = () => getRawString('Diagnostic.revealLocalsArgs');
export const revealLocalsNone = () => getRawString('Diagnostic.revealLocalsNone');
export const revealTypeArgs = () => getRawString('Diagnostic.revealTypeArgs');
@@ -1178,6 +1195,29 @@ export namespace Localizer {
export const unexpectedExprToken = () => getRawString('Diagnostic.unexpectedExprToken');
export const unexpectedIndent = () => getRawString('Diagnostic.unexpectedIndent');
export const unexpectedUnindent = () => getRawString('Diagnostic.unexpectedUnindent');
export const viperModifierRequiresDef = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperModifierRequiresDef'));
export const viperTypeDescriptorRequiresType = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperTypeDescriptorRequiresType'));
export const viperVolatileValueRequiresCall = () => getRawString('Diagnostic.viperVolatileValueRequiresCall');
export const viperVirtualModifierRequiresClassOrDef = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperVirtualModifierRequiresClassOrDef'));
export const viperVirtualDefPlacement = () => getRawString('Diagnostic.viperVirtualDefPlacement');
export const viperNoVirtualDefPlacement = () => getRawString('Diagnostic.viperNoVirtualDefPlacement');
export const viperVirtualDefInNoVirtualClass = () =>
getRawString('Diagnostic.viperVirtualDefInNoVirtualClass');
export const viperExternBodyInvalid = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperExternBodyInvalid'));
export const viperStaticUsedOutsideOfModule = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperStaticUsedOutsideOfModule'));
export const viperConstDerefAssignment = () =>
new ParameterizedString<{ type: string }>(getRawString('Diagnostic.viperConstDerefAssignment'));
export const viperExternVarCannotHaveValue = () =>
getRawString('Diagnostic.viperExternVarCannotHaveValue');
export const viperStaticInAnnotationNotAllowed = () =>
getRawString('Diagnostic.viperStaticInAnnotationNotAllowed');
export const viperLenNoLenMethod = () =>
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperLenNoLenMethod'));
export const unhashableDictKey = () => getRawString('Diagnostic.unhashableDictKey');
export const unhashableSetEntry = () => getRawString('Diagnostic.unhashableSetEntry');
export const unionForwardReferenceNotAllowed = () => getRawString('Diagnostic.unionForwardReferenceNotAllowed');
@@ -354,6 +354,14 @@
"message": "Type annotations are not allowed for enum members",
"comment": "{Locked='enum'}"
},
"viperEnumValueOutOfRange": {
"message": "Enum member value {value} is out of range for type \"{type}\" (representable range is {min} to {max})",
"comment": "{Locked='{value}'}{Locked='{type}'}{Locked='{min}'}{Locked='{max}'}"
},
"viperIntegerLiteralOutOfRange": {
"message": "Type \"{type}\" cannot correctly store the number {value} (representable range is {min} to {max}); widen the type",
"comment": "{Locked='{value}'}{Locked='{type}'}{Locked='{min}'}{Locked='{max}'}"
},
"viperBeforeInitReserved": {
"message": "\"__before_init__\" is auto-generated by the compiler and cannot be defined manually",
"comment": "{Locked='__before_init__'}"
@@ -649,6 +657,8 @@
"viperUnresolvedImportName": "Unresolved import name \"{name}\"",
"viperUnknownMemberAccess": "Cannot access member \"{memberName}\" on unresolved type, object may be undefined",
"viperGlobalAssignmentWithoutGlobal": "Global variable \"{name}\" requires global declaration when writing inside a function",
"viperGlobalRedeclarationWithoutGlobal": "Duplicate declaration: \"{name}\" is already a module-level variable; inside a function it must not be declared again with a type annotation when no global statement is present (it would shadow the global). Rename it for an independent local variable, or add a global statement and drop this annotation to modify the global",
"viperGlobalRedeclarationWithGlobal": "Duplicate declaration: \"{name}\" is already a module-level variable and is declared again with a type annotation after a global/nonlocal statement. Drop this annotation and assign directly to modify the global",
"importSymbolUnknown": "\"{name}\" is unknown import symbol",
"incompatibleMethodOverride": "Method \"{name}\" overrides class \"{className}\" in an incompatible manner",
"inconsistentIndent": "Unindent amount does not match previous indent",
@@ -1169,6 +1179,7 @@
"returnTypeMismatch": "Type \"{exprType}\" is not assignable to return type \"{returnType}\"",
"returnTypePartiallyUnknown": "Return type, \"{returnType}\", is partially unknown",
"returnTypeUnknown": "Return type is unknown",
"viperReturnExprInNoneFunction": "A Viper function that returns None (no return value) cannot \"return\" an expression whose value is None; only a bare \"return\" or \"return None\" is allowed",
"revealLocalsArgs": {
"message": "Expected no arguments for \"reveal_locals\" call",
"comment": "{Locked='reveal_locals'}"
@@ -1618,6 +1629,19 @@
"unexpectedExprToken": "Unexpected token at end of expression",
"unexpectedIndent": "Unexpected indentation",
"unexpectedUnindent": "Unindent not expected",
"viperModifierRequiresDef": "The modifier \"{name}\" must be used with 'def'",
"viperTypeDescriptorRequiresType": "The type descriptor \"{name}\" must be followed by a type (e.g. a: {name} int)",
"viperVolatileValueRequiresCall": "The volatile value prefix can only modify a call (e.g. a: int = volatile f() / a: int = volatile Struct())",
"viperVirtualModifierRequiresClassOrDef": "The modifier \"{name}\" must be used with 'class' or 'def'",
"viperVirtualDefPlacement": "'virtual def' must appear inside a 'class' or 'virtual class'",
"viperNoVirtualDefPlacement": "'novirtual def' must appear inside a 'class', 'virtual class' or 'novirtual class'",
"viperVirtualDefInNoVirtualClass": "'virtual def' cannot appear inside a 'novirtual class' (this class form emits no vtable)",
"viperExternBodyInvalid": "The extern function \"{name}\" must have a body of only 'pass' or 'None'",
"viperStaticUsedOutsideOfModule": "\"{name}\" is a static symbol and cannot be used across modules (static is visible only inside its defining module)",
"viperConstDerefAssignment": "Cannot assign through a dereference of a const pointer (the data pointed to by \"{type}\" is read-only)",
"viperExternVarCannotHaveValue": "An extern variable declaration cannot have an initializer (the symbol is defined externally)",
"viperStaticInAnnotationNotAllowed": "'static' is not allowed inside a type annotation; write it before the variable name instead ('static name: type')",
"viperLenNoLenMethod": "Type \"{name}\" has no __len__ method and cannot be used with len()",
"unhashableDictKey": "Dictionary key must be hashable",
"unhashableSetEntry": {
"message": "Set entry must be hashable",
@@ -161,6 +161,8 @@
"enumMemberDelete": "无法删除 Enum 成员 \"{name}\"",
"enumMemberSet": "无法分配 Enum 成员 \"{name}\"",
"enumMemberTypeAnnotation": "enum 成员不允许使用类型批注",
"viperEnumValueOutOfRange": "枚举成员的值 {value} 超出类型 \"{type}\" 的可表示范围({min} ~ {max})",
"viperIntegerLiteralOutOfRange": "类型 \"{type}\" 无法正确存储数字 {value}(可表示范围 {min} ~ {max}),需要扩容",
"viperBeforeInitReserved": "\"__before_init__\" 由编译器自动生成,不允许手动定义",
"exceptGroupMismatch": "Try 语句不能同时包含 \"except\" 和 \"except*\"",
"exceptGroupRequiresType": "异常组语法(\"except*\")需要异常类型",
@@ -270,6 +272,8 @@
"viperUnresolvedImportName": "无法解析的导入名称“{name}”",
"viperUnknownMemberAccess": "无法解析类型上的成员“{memberName}”访问,对象可能未定义",
"viperGlobalAssignmentWithoutGlobal": "全局变量“{name}”在函数内写入需要 global 声明",
"viperGlobalRedeclarationWithoutGlobal": "重复声明:“{name}”已是模块级变量,函数内未声明 global 时不得再用类型注解声明同名变量(会把全局遮蔽为局部)。如需独立局部变量请换名;如需修改全局请加 global 声明并删除本行注解",
"viperGlobalRedeclarationWithGlobal": "重复声明:“{name}”已是模块级变量,此处声明 global/nonlocal 之后又用类型注解重新声明。请删除本行的类型注解,直接赋值即可修改全局",
"importSymbolUnknown": "“{name}”是未知的导入符号",
"incompatibleMethodOverride": "方法“{name}”以不兼容的方式替代类“{className}”",
"inconsistentIndent": "取消缩进量与以前的缩进不匹配",
@@ -455,6 +459,7 @@
"returnTypeMismatch": "类型“{exprType}”不可分配给返回类型“{returnType}”",
"returnTypePartiallyUnknown": "返回类型“{returnType}”部分未知",
"returnTypeUnknown": "返回类型未知",
"viperReturnExprInNoneFunction": "返回 None(无返回值)的函数不能用 return 返回值为 None 的表达式,只有裸 return 和 return None 合法",
"revealLocalsArgs": "“reveal_locals”调用应没有参数",
"revealLocalsNone": "此范围内没有 locals 变量",
"revealTypeArgs": "“reveal_type”调用应为单个位置参数",
@@ -616,6 +621,19 @@
"unexpectedExprToken": "表达式末尾出现意外标记",
"unexpectedIndent": "意外缩进",
"unexpectedUnindent": "不应取消缩进",
"viperModifierRequiresDef": "修饰关键字 \"{name}\" 必须与 def 搭配使用",
"viperTypeDescriptorRequiresType": "类型描述符 \"{name}\" 后必须跟类型(如 a: {name} int)",
"viperVolatileValueRequiresCall": "volatile 值前缀只能修饰调用(如 a: int = volatile f() / a: int = volatile Struct())",
"viperVirtualModifierRequiresClassOrDef": "修饰关键字 \"{name}\" 必须与 class 或 def 搭配使用",
"viperVirtualDefPlacement": "\"virtual def\" 必须位于 class 或 virtual class 内",
"viperNoVirtualDefPlacement": "\"novirtual def\" 必须位于 class、virtual class 或 novirtual class 内",
"viperVirtualDefInNoVirtualClass": "\"virtual def\" 不能位于 novirtual class 内(该形态的类不生成 vtable)",
"viperExternBodyInvalid": "extern 函数 \"{name}\" 的函数体必须为 pass 或 None",
"viperStaticUsedOutsideOfModule": "\"{name}\" 是 static 符号,禁止跨模块使用(static 仅限定义模块内可见)",
"viperConstDerefAssignment": "不能给 const 指针的解引用赋值(\"{type}\" 指向的数据为只读)",
"viperExternVarCannotHaveValue": "extern 变量声明不能带初始值(符号定义在外部)",
"viperStaticInAnnotationNotAllowed": "static 修饰符不支持写在类型注解里,请写成「static 变量名: 类型」(修饰符必须位于变量名之前)",
"viperLenNoLenMethod": "类型 \"{name}\" 没有 __len__ 方法,不能用于 len()",
"unhashableDictKey": "字典密钥必须可哈希",
"unhashableSetEntry": "Set 条目必须是可哈希的",
"uninitializedAbstractVariables": "抽象基类中定义的变量未在 final 类 \"{classType}\" 中初始化",
@@ -37,6 +37,7 @@ export const ParseNodeTypeMap: ParseNodeTypeMapType = {
DictionaryExpandEntry: ParseNodeType.DictionaryExpandEntry,
DictionaryKeyEntry: ParseNodeType.DictionaryKeyEntry,
Ellipsis: ParseNodeType.Ellipsis,
Fallthrough: ParseNodeType.Fallthrough,
If: ParseNodeType.If,
Import: ParseNodeType.Import,
ImportAs: ParseNodeType.ImportAs,
@@ -49,6 +49,7 @@ export const enum ParseNodeType {
DictionaryKeyEntry, // 20
Ellipsis,
Fallthrough,
If,
Import,
ImportAs,
@@ -471,11 +472,22 @@ export interface FunctionNode extends ParseNodeBase<ParseNodeType.Function> {
returnAnnotation: ExpressionNode | undefined;
funcAnnotationComment: FunctionAnnotationNode | undefined;
suite: SuiteNode;
// Viper 扩展:函数声明前的 C 修饰关键字(inline/export/extern),用于语义校验。
viperModifier: string | undefined;
// Viper 扩展:define 宏函数标记(语法与 def 完全一致,语义为编译期宏展开)。
isDefine: boolean;
};
}
export namespace FunctionNode {
export function create(defToken: Token, name: NameNode, suite: SuiteNode, typeParams?: TypeParameterListNode) {
export function create(
defToken: Token,
name: NameNode,
suite: SuiteNode,
typeParams?: TypeParameterListNode,
viperModifier?: string,
isDefine?: boolean
) {
const node: FunctionNode = {
start: defToken.start,
length: defToken.length,
@@ -493,6 +505,8 @@ export namespace FunctionNode {
returnAnnotation: undefined,
funcAnnotationComment: undefined,
suite: suite,
viperModifier,
isDefine: isDefine ?? false,
},
};
@@ -555,6 +569,16 @@ export interface ClassNode extends ParseNodeBase<ParseNodeType.Class> {
typeParams: TypeParameterListNode | undefined;
arguments: ArgumentNode[];
suite: SuiteNode;
// Viper 扩展:由 `enum X:` 语法声明(等价 `class X(t.CEnum):`)。
// 成员可以只写成员名(裸名 / 逗号分隔的裸名),值由编译期推导。
isViperEnum: boolean;
// Viper 扩展:类声明前的 vtable 修饰关键字(`virtual class X` / `novirtual class X`)。
// 'virtual' 等价 `@t.CVTable class X`(启用多态 vtable);
// 'novirtual' 等价 `@t.NoVTable class X`(非多态,字段展平嵌入)。
// 未写修饰时为 undefined(普通 class)。
viperClassModifier?: string;
};
}
@@ -574,6 +598,7 @@ export namespace ClassNode {
typeParams,
arguments: [],
suite: suite,
isViperEnum: false,
},
};
@@ -637,6 +662,7 @@ export namespace ClassNode {
a: undefined,
d: { statements: [], typeComment: undefined },
},
isViperEnum: false,
},
};
@@ -904,6 +930,10 @@ export interface UnaryOperationNode extends ParseNodeBase<ParseNodeType.UnaryOpe
operatorToken: Token;
operator: OperatorType;
hasParens: boolean;
// Viper 扩展:该一元 '*' 是由 C 指针类型后缀 `T*` 生成的(而非前缀解引用 `*x`)。
// 用于 `(T*)x` 强转解析与类型求值时的区分。
isViperPointerTypeSuffix?: boolean;
};
}
@@ -1172,6 +1202,12 @@ export interface TypeAnnotationNode extends ParseNodeBase<ParseNodeType.TypeAnno
d: {
valueExpr: ExpressionNode;
annotation: ExpressionNode;
// Viper 扩展:`static x: T = v` 的注解(见
// parser._parseViperStaticAnnotatedStatement)。注解已被改写为
// `t.CStatic | T`,此标记供跨模块取参检查兜底判定(变量声明的
// 离节点是目标 NameNode,见 NameNode.d.isViperStatic)。
isViperStatic?: boolean;
};
}
@@ -1476,6 +1512,11 @@ export interface IndexNode extends ParseNodeBase<ParseNodeType.Index> {
leftExpr: ExpressionNode;
items: ArgumentNode[];
trailingComma: boolean;
// Viper 扩展:`[SIZE]T` 数组语法脱糖出的下标节点(见
// parser._parseViperBracketArrayType)。此时 leftExpr 是合成的 `t.CArray`,
// items 按源码顺序为 [长度, 元素类型](省略长度时只有 [元素类型]),
// 类型求值按 t.CArray[元素类型, 长度] 的语义顺序解读。
isViperBracketArray?: boolean;
};
}
@@ -1653,6 +1694,43 @@ export interface NameNode extends ParseNodeBase<ParseNodeType.Name> {
d: {
token: IdentifierToken;
value: string;
// Viper 扩展:`[SIZE]T` 数组语法脱糖(见 parser._parseViperBracketArrayType)
// 时合成出来的 `t` 名:零长度、不对应源码字符。文件未 import t 时其解析
// 失败须静默退化为 Unknown,不报"未定义 t"。
isViperBracketArraySynthetic?: boolean;
// Viper 扩展:`typedef T1 = T2` 脱糖(见 parser._parseViperTypedefStatement)
// 时合成出来的 `t` 名:零长度、不对应源码字符。语义与手写
// `T1: t.CTypedef = T2` 相同,但本写法不要求源码 import t,
// 故合成 `t` 解析失败时同样静默退化为 Unknown(不报"未定义 t")。
isViperTypedef?: boolean;
// Viper 扩展:类型描述符 `const T` / `volatile T` 脱糖
// (见 parser._parseViperTypeDescriptorPrefix)时合成出来的 `CConst` /
// `CVolatile` 名:零长度、不对应源码字符。语义与手写 `t.CConst | T` 相同,
// 但脱糖目标是 builtins stub 中的别名,不要求源码 import t;
// 名字解析不到时静默退化为 Unknown。
isViperTypeDescriptor?: boolean;
// Viper 扩展:`__attribute__` 脱糖(见 parser._desugarViperMagicAttribute /
// _makeViperAttrMarker)时合成出来的 `t` / `c` **模块根**名:零长度、
// 不对应源码字符。编译器的属性识别是 AST 级的(不要求源码 import t / import c),
// 故这里也允许隐式解析:该名在词法作用域链上取不到时回退到 builtins 作用域
// 的同名绑定(builtins.vpi 通过 `from t import ...` / `from c import ...`
// 绑定了它们),而不是报"未定义 t"。
isViperImplicitModule?: boolean;
// Viper 扩展:`static x: T = v` 的变量声明目标名(见
// parser._parseViperStaticAnnotatedStatement)。变量的 Declaration.node
// 就是这个 NameNode,跨模块取参检查依此判定「声明带 static」。
isViperStatic?: boolean;
// Viper 扩展:`__attribute__(name)` 脱糖(见 parser._makeViperAttrMarker)
// 时由**裸属性名实参**合成的成员名 `t.attr.<name>`:位置取自实参自身的
// token(真实位置,保留跳转/悬停)。属性名是编译器级的开放集合
// (TransPyV 支持的名字未必都声明在 t.attr 里),故该名解析不到时
// 不报「无法访问属性」,语义高亮直接按已声明属性同款(function)着色。
isViperAttrMarkerName?: boolean;
};
}
@@ -2318,6 +2396,30 @@ export namespace ContinueNode {
}
}
/**
* Viper 扩展:`fallthrough` 语句(case 穿透)。
* 与 `break` 同属「终止当前 match 分支」的控制流语句,区别是去向为**下一个 case 体**
* 而不是 match 之后。是软关键字:仅在 match-case 体内、且独占一条简单语句时才是本节点,
* 其它位置仍是普通标识符(见 parser._isAtViperFallthroughStatement)。
*/
export interface FallthroughNode extends ParseNodeBase<ParseNodeType.Fallthrough> {}
export namespace FallthroughNode {
export function create(range: TextRange) {
const node: FallthroughNode = {
start: range.start,
length: range.length,
nodeType: ParseNodeType.Fallthrough,
id: _nextNodeId++,
parent: undefined,
a: undefined,
d: {},
};
return node;
}
}
export interface ReturnNode extends ParseNodeBase<ParseNodeType.Return> {
d: {
expr?: ExpressionNode | undefined;
@@ -2796,6 +2898,7 @@ export type ParseNode =
| DictionaryExpandEntryNode
| DictionaryKeyEntryNode
| EllipsisNode
| FallthroughNode
| IfNode
| ImportNode
| ImportAsNode
File diff suppressed because it is too large. Load diff
@@ -60,6 +60,8 @@ const _keywords: Map<string, KeywordType> = new Map([
['continue', KeywordType.Continue],
['__debug__', KeywordType.Debug],
['def', KeywordType.Def],
// Viper 扩展:define 宏函数关键字(与 def 语义一致,语法完全相同)。
['define', KeywordType.Define],
['del', KeywordType.Del],
['elif', KeywordType.Elif],
['else', KeywordType.Else],
@@ -641,6 +643,188 @@ enum MagicsKind {
Cell,
}
// Viper 扩展:C 风格修饰/声明关键字处理。
// Viper 支持将 C 修饰/声明语义写成语句前缀关键字:
// inline/export/extern def f(...) —— 修饰关键字,必须与 def 搭配
// virtual/novirtual class Foo —— 类级 vtable 声明标记
// virtual/novirtual def f(...) —— 方法级 vtable 声明标记(须在类内)
// fn/fun/func/function f(...) —— def 的别名
// enum/rnum class|struct Foo —— 跳过前缀关键字(后续 class 正常解析)
// enum Foo: —— 等价 `class Foo(t.CEnum):`(枚举)
// rnum Foo: / union Foo: —— 等价 class Foo:
// struct Foo: —— 独立的 struct 关键字(等价 class)
// 返回 'skip':跳过该关键字 token(后续 def/class 正常解析,位置与缩进不变);
// 返回 'class':把 rnum/union/struct 当作 class 关键字;
// 返回 'classEnum':把 enum 当作 class 关键字,并标记为 Viper 枚举
// (等价 `class Foo(t.CEnum)`,analyzer 会赋予 CEnum 枚举语义);
// 返回 'def':把 fn/fun/func/function 当作 def 关键字;
// 返回 'viperModifier':inline/export/extern/virtual/novirtual/static 生成修饰关键字
// token,由 parser 判断搭配是否合法(单独使用则报错);
// undefined:按普通标识符处理(变量名、成员名、导入名、`state:` 注解等)。
function getViperStatementKeywordAction(
text: string,
identifierStart: number,
identifierEnd: number,
name: string,
precededByViperModifier: boolean
): 'skip' | 'class' | 'classEnum' | 'def' | 'viperModifier' | undefined {
const nextWord = peekViperKeywordWord(text, identifierEnd);
if (name === 'static') {
// Viper 扩展:`static` 是**软关键字**,只在语句起始且后随
// `def` 族(static def f())或 `名字 :`(static x: T = v)
// 时才作为修饰关键字;`static = 1` / `static: int` / `static.x` /
// `static(x)` 仍按普通标识符处理(与 TPC includes/ast/parser.vp
// 的 _IsStaticModifier 同判据)。
if (!isViperAtStatementStart(text, identifierStart)) {
return undefined;
}
// 后跟 ':' 直接结束(`static: t.CInt` 这类字段声明)按普通标识符处理。
if (isViperNextCharColon(text, identifierEnd)) {
return undefined;
}
const wordAndNextChar = peekViperKeywordWordAndNextChar(text, identifierEnd);
if (isViperDefFamilyWord(wordAndNextChar.word)) {
return 'viperModifier';
}
if (
wordAndNextChar.word.length > 0 &&
isIdentifierStartChar(wordAndNextChar.word.charCodeAt(0)) &&
wordAndNextChar.nextChar === ':'
) {
return 'viperModifier';
}
return undefined;
}
if (
name === 'inline' ||
name === 'export' ||
name === 'extern' ||
name === 'virtual' ||
name === 'novirtual'
) {
// 修饰关键字仅在语句起始处生效,避免误伤 `self.inline` / `self.virtual` 等成员名。
if (!isViperAtStatementStart(text, identifierStart)) {
return undefined;
}
// 后跟 ':'(注解,如 `state: t.CArray[...]` 风格的字段声明)按普通标识符处理。
if (isViperNextCharColon(text, identifierEnd)) {
return undefined;
}
// 一律生成修饰关键字 token,由 parser 判断搭配是否合法。
return 'viperModifier';
}
if (name === 'fn' || name === 'fun' || name === 'func' || name === 'function') {
// def 别名:语句起始(或紧跟修饰关键字后,如 `inline fn f()`)时:
// 后跟标识符(函数名)→ def;
// 行尾单独使用(后面无任何内容)→ def(parser 会报缺函数名);
// 后跟 ':' / '=' / '(' 等 → 保持标识符(保护 TransPyC 现有
// `func:` 注解、`func = x` 赋值、`func(x)` 调用等合法用法)。
if (!isViperAtStatementStart(text, identifierStart) && !precededByViperModifier) {
return undefined;
}
if (nextWord.length > 0) {
return isIdentifierStartChar(nextWord.charCodeAt(0)) ? 'def' : undefined;
}
return peekViperNextNonWsChar(text, identifierEnd) === undefined ? 'def' : undefined;
}
if (name === 'enum' || name === 'rnum' || name === 'union') {
if (nextWord === 'class' || nextWord === 'struct') {
return 'skip';
}
// `enum A:` / `rnum A:` / `union A:` 单独使用。
if (
!isViperAtStatementStart(text, identifierStart) ||
nextWord.length === 0 ||
!isIdentifierStartChar(nextWord.charCodeAt(0))
) {
return undefined;
}
// `enum A:` 等价 `class A(t.CEnum):`;`rnum A:` / `union A:` 等价 `class A:`。
return name === 'enum' ? 'classEnum' : 'class';
}
if (name === 'struct') {
// 仅当 struct 位于语句起始(行首仅空白)且后跟标识符(类名)时才视为 class,
// 避免误伤 `import struct as s`、`from struct import x` 等合法用法。
if (!isViperAtStatementStart(text, identifierStart)) {
return undefined;
}
return nextWord.length > 0 && isIdentifierStartChar(nextWord.charCodeAt(0)) ? 'class' : undefined;
}
return undefined;
}
function peekViperKeywordWord(text: string, from: number): string {
let pos = from;
const len = text.length;
while (pos < len && (text[pos] === ' ' || text[pos] === '\t')) {
pos++;
}
const start = pos;
while (pos < len && isIdentifierChar(text.charCodeAt(pos))) {
pos++;
}
return text.slice(start, pos);
}
// 跳过空白读出一个标识符词,并返回其后的首个非空白字符(用于判断
// `static x: T` 里名字之后是否紧跟 ':')。到行尾/文件尾时 nextChar 为 undefined。
function peekViperKeywordWordAndNextChar(
text: string,
from: number
): { word: string; nextChar: string | undefined } {
let pos = from;
const len = text.length;
while (pos < len && (text[pos] === ' ' || text[pos] === '\t')) {
pos++;
}
const start = pos;
while (pos < len && isIdentifierChar(text.charCodeAt(pos))) {
pos++;
}
const word = text.slice(start, pos);
let after = pos;
while (after < len && (text[after] === ' ' || text[after] === '\t')) {
after++;
}
return { word, nextChar: after < len ? text[after] : undefined };
}
// def 及其别名(fn/fun/func/function):`static def f()` 与 `static fn f()` 等价。
function isViperDefFamilyWord(word: string): boolean {
return word === 'def' || word === 'fn' || word === 'fun' || word === 'func' || word === 'function';
}
function isViperNextCharColon(text: string, from: number): boolean {
let pos = from;
const len = text.length;
while (pos < len && (text[pos] === ' ' || text[pos] === '\t')) {
pos++;
}
return pos < len && text[pos] === ':';
}
function isViperAtStatementStart(text: string, identifierStart: number): boolean {
let pos = identifierStart;
while (pos > 0 && (text[pos - 1] === ' ' || text[pos - 1] === '\t')) {
pos--;
}
return pos === 0 || text[pos - 1] === '\n' || text[pos - 1] === '\r';
}
// 跳过空白后返回首个非空白字符;若到行尾(换行)或文件结束则返回 undefined。
function peekViperNextNonWsChar(text: string, from: number): string | undefined {
let pos = from;
const len = text.length;
while (pos < len && (text[pos] === ' ' || text[pos] === '\t')) {
pos++;
}
if (pos < len && text[pos] !== '\r' && text[pos] !== '\n') {
return text[pos];
}
return undefined;
}
export class Tokenizer {
private _cs = new CharacterStream('');
private _tokens: Token[] = [];
@@ -1383,6 +1567,55 @@ export class Tokenizer {
this._tokens.push(KeywordToken.create(start, length, keywordType, this._getComments()));
} else {
const value = this._internIdentifier(text, start, end, length);
// Viper 扩展:类型描述符 const / volatile 是硬关键字(Viper 方言保留字),
// 出现在任何位置都生成关键字 token,不再作为普通标识符 ——
// 由此 `const = 1` / `x.volatile` 之类误用会在 parser 层直接报语法错误,
// 而正确的用法只有「类型注解前缀」(见 parser._parseViperTypeDescriptorPrefix)。
if (value === 'const' || value === 'volatile') {
this._tokens.push(
KeywordToken.create(start, length, KeywordType.ViperTypeDescriptor, this._getComments())
);
return true;
}
// Viper 扩展:C 风格修饰/声明关键字。
// inline/export/extern 生成修饰关键字 token(parser 判断是否配 def);
// enum/rnum/struct 在语句起始且后跟类名时按 class 关键字处理;
// fn/fun/func/function 在语句起始(或紧跟修饰关键字)时按 def 关键字处理。
const precededByViperModifier =
this._tokens.length > 0 &&
this._tokens[this._tokens.length - 1].type === TokenType.Keyword &&
(this._tokens[this._tokens.length - 1] as KeywordToken).keywordType === KeywordType.ViperModifier;
const viperAction = getViperStatementKeywordAction(
text,
start,
end,
value,
precededByViperModifier
);
if (viperAction === 'skip') {
return true;
}
if (viperAction === 'class') {
this._tokens.push(KeywordToken.create(start, length, KeywordType.Class, this._getComments()));
return true;
}
if (viperAction === 'classEnum') {
// `enum Foo:` → `class Foo(t.CEnum):`:合成带枚举标记的 class 关键字 token。
this._tokens.push(
KeywordToken.create(start, length, KeywordType.Class, this._getComments(), /* isViperEnum */ true)
);
return true;
}
if (viperAction === 'def') {
this._tokens.push(KeywordToken.create(start, length, KeywordType.Def, this._getComments()));
return true;
}
if (viperAction === 'viperModifier') {
this._tokens.push(
KeywordToken.create(start, length, KeywordType.ViperModifier, this._getComments())
);
return true;
}
this._tokens.push(IdentifierToken.create(start, length, value, this._getComments()));
}
return true;
@@ -121,6 +121,8 @@ export const enum KeywordType {
Continue,
Debug,
Def,
// Viper 扩展:define 宏函数关键字(与 def 语义一致,语法完全相同)。
Define,
Del,
Elif,
Else,
@@ -150,6 +152,10 @@ export const enum KeywordType {
While,
With,
Yield,
// Viper 扩展:inline/export/extern 等 C 风格修饰关键字(必须与 def 搭配)。
ViperModifier,
// Viper 扩展:类型描述符 const / volatile(硬关键字,只能作为类型注解前缀)。
ViperTypeDescriptor,
}
export const softKeywords = [
@@ -339,6 +345,11 @@ export namespace NewLineToken {
export interface KeywordToken extends Token {
readonly type: TokenType.Keyword;
readonly keywordType: KeywordType;
// Viper 扩展:该 class 关键字由 Viper 的 `enum X:` 语法合成
// (等价于 `class X(t.CEnum):`)。parser 据此在 ClassNode 上打标记,
// analyzer 据此赋予枚举语义。
readonly isViperEnum?: boolean;
}
export namespace KeywordToken {
@@ -346,7 +357,8 @@ export namespace KeywordToken {
start: number,
length: number,
keywordType: KeywordType,
comments: Comment[] | undefined
comments: Comment[] | undefined,
isViperEnum = false
): KeywordToken {
if (comments !== undefined) {
return {
@@ -355,6 +367,7 @@ export namespace KeywordToken {
type: TokenType.Keyword,
comments,
keywordType,
isViperEnum,
};
}
@@ -363,6 +376,7 @@ export namespace KeywordToken {
length,
type: TokenType.Keyword,
keywordType,
isViperEnum,
};
}
@@ -118,11 +118,18 @@ export class PartialStubService implements SupportPartialStubs {
continue;
}
// Merge partial stub packages to the library.
// 只映射存根文件(.vpi/.vpnci 为 Viper 存根,.pyi 为 Python 兼容回退,
// 见 importResolver.supportedSourceFileExtensions)与目录;
// 其它文件(如 py.typed 标记)不进入映射视图。
this._movedDirectories.push(
this._realFs.mapDirectory(
packagePath,
partialStubPackagePath,
(u, fs) => u.hasExtension('.vpi') || (fs.existsSync(u) && fs.statSync(u).isDirectory())
(u, fs) =>
u.hasExtension('.vpi') ||
u.hasExtension('.vpnci') ||
u.hasExtension('.pyi') ||
(fs.existsSync(u) && fs.statSync(u).isDirectory())
)
);
} catch {
@@ -0,0 +1,12 @@
import t
define MAX_SIZE: int = 1024
define PAGE_SIZE = 4096
define PAGE_BYTES: t.CUInt64T = 18446744073709551615
define SMALL: t.CInt8T = -128
define TINY: t.i8 = 127
def use() -> int:
a: int = MAX_SIZE
b: int = PAGE_SIZE
return a + b
@@ -0,0 +1,5 @@
define square(x: int) -> int:
return x * x
def use() -> int:
return square(3)
@@ -0,0 +1,11 @@
import t
define BIG_INT: int = 10000000000000000000000000000000
PLAIN_BIG: int = 10000000000000000000000000000000
define SMALL_ENUM: t.CInt8T = 200
define OK_BIG: t.CUInt64T = 18446744073709551615
define OK_NEG: int = -2147483648
define OK_POS: int = 2147483647
def use() -> int:
return 0
@@ -0,0 +1,9 @@
import t
define MAX_SIZE: int = 1024
define FLAG = 1
def use() -> int:
a: int = MAX_SIZE
b: int = FLAG
return a + b
@@ -0,0 +1,5 @@
define A():
pass
def B():
A()
@@ -0,0 +1,19 @@
import t
class CEnumRef(t.CEnum):
RED: t.State = 0
GREEN = 1
enum ASTFlag:
IsAsync
Simple
HasStar: int
enum Mid:
a
b = 5
c: t.u8
def use() -> int:
v: int = ASTFlag.IsAsync
return 0
@@ -0,0 +1,12 @@
import t
enum ASTFlag:
IsAsync
Simple
HasStar: int
def use_enum() -> int:
a: int = ASTFlag.IsAsync
b: int = ASTFlag.Simple
c: int = ASTFlag.HasStar
return 0
@@ -0,0 +1,8 @@
import t
enum X: a, b, c
d
def use_x() -> int:
v: int = X.d
return 0
@@ -0,0 +1,10 @@
import t
enum Small:
a
b
c: t.i1
def use_small() -> int:
v: int = Small.a
return 0
@@ -0,0 +1,10 @@
import t
enum Mid:
a
b = 5
c: t.u8
def use_mid() -> int:
v: int = Mid.c
return 0
@@ -0,0 +1,10 @@
def classify(n: int) -> int:
result: int = 0
if n > 0:
result = 1
elif n < 0:
result = -1
else:
result = 0
return result
@@ -0,0 +1,7 @@
def countdown(n: int) -> int:
while n > 0:
n = n - 1
else:
n = 0
return n
@@ -0,0 +1,6 @@
def early(n: int) -> int:
try:
return n + 1
finally:
n = 0
@@ -0,0 +1,9 @@
def handle(n: int) -> int:
try:
n = n + 1
except ValueError:
n = 0
finally:
n = n * 2
return n
@@ -0,0 +1,9 @@
def mixed(n: int) -> int:
total: int = 0
for i in n:
if i > 0:
total = total + i
else:
total = total - i
return total
@@ -0,0 +1,9 @@
def route(n: int) -> int:
match n:
case 0:
return 1
case 1:
return 2
case _:
return 3
@@ -0,0 +1,6 @@
def route2(n: int) -> int:
match n:
case 0:
n = 1
return n
@@ -0,0 +1,12 @@
def scan(n: int) -> int:
total: int = 0
for i in n:
if i == 0:
continue
if i < 0:
break
total = total + i
else:
total = -1
return total
@@ -0,0 +1,9 @@
def scan2(n: int) -> int:
for i in n:
try:
if i < 0:
break
finally:
n = n * 2
return n
@@ -0,0 +1,7 @@
import t, c
export def add[T](a: t.CInt, b: t.CInt = 10) -> t.CInt:
return a + b
def use():
add(1, 2)
@@ -0,0 +1,19 @@
import t, c
def takes_ptr(p: t.CInt | t.CPtr) -> t.CInt | t.CPtr:
return p
def test_zero_cast():
p: t.CInt | t.CPtr = None
q: t.CInt | t.CPtr = None
p = 0
p = 1
x: t.CInt = 0
p = x
takes_ptr(0)
takes_ptr(1)
addr: t.CUnsignedLong = 0x1234
p = addr
p = t.CInt(addr, t.CPtr)
takes_ptr(p)
takes_ptr(q)
@@ -0,0 +1,10 @@
import t, llvmlite
def test(rhs_val: llvmlite.Value | t.CPtr) -> t.CInt:
if rhs_val.Ty is None:
return 0
match rhs_val.Ty:
case llvmlite.LLVMType.Struct(_, _, _):
tmp: llvmlite.Value | t.CPtr = llvmlite.build_alloca(rhs_val, rhs_val.Ty)
return 0
return 0
@@ -0,0 +1,19 @@
import t, c
class MyEnum(t.REnum):
class Int:
Bits: t.CInt
class Ptr:
Pointee: MyEnum | t.CPtr
def test_enum(e: MyEnum | t.CPtr) -> int:
k: t.CInt = e.kind()
k2: t.CInt = e.kind
return k + k2
def test_assign() -> int:
e: MyEnum | t.CPtr = MyEnum.Int(32)
if e.kind() == 1:
return 1
return 0
@@ -1,491 +0,0 @@
/*
* chainedSourceFiles.test.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*
* Unit tests for tokenizer ipython mode
*/
import assert from 'assert';
import { CancellationToken } from 'vscode-jsonrpc';
import { MarkupKind } from 'vscode-languageserver-types';
import { Program } from '../analyzer/program';
import { AnalyzerService } from '../analyzer/service';
import { IPythonMode } from '../analyzer/sourceFile';
import { ConfigOptions } from '../common/configOptions';
import { NullConsole } from '../common/console';
import { normalizeSlashes } from '../common/pathUtils';
import { convertOffsetsToRange, convertOffsetToPosition } from '../common/positionUtils';
import { ServiceProvider } from '../common/serviceProvider';
import { Uri } from '../common/uri/uri';
import { UriEx } from '../common/uri/uriUtils';
import { CompletionProvider } from '../languageService/completionProvider';
import { parseTestData } from './harness/fourslash/fourSlashParser';
import { TestAccessHost } from './harness/testAccessHost';
import * as host from './harness/testHost';
import { createFromFileSystem, distlibFolder, libFolder } from './harness/vfs/factory';
import * as vfs from './harness/vfs/filesystem';
test('check chained files', () => {
const code = `
// @filename: test1.py
//// def foo1(): pass
// @filename: test2.py
//// def foo2(): pass
// @filename: test3.py
//// def foo3(): pass
// @filename: test4.py
//// [|foo/*marker*/|]
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const parseResult = service.getParseResults(markerUri)!;
const result = new CompletionProvider(
service.test_program,
markerUri,
convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines),
{
format: MarkupKind.Markdown,
lazyEdit: false,
snippet: false,
},
CancellationToken.None
).getCompletions();
assert(result?.items.some((i) => i.label === 'foo1'));
assert(result?.items.some((i) => i.label === 'foo2'));
assert(result?.items.some((i) => i.label === 'foo3'));
});
test('modify chained files', () => {
const code = `
// @filename: test1.py
//// def foo1(): pass
// @filename: test2.py
//// [|/*delete*/|]
//// def foo2(): pass
// @filename: test3.py
//// def foo3(): pass
// @filename: test4.py
//// [|foo/*marker*/|]
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
// Make sure files are all realized.
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const parseResult = service.getParseResults(markerUri)!;
// Close file in the middle of the chain
service.setFileClosed(data.markerPositions.get('delete')!.fileUri);
// Make sure we don't get suggestion from auto import but from chained files.
service.test_program.configOptions.autoImportCompletions = false;
const result = new CompletionProvider(
service.test_program,
markerUri,
convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines),
{
format: MarkupKind.Markdown,
lazyEdit: false,
snippet: false,
},
CancellationToken.None
).getCompletions();
assert(result);
assert(!result.items.some((i) => i.label === 'foo1'));
assert(!result.items.some((i) => i.label === 'foo2'));
assert(result.items.some((i) => i.label === 'foo3'));
});
test('modify chained files', async () => {
const code = `
// @filename: test1.py
//// [|/*changed*/|]
//// def foo1(): pass
// @filename: test2.py
//// def foo2(): pass
// @filename: test3.py
//// def foo3(): pass
// @filename: test4.py
//// [|/*marker*/foo1()|]
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
analyze(service.test_program);
// Initially, there should be no error.
const initialDiags = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(initialDiags.length, 0);
// Change test1 content
service.updateOpenFileContents(data.markerPositions.get('changed')!.fileUri, 2, 'def foo5(): pass');
analyze(service.test_program);
const finalDiags = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(finalDiags.length, 1);
});
function generateChainedFiles(count: number, lastFile: string) {
let code = '';
for (let i = 0; i < count; i++) {
code += `
// @filename: test${i + 1}.py
//// def foo${i + 1}(): pass
`;
}
code += lastFile;
return code;
}
test('chained files with 1000s of files', async () => {
const lastFile = `
// @filename: testFinal.py
//// [|/*marker*/foo1()|]
`;
const code = generateChainedFiles(1000, lastFile);
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
analyze(service.test_program);
// There should be no error as it should find the foo1 in the first chained file.
const initialDiags = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(initialDiags.length, 0);
});
test('imported by files', async () => {
const code = `
// @filename: test1.py
//// import [|/*marker*/os|]
// @filename: test2.py
//// os.path.join()
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(diagnostics.length, 0);
});
test('re ordering cells', async () => {
const code = `
// @filename: test1.py
//// import [|/*marker*/os|]
// @filename: test2.py
//// /*bottom*/os.path.join()
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const bottom = data.markerPositions.get('bottom')!;
const bottomUri = bottom.fileUri;
service.updateChainedUri(bottomUri, undefined);
service.updateChainedUri(markerUri, bottomUri);
analyze(service.test_program);
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(diagnostics.length, 1);
});
test('later chained-file globals resolved in function body', async () => {
const code = `
// @filename: test1.py
//// def use_x():
//// [|/*marker*/return x + 1|]
// @filename: test2.py
//// x = 42
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(diagnostics.length, 0);
});
test('later chained-file globals resolved in lambda body', async () => {
const code = `
// @filename: test1.py
//// [|/*marker*/f = lambda: x * 2|]
// @filename: test2.py
//// x = 10
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(diagnostics.length, 0);
});
test('later chained-file globals resolved in class header', async () => {
const code = `
// @filename: test1.py
//// [|/*marker*/class MyClass(Base):|]
//// pass
// @filename: test2.py
//// class Base:
//// pass
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(diagnostics.length, 0);
});
test('later chained-file globals resolved in comprehension inside function', async () => {
const code = `
// @filename: test1.py
//// def build_list():
//// [|/*marker*/return [i * x for i in [1, 2, 3]]|]
// @filename: test2.py
//// x = 5
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(diagnostics.length, 0);
});
test('later chained-file globals resolved in nested function', async () => {
const code = `
// @filename: test1.py
//// def outer():
//// def inner():
//// [|/*marker*/return x|]
//// return inner
// @filename: test2.py
//// x = 99
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(diagnostics.length, 0);
});
test('later chained-file globals not visible without chaining', async () => {
// Verify that the same code produces errors without chained files
// (i.e. our test scenarios actually catch a real bug).
const code = `
// @filename: test1.py
//// def use_x():
//// [|/*marker*/return x + 1|]
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
// Should have an error because x is not defined anywhere
assert(diagnostics.length > 0);
});
test('later chained-file globals with multiple files and mixed scopes', async () => {
const code = `
// @filename: test1.py
//// def use_both():
//// [|/*marker*/return x + y|]
// @filename: test2.py
//// x = 42
// @filename: test3.py
//// y = 100
`;
const basePath = UriEx.file(normalizeSlashes('/'));
const { data, service } = createServiceWithChainedSourceFiles(basePath, code);
analyze(service.test_program);
const marker = data.markerPositions.get('marker')!;
const markerUri = marker.fileUri;
const range = data.ranges.find((r) => r.marker === marker)!;
const parseResults = service.getParseResults(markerUri)!;
const diagnostics = await service.getDiagnosticsForRange(
markerUri,
convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines),
CancellationToken.None
);
assert.strictEqual(diagnostics.length, 0);
});
function createServiceWithChainedSourceFiles(basePath: Uri, code: string) {
const fs = createFromFileSystem(host.HOST, /*ignoreCase*/ false, { cwd: basePath.getFilePath() });
const service = new AnalyzerService('test service', new ServiceProvider(), {
console: new NullConsole(),
hostFactory: () => new TestAccessHost(UriEx.file(vfs.MODULE_PATH), [libFolder, distlibFolder]),
importResolverFactory: AnalyzerService.createImportResolver,
configOptions: new ConfigOptions(basePath),
fileSystem: fs,
shouldRunAnalysis: () => true,
});
const data = parseTestData(basePath.getFilePath(), code, '');
let chainedFilePath: Uri | undefined;
for (const file of data.files) {
const uri = file.fileUri;
service.setFileOpened(uri, 1, file.content, IPythonMode.CellDocs, chainedFilePath);
chainedFilePath = uri;
}
return { data, service };
}
function analyze(program: Program) {
while (program.analyze()) {
// Process all queued items
}
}
@@ -1,723 +0,0 @@
/*
* checker.test.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
* Author: Eric Traut
*
* Unit tests for viper type checker. These tests also
* exercise the type evaluator (which the checker relies
* heavily upon).
*/
import { ConfigOptions } from '../common/configOptions';
import { pythonVersion3_10, pythonVersion3_8, pythonVersion3_9 } from '../common/pythonVersion';
import { Uri } from '../common/uri/uri';
import * as TestUtils from './testUtils';
test('BadToken1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['badToken1.py']);
// We include this in the checker test rather than the tokenizer or
// parser test suite because it has cascading effects that potentially
// affect the type checker logic.
TestUtils.validateResults(analysisResults, 1);
});
test('Unicode1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['unicode1.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('CircularBaseClass', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['circularBaseClass.py']);
TestUtils.validateResults(analysisResults, 2);
});
test('Private1', () => {
const configOptions = new ConfigOptions(Uri.empty());
// By default, optional diagnostics are ignored.
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['private1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Turn on errors.
configOptions.diagnosticRuleSet.reportPrivateUsage = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['private1.py'], configOptions);
TestUtils.validateResults(analysisResults, 4);
});
test('Private2', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.reportPrivateUsage = 'error';
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['private4.py'], configOptions);
TestUtils.validateResults(analysisResults, 1);
});
test('Private3', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.reportPrivateUsage = 'error';
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['private6.py'], configOptions);
TestUtils.validateResults(analysisResults, 1);
});
test('Constant1', () => {
const configOptions = new ConfigOptions(Uri.empty());
// By default, optional diagnostics are ignored.
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['constant1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Turn on errors.
configOptions.diagnosticRuleSet.reportConstantRedefinition = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['constant1.py'], configOptions);
TestUtils.validateResults(analysisResults, 5);
});
test('AbstractClass1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass1.py']);
TestUtils.validateResults(analysisResults, 2);
});
test('AbstractClass2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass2.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('AbstractClass3', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass3.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('AbstractClass4', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass4.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('AbstractClass5', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass5.py']);
TestUtils.validateResults(analysisResults, 3);
});
test('AbstractClass6', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass6.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('AbstractClass7', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass7.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('AbstractClass8', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass8.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('AbstractClass9', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass9.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('AbstractClass10', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass10.py']);
TestUtils.validateResults(analysisResults, 6);
});
test('AbstractClass11', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['abstractClass11.py']);
TestUtils.validateResults(analysisResults, 2);
});
test('Constants1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['constants1.py']);
TestUtils.validateResults(analysisResults, 20);
});
test('NoReturn1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['noreturn1.py']);
TestUtils.validateResults(analysisResults, 5);
});
test('NoReturn2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['noreturn2.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('NoReturn3', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['noreturn3.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('NoReturn4', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['noreturn4.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('With1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['with1.py']);
TestUtils.validateResults(analysisResults, 4);
});
test('With2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['with2.py']);
TestUtils.validateResults(analysisResults, 3);
});
test('With3', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['with3.py']);
TestUtils.validateResults(analysisResults, 4);
});
test('With4', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.defaultPythonVersion = pythonVersion3_8;
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['with4.py'], configOptions);
TestUtils.validateResults(analysisResults1, 4);
configOptions.defaultPythonVersion = pythonVersion3_9;
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['with4.py'], configOptions);
TestUtils.validateResults(analysisResults2, 0);
});
test('With5', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['with5.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('With6', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['with6.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('With7', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['with7.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('Mro1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['mro1.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('Mro2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['mro2.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('Mro3', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['mro3.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('Mro4', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['mro4.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('DefaultInitializer1', () => {
const configOptions = new ConfigOptions(Uri.empty());
// By default, the reportCallInDefaultInitializer is disabled.
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['defaultInitializer1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Turn on errors.
configOptions.diagnosticRuleSet.reportCallInDefaultInitializer = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['defaultInitializer1.py'], configOptions);
TestUtils.validateResults(analysisResults, 5);
});
test('UnnecessaryIsInstance1', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryIsInstance1.py'], configOptions);
TestUtils.validateResults(analysisResults, 1);
// Turn on errors.
configOptions.diagnosticRuleSet.reportUnnecessaryIsInstance = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryIsInstance1.py'], configOptions);
TestUtils.validateResults(analysisResults, 5);
});
test('UnnecessaryIsInstance2', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryIsInstance2.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Turn on errors.
configOptions.diagnosticRuleSet.reportUnnecessaryIsInstance = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryIsInstance2.py'], configOptions);
TestUtils.validateResults(analysisResults, 2);
});
test('UnnecessaryIsSubclass1', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryIsSubclass1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Turn on errors.
configOptions.diagnosticRuleSet.reportUnnecessaryIsInstance = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryIsSubclass1.py'], configOptions);
TestUtils.validateResults(analysisResults, 2);
});
test('UnnecessaryCast1', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryCast1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Turn on errors.
configOptions.diagnosticRuleSet.reportUnnecessaryCast = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryCast1.py'], configOptions);
TestUtils.validateResults(analysisResults, 6);
});
test('UnnecessaryContains1', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryContains1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Turn on errors.
configOptions.diagnosticRuleSet.reportUnnecessaryContains = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryContains1.py'], configOptions);
TestUtils.validateResults(analysisResults, 5);
});
test('TypeIgnore1', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Disable type ignore
configOptions.diagnosticRuleSet.enableTypeIgnoreComments = false;
analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore1.py'], configOptions);
TestUtils.validateResults(analysisResults, 3);
});
test('TypeIgnore2', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore2.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Disable type ignore
configOptions.diagnosticRuleSet.enableTypeIgnoreComments = false;
analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore2.py'], configOptions);
TestUtils.validateResults(analysisResults, 4);
});
test('TypeIgnore3', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore3.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Disable type ignore
configOptions.diagnosticRuleSet.enableTypeIgnoreComments = false;
analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore3.py'], configOptions);
TestUtils.validateResults(analysisResults, 4);
});
test('TypeIgnore4', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore4.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
configOptions.diagnosticRuleSet.reportUnnecessaryTypeIgnoreComment = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore4.py'], configOptions);
TestUtils.validateResults(analysisResults, 2);
});
test('TypeIgnore5', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore5.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
configOptions.diagnosticRuleSet.reportUnnecessaryTypeIgnoreComment = 'warning';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore5.py'], configOptions);
TestUtils.validateResults(analysisResults, 0, 1);
});
test('ViperIgnore1', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['viperIgnore1.py'], configOptions);
TestUtils.validateResults(analysisResults, 1);
});
test('ViperIgnore2', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['viperIgnore2.py'], configOptions);
TestUtils.validateResults(analysisResults, 2);
configOptions.diagnosticRuleSet.reportUnnecessaryTypeIgnoreComment = 'warning';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['viperIgnore2.py'], configOptions);
TestUtils.validateResults(analysisResults, 2, 3);
});
test('ViperComment1', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['viperComment1.py'], configOptions);
TestUtils.validateResults(analysisResults, 9);
});
test('DuplicateImports1', () => {
const configOptions = new ConfigOptions(Uri.empty());
// By default, optional diagnostics are ignored.
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['duplicateImports1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Turn on errors.
configOptions.diagnosticRuleSet.reportDuplicateImport = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['duplicateImports1.py'], configOptions);
TestUtils.validateResults(analysisResults, 2);
});
test('ParamNames1', () => {
const configOptions = new ConfigOptions(Uri.empty());
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['paramNames1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0, 11);
configOptions.diagnosticRuleSet.reportSelfClsParameterName = 'none';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['paramNames1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0, 0);
configOptions.diagnosticRuleSet.reportSelfClsParameterName = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['paramNames1.py'], configOptions);
TestUtils.validateResults(analysisResults, 11, 0);
});
test('ParamType1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['paramType1.py']);
TestUtils.validateResults(analysisResults, 9);
});
test('Python2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['python2.py']);
TestUtils.validateResults(analysisResults, 7);
});
test('InconsistentSpaceTab1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['inconsistentSpaceTab1.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('InconsistentSpaceTab2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['inconsistentSpaceTab2.py']);
TestUtils.validateResults(analysisResults, 1);
});
test('DuplicateDeclaration1', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['duplicateDeclaration1.py']);
TestUtils.validateResults(analysisResults, 10);
});
test('DuplicateDeclaration2', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['duplicateDeclaration2.py']);
TestUtils.validateResults(analysisResults, 4);
});
test('Strings1', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['strings1.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0);
configOptions.diagnosticRuleSet.reportImplicitStringConcatenation = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['strings1.py'], configOptions);
TestUtils.validateResults(analysisResults2, 2);
});
test('UnusedExpression1', () => {
const configOptions = new ConfigOptions(Uri.empty());
// By default, this is a warning.
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedExpression1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0, 14);
// Disable it.
configOptions.diagnosticRuleSet.reportUnusedExpression = 'none';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedExpression1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Enable it as an error.
configOptions.diagnosticRuleSet.reportUnusedExpression = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedExpression1.py'], configOptions);
TestUtils.validateResults(analysisResults, 14);
});
test('UnusedImport1', () => {
const configOptions = new ConfigOptions(Uri.empty());
// Enabled it
configOptions.diagnosticRuleSet.reportUnusedImport = 'warning';
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedImport1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0, 2);
// Disable it.
configOptions.diagnosticRuleSet.reportUnusedImport = 'none';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedImport1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Enable it as an error.
configOptions.diagnosticRuleSet.reportUnusedImport = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedImport1.py'], configOptions);
TestUtils.validateResults(analysisResults, 2);
});
test('UnusedImport2', () => {
const configOptions = new ConfigOptions(Uri.empty());
// Disable it.
configOptions.diagnosticRuleSet.reportUnusedImport = 'none';
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedImport2.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Enable it as an error.
configOptions.diagnosticRuleSet.reportUnusedImport = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedImport2.py'], configOptions);
TestUtils.validateResults(analysisResults, 2);
});
test('UninitializedVariable1', () => {
const configOptions = new ConfigOptions(Uri.empty());
// By default, this is off.
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['uninitializedVariable1.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Enable it as an error.
configOptions.diagnosticRuleSet.reportUninitializedInstanceVariable = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['uninitializedVariable1.py'], configOptions);
TestUtils.validateResults(analysisResults, 3);
});
test('UninitializedVariable2', () => {
const configOptions = new ConfigOptions(Uri.empty());
// By default, this is off.
let analysisResults = TestUtils.typeAnalyzeSampleFiles(['uninitializedVariable2.py'], configOptions);
TestUtils.validateResults(analysisResults, 0);
// Enable it as an error.
configOptions.diagnosticRuleSet.reportUninitializedInstanceVariable = 'error';
analysisResults = TestUtils.typeAnalyzeSampleFiles(['uninitializedVariable2.py'], configOptions);
TestUtils.validateResults(analysisResults, 3);
});
test('UninitializedVariable3', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['uninitializedVariable3.py']);
TestUtils.validateResults(analysisResults, 0);
});
test('DeprecatedAlias1', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.defaultPythonVersion = pythonVersion3_8;
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_9;
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults2, 0, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_10;
const analysisResults3 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults3, 0, 0, 0, undefined, undefined, 0);
// Now enable the deprecateTypingAliases setting.
configOptions.diagnosticRuleSet.deprecateTypingAliases = true;
configOptions.defaultPythonVersion = pythonVersion3_8;
const analysisResults4 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults4, 0, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_9;
const analysisResults5 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults5, 0, 0, 0, undefined, undefined, 45);
configOptions.defaultPythonVersion = pythonVersion3_10;
const analysisResults6 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults6, 0, 0, 0, undefined, undefined, 49);
// Now change reportDeprecated to emit an error.
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
configOptions.defaultPythonVersion = pythonVersion3_8;
const analysisResults7 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults7, 0, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_9;
const analysisResults8 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults8, 45, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_10;
const analysisResults9 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias1.py'], configOptions);
TestUtils.validateResults(analysisResults9, 49, 0, 0, undefined, undefined, 0);
});
test('DeprecatedAlias2', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.defaultPythonVersion = pythonVersion3_8;
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_9;
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults2, 0, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_10;
const analysisResults3 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults3, 0, 0, 0, undefined, undefined, 0);
// Now enable the deprecateTypingAliases setting.
configOptions.diagnosticRuleSet.deprecateTypingAliases = true;
configOptions.defaultPythonVersion = pythonVersion3_8;
const analysisResults4 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults4, 0, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_9;
const analysisResults5 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults5, 0, 0, 0, undefined, undefined, 42);
configOptions.defaultPythonVersion = pythonVersion3_10;
const analysisResults6 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults6, 0, 0, 0, undefined, undefined, 46);
// Now change reportDeprecated to emit an error.
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
configOptions.defaultPythonVersion = pythonVersion3_8;
const analysisResults7 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults7, 0, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_9;
const analysisResults8 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults8, 42, 0, 0, undefined, undefined, 0);
configOptions.defaultPythonVersion = pythonVersion3_10;
const analysisResults9 = TestUtils.typeAnalyzeSampleFiles(['deprecatedAlias2.py'], configOptions);
TestUtils.validateResults(analysisResults9, 46, 0, 0, undefined, undefined, 0);
});
test('Deprecated2', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated2.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 14);
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated2.py'], configOptions);
TestUtils.validateResults(analysisResults2, 14);
});
test('Deprecated3', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated3.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 5);
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated3.py'], configOptions);
TestUtils.validateResults(analysisResults2, 5);
});
test('Deprecated4', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated4.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 7);
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated4.py'], configOptions);
TestUtils.validateResults(analysisResults2, 7);
});
test('Deprecated5', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated5.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 2);
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated5.py'], configOptions);
TestUtils.validateResults(analysisResults2, 2);
});
test('Deprecated6', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated6.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 3);
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated6.py'], configOptions);
TestUtils.validateResults(analysisResults2, 3);
});
test('Deprecated7', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated7.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 2);
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated7.py'], configOptions);
TestUtils.validateResults(analysisResults2, 2);
});
test('Deprecated8', () => {
const configOptions = new ConfigOptions(Uri.empty());
const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated8.py'], configOptions);
TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 4);
configOptions.diagnosticRuleSet.reportDeprecated = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['deprecated8.py'], configOptions);
TestUtils.validateResults(analysisResults2, 4);
});
@@ -1,198 +0,0 @@
/*
* classDeclaration.test.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*
* Test class detail's declaration
*/
import assert from 'assert';
import { isClassDeclaration, isSpecialBuiltInClassDeclaration } from '../analyzer/declaration';
import { getEnclosingFunction } from '../analyzer/parseTreeUtils';
import { isProperty } from '../analyzer/typeUtils';
import { TypeCategory, isClassInstance } from '../analyzer/types';
import { TextRange } from '../common/textRange';
import { ParseNodeType } from '../parser/parseNodes';
import { Range } from './harness/fourslash/fourSlashTypes';
import { TestState, getNodeAtMarker, parseAndGetTestState } from './harness/fourslash/testState';
test('regular class', () => {
const code = `
// @filename: test.py
//// [|class /*marker*/A:
//// pass|]
`;
checkClassDetail(code);
});
test('Meta class', () => {
const code = `
// @filename: test.py
//// [|class /*range*/MyMeta(type):
//// def __new__(cls, name, bases, dct):
//// return super().__new__(cls, name, bases, dct)|]
////
//// class MyClass(metaclass=MyMeta):
//// pass
////
//// /*marker*/E = MyMeta()
`;
checkClassDetail(code, '__class_MyMeta');
});
test('special built in class', () => {
const code = `
// @filename: test.py
//// from typing import TypedDict
//// def foo(t: /*marker*/TypedDict): ...
`;
checkSpecialBuiltInClassDetail(code);
});
test('dynamic enum', () => {
const code = `
// @filename: test.py
//// from enum import Enum
//// /*marker*/E = Enum('E', { 'One': 1 })
`;
checkNoDeclarationInClassDetail(code);
});
test('dynamic named tuple', () => {
const code = `
// @filename: test.py
//// from typing import NamedTuple
//// /*marker*/N = NamedTuple("N", [('name', str)])
`;
checkNoDeclarationInClassDetail(code);
});
test('dynamic typed dict', () => {
const code = `
// @filename: test.py
//// from typing import TypedDict
//// /*marker*/T = TypedDict("T", { "one": str })
`;
checkNoDeclarationInClassDetail(code);
});
test('dynamic new type', () => {
const code = `
// @filename: test.py
//// from typing import NewType
//// /*marker*/I = NewType('I', int)
`;
checkNoDeclarationInClassDetail(code);
});
test('dynamic type', () => {
const code = `
// @filename: test.py
//// /*marker*/D = type('D', (object,), {})
`;
checkNoDeclarationInClassDetail(code);
});
test('property', () => {
const code = `
// @filename: test.py
//// class MyClass:
//// def __init__(self):
//// self._v = None
////
//// @property
//// def /*getter*/value(self):
//// return self._v
////
//// @value.setter
//// def /*setter*/value(self, value):
//// self._v = value
////
//// @value.deleter
//// def /*deleter*/value(self):
//// del self._v
`;
const state = parseAndGetTestState(code).state;
['getter', 'setter', 'deleter'].forEach((marker) => {
const node = getNodeAtMarker(state, marker);
assert(node.nodeType === ParseNodeType.Name);
const functionNode = getEnclosingFunction(node);
assert(functionNode?.nodeType === ParseNodeType.Function);
const result = state.program.evaluator!.getTypeOfFunction(functionNode);
assert(result?.decoratedType);
assert(isProperty(result.decoratedType));
assert(isClassInstance(result.decoratedType));
assert(result.decoratedType.shared.declaration);
assert(isClassDeclaration(result.decoratedType.shared.declaration));
assert(result.decoratedType.shared.declaration.moduleName === 'builtins');
assert(result.decoratedType.shared.declaration.node.d.name.d.value === 'property');
});
});
function checkSpecialBuiltInClassDetail(code: string) {
const state = parseAndGetTestState(code).state;
const node = getNodeAtMarker(state);
assert(node.nodeType === ParseNodeType.Name);
const type = state.program.evaluator!.getType(node);
assert(type?.category === TypeCategory.Class);
assert.strictEqual(node.d.value, type.priv.aliasName ?? type.shared.name);
assert(type.shared.declaration);
if (type.priv.aliasName) {
assert(isClassDeclaration(type.shared.declaration));
} else {
assert(isSpecialBuiltInClassDeclaration(type.shared.declaration));
}
}
function checkNoDeclarationInClassDetail(code: string) {
const state = parseAndGetTestState(code).state;
_checkClassDetail(state, undefined);
}
function checkClassDetail(code: string, name?: string) {
const state = parseAndGetTestState(code).state;
_checkClassDetail(state, state.getRangeByMarkerName('marker') ?? state.getRangeByMarkerName('range'), name);
}
function _checkClassDetail(state: TestState, range: Range | undefined, name?: string) {
const node = getNodeAtMarker(state);
assert(node.nodeType === ParseNodeType.Name);
const type = state.program.evaluator!.getType(node);
assert(type?.category === TypeCategory.Class);
assert.strictEqual(name ?? node.d.value, type.priv.aliasName ?? type.shared.name);
if (range) {
assert(type.shared.declaration);
assert(isClassDeclaration(type.shared.declaration));
assert.deepStrictEqual(
TextRange.create(type.shared.declaration.node.start, type.shared.declaration.node.length),
TextRange.fromBounds(range.pos, range.end)
);
} else {
// There should be no decl.
assert(!type.shared.declaration);
}
}
File diff suppressed because it is too large. Load diff
@@ -55,7 +55,7 @@ describe(`config test'}`, () => {
const fileList = service.test_getFileNamesFromFileSpecs();
// The config file specifies a subdirectory, so we should find
// only two of the three "*.py" files present in the project
// only two of the three "*.vp" files present in the project
// directory.
assert.strictEqual(fileList.length, 2);
});
@@ -72,10 +72,10 @@ describe(`config test'}`, () => {
// The myVenv directory is detected as a venv and will be automatically excluded
const fileList = service.test_getFileNamesFromFileSpecs();
// There are 3 python files in the workspace, outside of myVenv
// There is 1 python file in myVenv, which should be excluded
// There are 3 Viper files in the workspace, outside of myVenv
// There is 1 Viper file in myVenv, which should be excluded
const fileNames = fileList.map((p) => p.fileName).sort();
assert.deepStrictEqual(fileNames, ['sample1.py', 'sample2.py', 'sample3.py']);
assert.deepStrictEqual(fileNames, ['sample1.vp', 'sample2.vp', 'sample3.vp']);
});
test('FindFilesVirtualEnvAutoDetectInclude', () => {
@@ -89,11 +89,11 @@ describe(`config test'}`, () => {
// Config file defines 'exclude' folder so virtual env will be included
const fileList = service.test_getFileNamesFromFileSpecs();
// There are 3 python files in the workspace, outside of myVenv
// There is 1 more python file in excluded folder
// There is 1 python file in myVenv, which should be included
// There are 3 Viper files in the workspace, outside of myVenv
// There is 1 more Viper file in excluded folder
// There is 1 Viper file in myVenv, which should be included
const fileNames = fileList.map((p) => p.fileName).sort();
assert.deepStrictEqual(fileNames, ['library1.py', 'sample1.py', 'sample2.py', 'sample3.py']);
assert.deepStrictEqual(fileNames, ['library1.vp', 'sample1.vp', 'sample2.vp', 'sample3.vp']);
});
test('FileSpecNotAnArray', () => {
@@ -416,7 +416,7 @@ describe(`config test'}`, () => {
const fileList = service.test_getFileNamesFromFileSpecs();
const fileNames = fileList.map((p) => p.fileName).sort();
assert.deepStrictEqual(fileNames, ['sample.pyi', 'test.py']);
assert.deepStrictEqual(fileNames, ['sample.vpi', 'test.vp']);
const configOptions = service.test_getConfigOptions(commandLineOptions);
assert.equal(configOptions.diagnosticRuleSet.strictListInference, true);
@@ -771,9 +771,9 @@ describe(`config test'}`, () => {
const configWithoutOverride = service.test_getConfigOptions(commandLineOptions);
assert.equal(configWithoutOverride.diagnosticRuleSet.reportPrivateImportUsage, 'none');
// The basic default would be 'error', verify our config overrides it.
// Viper 模式默认关闭 reportPrivateImportUsage(无 PEP 561 / py.typed 私有导入概念)。
const basicDefaults = getBasicDiagnosticRuleSet();
assert.equal(basicDefaults.reportPrivateImportUsage, 'error');
assert.equal(basicDefaults.reportPrivateImportUsage, 'none');
// Now simulate positional args overriding include (like `viper --project config.json subdir`).
const commandLineOptionsWithOverride = new CommandLineOptions(cwd, /* fromLanguageServer */ false);
@@ -68,7 +68,7 @@ describe('Diagnostic overrides', () => {
const json = JSON.parse(jsonString);
expect(json.contributes?.configuration?.properties).toBeDefined();
const overrides = json.contributes?.configuration?.properties['python.analysis.diagnosticSeverityOverrides'];
const overrides = json.contributes?.configuration?.properties['viper.analysis.diagnosticSeverityOverrides'];
expect(overrides).toBeDefined();
const props = overrides.properties;
expect(props).toBeDefined();
@@ -1,85 +0,0 @@
/*
* diagnostics.test.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*
* Unit tests for diagnostics
*/
import assert from 'assert';
import { DiagnosticRule } from '../common/diagnosticRules';
import { Range } from '../common/textRange';
import { parseAndGetTestState } from './harness/fourslash/testState';
// A range that spans the whole document so range intersection never filters diagnostics out.
const wholeFileRange: Range = {
start: { line: 0, character: 0 },
end: { line: 1_000_000, character: 0 },
};
test('unused import', async () => {
const code = `
// @filename: test1.py
//// from test2 import [|/*marker*/foo|]
// @filename: test2.py
//// def foo(): pass
`;
const state = parseAndGetTestState(code).state;
state.verifyDiagnostics({
marker: { category: 'unused', message: '"foo" is not accessed' },
});
});
test('viper ignore unused import', async () => {
const code = `
// @filename: test1.py
//// from test2 import [|/*marker*/foo|] # viper: ignore
// @filename: test2.py
//// def foo(): pass
`;
const state = parseAndGetTestState(code).state;
state.verifyDiagnostics({
marker: { category: 'none', message: '' },
});
});
test('getDiagnosticsForRangeWithoutFileIgnore analyzes the file on demand when its cache is empty', () => {
// Regression for the foreground/background Program split: in the product the published
// diagnostics are produced by the background-analysis Program, so the foreground Program
// that serves code-action requests may never have checked the file, leaving its pre-ignore
// diagnostics cache empty. The getter must populate that cache on demand.
const code = `
// @filename: viperconfig.json
//// {
//// "ignore": ["**/test.py"]
//// }
// @filename: test.py
//// /*marker*/undefinedSymbol
`;
const state = parseAndGetTestState(code).state;
const marker = state.getMarkerByName('marker');
state.openFile(marker.fileName);
// Intentionally do NOT analyze: this mirrors a foreground Program that has not checked the
// file. The file is ignored, so its published diagnostics are empty regardless.
assert.strictEqual(state.program.getDiagnosticsForRange(marker.fileUri, wholeFileRange).length, 0);
// The pre-ignore getter should still surface the unknown-symbol diagnostic by analyzing the
// file on demand. Without the on-demand analysis this returns [] and the quick fixes vanish.
const preIgnoreDiagnostics = state.program.getDiagnosticsForRangeWithoutFileIgnore(marker.fileUri, wholeFileRange);
assert(
preIgnoreDiagnostics.some((diag) => diag.getRule() === DiagnosticRule.reportUndefinedVariable),
`Expected a reportUndefinedVariable diagnostic, got ${JSON.stringify(
preIgnoreDiagnostics.map((diag) => ({ rule: diag.getRule(), message: diag.message }))
)}`
);
});
@@ -1,399 +0,0 @@
/*
* documentSymbolCollector.test.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*
* Tests documentSymbolCollector
*/
import { parseAndGetTestState } from './harness/fourslash/testState';
import { verifyReferencesAtPosition } from './testStateUtils';
test('folder reference', () => {
const code = `
// @filename: common/__init__.py
//// from [|io2|] import tools as tools
//// from [|io2|].tools import pathUtils as pathUtils
// @filename: io2/empty.py
//// # empty
// @filename: io2/tools/__init__.py
//// def combine(a, b):
//// pass
// @filename: io2/tools/pathUtils.py
//// def getFilename(path):
//// pass
// @filename: test1.py
//// from common import *
////
//// tools.combine(1, 1)
//// pathUtils.getFilename("c")
// @filename: test2.py
//// from .[|io2|] import tools as t
////
//// t.combine(1, 1)
// @filename: test3.py
//// from .[|io2|].tools import pathUtils as p
////
//// p.getFilename("c")
// @filename: test4.py
//// from common import tools, pathUtils
////
//// tools.combine(1, 1)
//// pathUtils.getFilename("c")
// @filename: test5.py
//// from [|io2|] import tools as tools
//// from [|io2|].tools import pathUtils as pathUtils
////
//// tools.combine(1, 1)
//// pathUtils.getFilename("c")
`;
const state = parseAndGetTestState(code).state;
const ranges = state.getRangesByText().get('io2')!;
for (const range of ranges) {
verifyReferencesAtPosition(state.program, state.configOptions, 'io2', range.fileName, range.pos, ranges);
}
});
test('__init__ wildcard import', () => {
const code = `
// @filename: common/__init__.py
//// from io2 import [|tools|] as [|tools|]
//// from io2.[|tools|] import pathUtils as pathUtils
// @filename: io2/empty.py
//// # empty
// @filename: io2/tools/__init__.py
//// def combine(a, b):
//// pass
// @filename: io2/tools/pathUtils.py
//// def getFilename(path):
//// pass
// @filename: test1.py
//// from common import *
////
//// [|tools|].combine(1, 1)
//// pathUtils.getFilename("c")
// @filename: test2.py
//// from .io2 import [|tools|] as t
////
//// t.combine(1, 1)
// @filename: test3.py
//// from .io2.[|tools|] import pathUtils as p
////
//// p.getFilename("c")
// @filename: test4.py
//// from common import [|tools|], pathUtils
////
//// [|tools|].combine(1, 1)
//// pathUtils.getFilename("c")
// @filename: test5.py
//// from io2 import [|tools|] as [|tools|]
//// from io2.[|tools|] import pathUtils as pathUtils
////
//// [|tools|].combine(1, 1)
//// pathUtils.getFilename("c")
`;
const state = parseAndGetTestState(code).state;
const ranges = state.getRangesByText().get('tools')!;
for (const range of ranges) {
verifyReferencesAtPosition(state.program, state.configOptions, 'tools', range.fileName, range.pos, ranges);
}
});
test('submodule wildcard import', () => {
const code = `
// @filename: common/__init__.py
//// from io2 import tools as tools
//// from io2.tools import [|pathUtils|] as [|pathUtils|]
// @filename: io2/empty.py
//// # empty
// @filename: io2/tools/__init__.py
//// def combine(a, b):
//// pass
// @filename: io2/tools/pathUtils.py
//// def getFilename(path):
//// pass
// @filename: test1.py
//// from common import *
////
//// tools.combine(1, 1)
//// [|pathUtils|].getFilename("c")
// @filename: test2.py
//// from .io2 import tools as t
////
//// t.combine(1, 1)
// @filename: test3.py
//// from .io2.tools import [|pathUtils|] as p
////
//// p.getFilename("c")
// @filename: test4.py
//// from common import tools, [|pathUtils|]
////
//// tools.combine(1, 1)
//// [|pathUtils|].getFilename("c")
// @filename: test5.py
//// from io2 import tools as tools
//// from io2.tools import [|pathUtils|] as [|pathUtils|]
////
//// tools.combine(1, 1)
//// [|pathUtils|].getFilename("c")
`;
const state = parseAndGetTestState(code).state;
const ranges = state.getRangesByText().get('pathUtils')!;
for (const range of ranges) {
verifyReferencesAtPosition(state.program, state.configOptions, 'pathUtils', range.fileName, range.pos, ranges);
}
});
test('use localName import alias', () => {
const code = `
// @filename: common/__init__.py
//// from io2 import tools as [|/*marker1*/tools|]
//// from io2.tools import pathUtils as pathUtils
// @filename: io2/empty.py
//// # empty
// @filename: io2/tools/__init__.py
//// def combine(a, b):
//// pass
// @filename: io2/tools/pathUtils.py
//// def getFilename(path):
//// pass
// @filename: test1.py
//// from common import *
////
//// [|/*marker2*/tools|].combine(1, 1)
//// pathUtils.getFilename("c")
// @filename: test2.py
//// from .io2 import tools as t
////
//// t.combine(1, 1)
// @filename: test3.py
//// from .io2.tools import pathUtils as p
////
//// p.getFilename("c")
// @filename: test4.py
//// from common import [|/*marker3*/tools|], pathUtils
////
//// [|/*marker4*/tools|].combine(1, 1)
//// pathUtils.getFilename("c")
// @filename: test5.py
//// from io2 import tools as [|/*marker5*/tools|]
//// from io2.tools import pathUtils as pathUtils
////
//// [|/*marker6*/tools|].combine(1, 1)
//// pathUtils.getFilename("c")
`;
const state = parseAndGetTestState(code).state;
const references = state
.getRangesByText()
.get('tools')!
.map((r) => ({ uri: r.fileUri, range: state.convertPositionRange(r) }));
state.verifyFindAllReferences({
marker1: { references },
marker2: { references },
marker3: { references },
marker4: { references },
marker5: { references },
marker6: { references },
});
});
test('use localName import module', () => {
const code = `
// @filename: common/__init__.py
//// from io2 import [|/*marker1*/tools|] as [|tools|]
//// from io2.[|/*marker2*/tools|] import pathUtils as pathUtils
// @filename: io2/empty.py
//// # empty
// @filename: io2/tools/__init__.py
//// def combine(a, b):
//// pass
// @filename: io2/tools/pathUtils.py
//// def getFilename(path):
//// pass
// @filename: test1.py
//// from common import *
////
//// [|tools|].combine(1, 1)
//// pathUtils.getFilename("c")
// @filename: test2.py
//// from .io2 import [|/*marker3*/tools|] as t
////
//// t.combine(1, 1)
// @filename: test3.py
//// from .io2.[|/*marker4*/tools|] import pathUtils as p
////
//// p.getFilename("c")
// @filename: test4.py
//// from common import [|tools|], pathUtils
////
//// [|tools|].combine(1, 1)
//// pathUtils.getFilename("c")
// @filename: test5.py
//// from io2 import [|/*marker5*/tools|] as [|tools|]
//// from io2.[|/*marker6*/tools|] import pathUtils as pathUtils
////
//// [|tools|].combine(1, 1)
//// pathUtils.getFilename("c")
`;
const state = parseAndGetTestState(code).state;
const references = state
.getRangesByText()
.get('tools')!
.map((r) => ({ uri: r.fileUri, range: state.convertPositionRange(r) }));
state.verifyFindAllReferences({
marker1: { references },
marker2: { references },
marker3: { references },
marker4: { references },
marker5: { references },
marker6: { references },
});
});
test('import dotted name', () => {
const code = `
// @filename: nest1/__init__.py
//// # empty
// @filename: nest1/nest2/__init__.py
//// # empty
// @filename: nest1/nest2/module.py
//// def foo():
//// pass
// @filename: test1.py
//// import [|nest1|].[|nest2|].[|module|]
////
//// [|nest1|].[|nest2|].[|module|]
// @filename: nest1/test2.py
//// import [|nest1|].[|nest2|].[|module|]
////
//// [|nest1|].[|nest2|].[|module|]
`;
const state = parseAndGetTestState(code).state;
function verify(name: string) {
const ranges = state.getRangesByText().get(name)!;
for (const range of ranges) {
verifyReferencesAtPosition(state.program, state.configOptions, name, range.fileName, range.pos, ranges);
}
}
verify('nest1');
verify('nest2');
verify('module');
});
test('import alias', () => {
const code = `
// @filename: nest/__init__.py
//// # empty
// @filename: nest/module2.py
//// # empty
// @filename: module1.py
//// # empty
// @filename: test1.py
//// import [|/*marker1*/module1|] as [|module1|]
// @filename: test2.py
//// import nest.[|/*marker2*/module2|] as [|module2|]
`;
const state = parseAndGetTestState(code).state;
const marker1 = state.getMarkerByName('marker1');
const ranges1 = state.getRangesByText().get('module1')!;
verifyReferencesAtPosition(
state.program,
state.configOptions,
'module1',
marker1.fileName,
marker1.position,
ranges1
);
const marker2 = state.getMarkerByName('marker2');
const ranges2 = state.getRangesByText().get('module2')!;
verifyReferencesAtPosition(
state.program,
state.configOptions,
'module2',
marker2.fileName,
marker2.position,
ranges2
);
});
test('string in __all__', () => {
const code = `
// @filename: test1.py
//// class [|/*marker1*/A|]:
//// pass
////
//// a: "[|A|]" = "A"
////
//// __all__ = [ "[|A|]" ]
`;
const state = parseAndGetTestState(code).state;
const marker1 = state.getMarkerByName('marker1');
const ranges1 = state.getRangesByText().get('A')!;
verifyReferencesAtPosition(state.program, state.configOptions, 'A', marker1.fileName, marker1.position, ranges1);
});
Loaded 100 of 1728 files, more files were not shown because too many files have changed in this diff. Show more