Compare commits
70
Commits
Vipeye_1.1.412
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5463fac3d2 | ||
|
|
9e0d12553d | ||
|
|
065ab18231 | ||
|
|
4c30604ecd | ||
|
|
3fd61c2594 | ||
|
|
b8101947cf | ||
|
|
bc5bd3a425 | ||
|
|
f31ff4904c | ||
|
|
bc500d060f | ||
|
|
d91fc2d67e | ||
|
|
c7878ab9d7 | ||
|
|
71cb333549 | ||
|
|
b4808c2821 | ||
|
|
f234e38701 | ||
|
|
0bc7a7c4d4 | ||
|
|
c8817cacb1 | ||
|
|
5808a76b23 | ||
|
|
2a7757e3b7 | ||
|
|
a7a00a23d5 | ||
|
|
90f4118603 | ||
|
|
4d5a0d203b | ||
|
|
baec16e540 | ||
|
|
b98102b78f | ||
|
|
aa1c84a326 | ||
|
|
e7cf7643de | ||
|
|
cef653b637 | ||
|
|
36dab6433b | ||
|
|
988ba922f4 | ||
|
|
d2f66811fb | ||
|
|
1aa1b1f2f0 | ||
|
|
00b781a5cc | ||
|
|
4bc462e0f3 | ||
|
|
f547737cf0 | ||
|
|
de2d49cc4a | ||
|
|
86fc6c541f | ||
|
|
a7844d359f | ||
|
|
91f668a033 | ||
|
|
d9559869e7 | ||
|
|
a6299ab878 | ||
|
|
f100f585d0 | ||
|
|
525a654f5c | ||
|
|
70a4405294 | ||
|
|
95a0dbf0ad | ||
|
|
ad3fd9810c | ||
|
|
26216889de | ||
|
|
5d6fcf7c82 | ||
|
|
b0f4b8b5fd | ||
|
|
640d7e4a07 | ||
|
|
cfccbac6f4 | ||
|
|
c9acd4ecb8 | ||
|
|
04d5a32502 | ||
|
|
a955a5356f | ||
|
|
e4635eca37 | ||
|
|
d5398ab4e2 | ||
|
|
86e858667e | ||
|
|
d9b5015c71 | ||
|
|
aef18c2891 | ||
|
|
23cc75e444 | ||
|
|
ac08b01baa | ||
|
|
41bc302916 | ||
|
|
bbd8b1022e | ||
|
|
817bc47ee9 | ||
|
|
e5aacbfd0b | ||
|
|
9c816ec5b9 | ||
|
|
4182a1fd53 | ||
|
|
f4e19ed72a | ||
|
|
ad3eecb077 | ||
|
|
a20c643b72 | ||
|
|
2b752a7aec | ||
|
|
5e07800c7c |
No files matched your search
+37
@@ -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/
|
||||
|
||||
|
||||
Vendored
+4
-2
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -1,10 +1,52 @@
|
||||

|
||||

|
||||
|
||||
# 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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 '未启用';
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)}%)`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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>`;
|
||||
}
|
||||
@@ -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 '未启用';
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in new issue
Block a user