基本对齐了现阶段 Viper 的所有必要功能,但还缺少反向测试

This commit is contained in:
TermiNexus committed 2026-08-05 18:15:53 +08:00
1 parent b6692ee800
commit de1160be61
13 files changed
+678 -21

No files matched your search

View File
File renamed without changes.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File
File renamed without changes.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

+1 -1
View File
@@ -2,7 +2,7 @@
"name": "vipeye-internal",
"displayName": "vipeye",
"description": "Type checker for the Viper language",
"version": "1.1.411",
"version": "1.1.412",
"license": "MIT",
"private": true,
"files": [
@@ -9,6 +9,7 @@
*/
import { appendArray } from '../common/collectionUtils';
import { IsViperDialect } from '../common/dialect';
import { ArgCategory, CallNode, DecoratorNode, FunctionNode, ParamCategory, ParseNodeType } from '../parser/parseNodes';
import { getDeclaration, getFileInfo } from './analyzerNodeInfo';
import {
@@ -412,6 +413,22 @@ function getTypeOfDecorator(evaluator: TypeEvaluator, node: DecoratorNode, funct
const decoratorTypeResult = evaluator.getTypeOfExpression(node.d.expr, flags);
// Viper 扩展:t.CVTable / t.Object / t.NoVTable 是类型透传装饰器
// (签名 `(cls: Type[T]) -> Type[T]`)。在 Viper 方言下,泛型 `Type[T]`
// 的参数绑定在 stub 求值中可能退化为裸 `Type`,导致被装饰类被解析为
// `Type` 而非 `type[Class]`,进而丢失全部成员(如 __enter__/__exit__)。
// 这些装饰器语义上就是"返回原类",因此直接透传输入类型。
if (IsViperDialect(getFileInfo(node).executionEnvironment.dialect)) {
const decExpr = node.d.expr as any;
const decName =
decExpr?.nodeType === 'Call'
? decExpr?.d?.leftExpr?.d?.member?.d?.value ?? decExpr?.d?.leftExpr?.d?.value
: decExpr?.d?.member?.d?.value ?? decExpr?.d?.value;
if (decName === 'CVTable' || decName === 'Object' || decName === 'NoVTable') {
return functionOrClassType;
}
}
// Special-case the combination of a classmethod decorator applied
// to a property. This is allowed in Python 3.9, but it's not reflected
// in the builtins.pyi stub for classmethod.
@@ -31,6 +31,7 @@ import {
import { CodeFlowReferenceExpressionNode } from './codeFlowTypes';
import { addConstraintsForExpectedType } from './constraintSolver';
import { ConstraintTracker } from './constraintTracker';
import { DeclarationType } from './declaration';
import { getTypeVarScopesForNode, isMatchingExpression } from './parseTreeUtils';
import { getTypedDictMembersForClass } from './typedDicts';
import { EvalFlags, TypeEvaluator, TypeResult } from './typeEvaluatorTypes';
@@ -54,6 +55,9 @@ import {
isAnyOrUnknown,
isClass,
isClassInstance,
isCPtrType,
isCTypeSubclassType,
isCVoidType,
isInstantiableClass,
isNever,
isSameWithoutLiteralValue,
@@ -664,6 +668,42 @@ function getPositionalMatchArgNames(evaluator: TypeEvaluator, type: ClassType):
}
}
// Viper 扩展:REnum 变体类的 match 位置参数。
// Viper 的 REnum(t.REnum)嵌套类变体(如 class LLVMType 内的 class Ptr)
// 没有 __match_args__,但 match 模式 `LLVMType.Ptr(pointee)` 允许按声明顺序
// 解构其类型注解字段。Pyright 默认对无 __match_args__ 的类只允许 0 个位置参数,
// 会把 `LLVMType.Ptr(pointee)` 误报为"位置模式太多"。
// 此处返回类中所有类型注解字段名作为位置参数名。
// 判断依据:类自身是 CType 子类(CPtr/CVoid),或声明在 CType 子类内部
// (REnum 的嵌套类变体)。
const isNestedInCType =
(() => {
const decl = type.shared.declaration;
const classNode = decl?.type === DeclarationType.Class ? decl.node : undefined;
if (!classNode) {
return false;
}
// 嵌套类的直接父节点是外层类的 Suite(nodeType=50),
// Suite 的父节点才是外层 ClassNode(nodeType=10)。
const parentNode = classNode.parent;
const grandparentNode =
parentNode?.nodeType === ParseNodeType.Suite ? parentNode.parent : undefined;
return grandparentNode?.nodeType === ParseNodeType.Class;
})();
if (isCTypeSubclassType(type) || isCPtrType(type) || isCVoidType(type) || isNestedInCType) {
const fieldNames: string[] = [];
const fields = type.shared.fields;
if (fields) {
for (const [name, symbol] of fields) {
const decls = symbol.getDeclarations();
if (decls.some((decl) => decl.type === DeclarationType.Variable)) {
fieldNames.push(name);
}
}
}
return fieldNames;
}
return [];
}
@@ -635,6 +635,7 @@ export function getOffDiagnosticRuleSet(dialect?: LanguageDialect): DiagnosticRu
diagSettings.reportDeprecated = 'none';
diagSettings.reportMissingTypeStubs = 'none';
diagSettings.reportMissingModuleSource = 'none';
diagSettings.reportMissingImports = 'warning';
diagSettings.reportTypeCommentUsage = 'none';
diagSettings.reportIncompleteStub = 'none';
diagSettings.reportInvalidStubStatement = 'none';
@@ -742,7 +743,7 @@ export function getBasicDiagnosticRuleSet(dialect?: LanguageDialect): Diagnostic
reportImplicitStringConcatenation: 'none',
reportUnboundVariable: 'error',
reportUnhashable: 'error',
reportUndefinedVariable: 'error',
reportUndefinedVariable: 'warning',
reportInvalidStubStatement: 'none',
reportIncompleteStub: 'none',
reportUnsupportedDunderAll: 'warning',
@@ -867,7 +868,7 @@ export function getStandardDiagnosticRuleSet(dialect?: LanguageDialect): Diagnos
reportImplicitStringConcatenation: 'none',
reportUnboundVariable: 'error',
reportUnhashable: 'error',
reportUndefinedVariable: 'error',
reportUndefinedVariable: 'warning',
reportInvalidStubStatement: 'none',
reportIncompleteStub: 'none',
reportUnsupportedDunderAll: 'warning',
@@ -885,6 +886,7 @@ export function getStandardDiagnosticRuleSet(dialect?: LanguageDialect): Diagnos
diagSettings.reportDeprecated = 'none';
diagSettings.reportMissingTypeStubs = 'none';
diagSettings.reportMissingModuleSource = 'none';
diagSettings.reportMissingImports = 'warning';
diagSettings.reportTypeCommentUsage = 'none';
diagSettings.reportIncompleteStub = 'none';
diagSettings.reportInvalidStubStatement = 'none';
@@ -995,7 +997,7 @@ export function getStrictDiagnosticRuleSet(dialect?: LanguageDialect): Diagnosti
reportImplicitStringConcatenation: 'none',
reportUnboundVariable: 'error',
reportUnhashable: 'error',
reportUndefinedVariable: 'error',
reportUndefinedVariable: 'warning',
reportInvalidStubStatement: 'error',
reportIncompleteStub: 'error',
reportUnsupportedDunderAll: 'error',
@@ -1013,6 +1015,7 @@ export function getStrictDiagnosticRuleSet(dialect?: LanguageDialect): Diagnosti
diagSettings.reportDeprecated = 'none';
diagSettings.reportMissingTypeStubs = 'none';
diagSettings.reportMissingModuleSource = 'none';
diagSettings.reportMissingImports = 'warning';
diagSettings.reportTypeCommentUsage = 'none';
diagSettings.reportIncompleteStub = 'none';
diagSettings.reportInvalidStubStatement = 'none';
@@ -14,10 +14,11 @@
* - 基础类型分类:class/function/parameter/namespace
*/
import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo';
import { Declaration, DeclarationType, VariableDeclaration } from '../analyzer/declaration';
import { ParseTreeWalker } from '../analyzer/parseTreeWalker';
import { isWithinTypeAnnotation } from '../analyzer/parseTreeUtils';
import { TypeCategory } from '../analyzer/types';
import { TypeCategory, isCDefineType } from '../analyzer/types';
import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes';
import { LogTracker } from '../common/logTracker';
import { ProgramView } from '../common/extensibility';
@@ -468,9 +469,14 @@ class SemanticTokenWalker extends ParseTreeWalker {
return true;
}
/** 访问 ModuleNameNode:import 语句中的模块名路径部分 → namespace。
/** 访问 ModuleNameNode:import 语句中的模块名路径部分。
* 解析成功的模块 → namespace(绿色);未解析的模块(如不存在的
* `from .visitor import ...`)→ undefined(白色,与诊断一致)。
* 返回 false 阻止遍历 nameParts(避免重复 token,因 visitName 也会处理) */
override visitModuleName(node: ModuleNameNode): boolean {
const importInfo = AnalyzerNodeInfo.getImportInfo(node);
// 模块整体未解析:每个 namePart 都视为未定义(白色)
const isUnresolved = !!importInfo && !importInfo.isImportFound && !importInfo.isNativeLib;
for (const part of node.d.nameParts) {
const position = convertOffsetToPosition(part.start, this._lines);
if (position && part.length > 0) {
@@ -478,7 +484,9 @@ class SemanticTokenWalker extends ParseTreeWalker {
line: position.line,
startChar: position.character,
length: part.length,
tokenType: SemanticTokenTypes.namespace,
tokenType: isUnresolved
? SemanticTokenTypes.undefined
: SemanticTokenTypes.namespace,
tokenModifiers: 0,
});
}
@@ -634,8 +642,43 @@ class SemanticTokenWalker extends ParseTreeWalker {
}
break;
case DeclarationType.Alias:
// import 的模块别名
tokenType = SemanticTokenTypes.namespace;
// import 的模块别名。按别名实际指向的类型分类:
// `from .base import AST` 中 AST 实际是类 → class;
// `import os.path` 中 os 是模块 → namespace。
// 避免"无论实际上是什么都显示成绿色(namespace)"。
if (type) {
if (type.category === TypeCategory.Unknown ||
type.category === TypeCategory.Any ||
type.category === TypeCategory.Unbound) {
// 未解析的导入(如 `from .visitor import ASTVisitor`,
// visitor 模块不存在):标 undefined(白色),与诊断一致。
tokenType = SemanticTokenTypes.undefined;
} else if (isCDefineType(type)) {
// t.CDefine 常量(如 FLAG_IS_ASYNC: t.CDefine = 1):
// 编译时元指令,按常量显示(variable + readonly)。
tokenType = SemanticTokenTypes.variable;
tokenModifiers |= SemanticTokenModifiers.readonly;
} else if (type.category === TypeCategory.Class) {
tokenType = BUILTIN_CLASS_NAMES.has(nameText)
? SemanticTokenTypes.class
: (BUILTIN_TYPE_NAMES.has(nameText)
? SemanticTokenTypes.builtin
: SemanticTokenTypes.class);
} else if (
type.category === TypeCategory.Function ||
type.category === TypeCategory.Overloaded
) {
tokenType = SemanticTokenTypes.function;
} else if (type.category === TypeCategory.Module) {
tokenType = SemanticTokenTypes.namespace;
} else if (type.category === TypeCategory.TypeVar) {
tokenType = SemanticTokenTypes.type;
} else {
tokenType = SemanticTokenTypes.variable;
}
} else {
tokenType = SemanticTokenTypes.namespace;
}
tokenModifiers |= defMod;
break;
case DeclarationType.TypeAlias:
@@ -0,0 +1,113 @@
/*
* viperUnionMemberAccess.test.ts
*
* 验证 Viper 扩展:联合类型 `MemBuddy | CPtr` 的变量访问成员方法(如 realloc)
* 不应报 "Cannot access attribute for class UnionType"。
*
* 场景:TransPyC/Test/BuddyTest/App/mbuddytest.vp
* bd3: memhub.MemBuddy | CPtr = memhub.MemBuddy(arena3, 4096)
* rp2: bytes = bd3.realloc(rp, 128)
*
* 同时覆盖 `with memhub.MemBuddy(...) as wbd:` 上下文管理器场景:
* - @t.CVTable 装饰的 MemManager 类必须解析为 type[MemManager] 而非 Type
* - __enter__ 返回 'MemManager' | t.CPtr 折叠为 CPtr[MemManager]
* - wbd.alloc / bd3.realloc 成员访问不报 Unknown
*
* 运行:npx jest src/tests/viperUnionMemberAccess.test.ts --runInBand
*/
import assert from 'assert';
import * as path from 'path';
import { ImportResolver } from '../analyzer/importResolver';
import { Program } from '../analyzer/program';
import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions';
import { NullConsole } from '../common/console';
import { RealTempFile, createFromRealFileSystem } from '../common/realFileSystem';
import { createServiceProvider } from '../common/serviceProviderExtensions';
import { UriEx } from '../common/uri/uriUtils';
import { FullAccessHost } from '../common/fullAccessHost';
// 允许 vipershed-fallback(t.vpi/c.vpi 存根)被定位。
(global as any).__rootDirectory = path.resolve();
const BUDDY_TEST = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\Test\\BuddyTest\\App\\mbuddytest.vp';
const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes';
function buildProgram(targetPath: string) {
const tempFile = new RealTempFile();
const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile);
const projectRoot = UriEx.file(TRANSPYC_INCLUDES);
const configOptions = new ConfigOptions(projectRoot);
configOptions.internalTestMode = true;
configOptions.initializeTypeCheckingMode('standard');
const execEnv = new ExecutionEnvironment(
'python',
projectRoot,
configOptions.diagnosticRuleSet,
configOptions.defaultPythonVersion,
configOptions.defaultPythonPlatform,
[projectRoot],
/* skipNativeLibraries */ false,
/* defaultDialect */ 'viper'
);
configOptions.executionEnvironments = [execEnv];
const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider));
const program = new Program(importResolver, configOptions, serviceProvider);
const uri = UriEx.file(targetPath);
program.setTrackedFiles([uri]);
while (program.analyze()) {
// 循环直到分析完成
}
return { program, serviceProvider, uri, configOptions };
}
function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): any[] {
const sourceFile = program.getSourceFile(uri);
assert(sourceFile, 'source file not found');
return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => ({
category: diag.category,
message: diag.message,
range: diag.range ? `L${diag.range.start.line + 1}` : '(no range)',
}));
}
test('BuddyTest 联合类型变量访问 realloc/alloc 成员不报 UnionType/Unknown 错误', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(BUDDY_TEST);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 断言:联合类型 `MemBuddy | CPtr` 的成员访问(realloc/alloc/__enter__/__exit__)无错误。
const unionErrors = diagnostics.filter(
(d) =>
d.message.includes('UnionType') ||
d.message.includes('realloc') ||
d.message.includes('alloc') ||
d.message.includes('__enter__') ||
d.message.includes('__exit__') ||
d.message.includes('不能与 "with" 一起使用')
);
assert.strictEqual(
unionErrors.length,
0,
`unexpected union/member access errors: ${JSON.stringify(unionErrors)}`
);
// 排除合理的既有告警(未使用的 import/变量),不应有 category=0/1 的其它错误。
// 唯一允许的已知源码问题:mbuddytest.vp L209 引用了 MemBuddy 不存在的属性 "mem"
// (应为 base,属于 TransPyC 测试源码自身的问题,LSP 报错是正确的)。
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
const knownSourceIssue = hardErrors.filter((d) => d.range === 'L209' && d.message.includes('mem'));
assert.strictEqual(
hardErrors.length,
knownSourceIssue.length,
`unexpected hard errors: ${JSON.stringify(hardErrors)}`
);
program.dispose();
serviceProvider.dispose();
});
@@ -0,0 +1,399 @@
/*
* viperVariadicArgs.test.ts
*
* 验证 Viper 扩展:`*args` 在函数体内应绑定为 `va_list`(指针),而非 Python 语义的 tuple。
*
* 场景:TransPyC/includes/viperlib.vp
* def sprintf(buf: t.CChar | t.CPtr, rule: str, *args) -> t.CVoid | t.CExport:
* if buf is None or rule is None: return
* vsnprintf(buf, 0x7FFFFFFF, rule, args) # 不应报 tuple 不能赋值给 CPtr[CChar]
*
* 同时覆盖 vsprintf / snprintf 内部将 `args` 透传给 vsnprintf 的场景。
*
* 运行:npx jest src/tests/viperVariadicArgs.test.ts --runInBand
*/
import assert from 'assert';
import * as path from 'path';
import { ImportResolver } from '../analyzer/importResolver';
import { Program } from '../analyzer/program';
import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions';
import { NullConsole } from '../common/console';
import { RealTempFile, createFromRealFileSystem } from '../common/realFileSystem';
import { createServiceProvider } from '../common/serviceProviderExtensions';
import { UriEx } from '../common/uri/uriUtils';
import { FullAccessHost } from '../common/fullAccessHost';
// 允许 vipershed-fallback(t.vpi/c.vpi 存根)被定位。
(global as any).__rootDirectory = path.resolve();
const VIPERLIB_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\viperlib.vp';
const MEMHUB_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\memhub.vp';
const REQUESTS_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\requests.vp';
const ARGPARSE_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\argparse.vp';
const TYPES_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\llvmlite\\__types.vp';
const OS_INIT_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\os\\__init__.vp';
const SHA256_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\hashlib\\__sha256.vp';
const SHA512_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\hashlib\\__sha512.vp';
const AST_INIT_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\ast\\__init__.vp';
const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes';
function buildProgram(targetPath: string) {
const tempFile = new RealTempFile();
const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile);
const projectRoot = UriEx.file(TRANSPYC_INCLUDES);
const configOptions = new ConfigOptions(projectRoot);
configOptions.internalTestMode = true;
configOptions.initializeTypeCheckingMode('standard');
const execEnv = new ExecutionEnvironment(
'python',
projectRoot,
configOptions.diagnosticRuleSet,
configOptions.defaultPythonVersion,
configOptions.defaultPythonPlatform,
[projectRoot],
/* skipNativeLibraries */ false,
/* defaultDialect */ 'viper'
);
configOptions.executionEnvironments = [execEnv];
const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider));
const program = new Program(importResolver, configOptions, serviceProvider);
const uri = UriEx.file(targetPath);
program.setTrackedFiles([uri]);
while (program.analyze()) {
// 循环直到分析完成
}
return { program, serviceProvider, uri, configOptions };
}
function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): any[] {
const sourceFile = program.getSourceFile(uri);
assert(sourceFile, 'source file not found');
return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => ({
category: diag.category,
message: diag.message,
range: diag.range ? `L${diag.range.start.line + 1}` : '(no range)',
}));
}
test('viperlib.vp 中 *args 透传 vsnprintf 不报 tuple 类型错误', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(VIPERLIB_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 断言:sprintf/vsprintf/snprintf 内部的 `args` 透传(vsnprintf 的 ap 参数)不应报
// tuple 类型不能赋值给 CPtr 的错误。
const variadicErrors = diagnostics.filter(
(d) =>
d.message.includes('tuple') ||
d.message.includes('vsnprintf') ||
d.message.includes('cannot be assigned to parameter "ap"') ||
d.message.includes('参数 "ap"')
);
assert.strictEqual(
variadicErrors.length,
0,
`unexpected variadic arg errors: ${JSON.stringify(variadicErrors)}`
);
// va_arg / c.Deref 的误报(stdarg.vp 声明与 Viper 语义不符)也应消除。
const derefVaArgErrors = diagnostics.filter(
(d) => d.message.includes('va_arg') || d.message.includes('Deref')
);
assert.strictEqual(
derefVaArgErrors.length,
0,
`unexpected va_arg/Deref errors: ${JSON.stringify(derefVaArgErrors)}`
);
// 不应有任何 category=0/1 的错误。
// 唯一允许的已知源码问题:viperlib.vp L13 `return count1` 引用不存在的变量
// (应为 `return count`,属于 TransPyC 源码自身的拼写错误,LSP 报错正确)。
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
const knownSourceIssue = hardErrors.filter(
(d) => d.range === 'L13' && (d.message.includes('count1') || d.message.includes('未定义'))
);
assert.strictEqual(
hardErrors.length,
knownSourceIssue.length,
`unexpected hard errors: ${JSON.stringify(hardErrors)}`
);
program.dispose();
serviceProvider.dispose();
});
test('memhub.vp 中 viperio.Buf.__sizeof__() 不报 Argument missing for parameter "self"', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(MEMHUB_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// __sizeof__ 是 Viper 的类方法(sizeof(Type) -> int),不需要 self 参数。
// 命中场景:memhub.vp L109 `self.alloc(viperio.Buf.__sizeof__())`。
const sizeOfErrors = diagnostics.filter(
(d) => d.message.includes('__sizeof__') || d.message.includes('parameter "self"')
);
assert.strictEqual(
sizeOfErrors.length,
0,
`unexpected __sizeof__ errors: ${JSON.stringify(sizeOfErrors)}`
);
// 不应有任何 category=0/1 的错误。
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
assert.strictEqual(hardErrors.length, 0, `unexpected hard errors: ${JSON.stringify(hardErrors)}`);
program.dispose();
serviceProvider.dispose();
});
test('argparse.vp 中 str = None 参数默认值不报类型不兼容(回归)', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(ARGPARSE_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 命中场景:argparse.vp L155-158 `short: str = None` / `help: str = None`。
// Viper 下 None 是空指针,str 是 i8*,`str = None` 合法且不应展开为 str | None。
const strNoneErrors = diagnostics.filter(
(d) => d.message.includes('str') && (d.message.includes('None') || d.message.includes('default'))
);
assert.strictEqual(
strNoneErrors.length,
0,
`unexpected str=None errors: ${JSON.stringify(strNoneErrors)}`
);
// 不应有任何 category=0/1 的错误。
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
assert.strictEqual(hardErrors.length, 0, `unexpected hard errors: ${JSON.stringify(hardErrors)}`);
program.dispose();
serviceProvider.dispose();
});
test('__types.vp 嵌套类成员 Pointee: LLVMType | t.CPtr 自我引用不被标为 Unknown', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(TYPES_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 命中场景:llvmlite/__types.vp 的 class LLVMType(t.REnum) 中,
// class Ptr 的成员 Pointee: LLVMType | t.CPtr 引用外层类 LLVMType。
// 嵌套类成员注解引用外层类名(自我指定)应解析为类类型而非 Unknown。
const unknownErrors = diagnostics.filter(
(d) => d.message.includes('Unknown') && d.message.includes('LLVMType')
);
assert.strictEqual(
unknownErrors.length,
0,
`unexpected Unknown errors for nested self-ref: ${JSON.stringify(unknownErrors)}`
);
// 不应有 TypePrint 参数不匹配(Struct | Unknown)错误。
const typePrintErrors = diagnostics.filter(
(d) => d.message.includes('TypePrint') && d.message.includes('Struct')
);
assert.strictEqual(
typePrintErrors.length,
0,
`unexpected TypePrint errors: ${JSON.stringify(typePrintErrors)}`
);
// REnum 变体构造(LLVMType.Int(1) / LLVMType.Struct(f, c, n))不报位置参数错误。
const variantCtorErrors = diagnostics.filter(
(d) => d.message.includes('位置参数') && d.message.includes('应为 0')
);
assert.strictEqual(
variantCtorErrors.length,
0,
`unexpected REnum variant ctor errors: ${JSON.stringify(variantCtorErrors)}`
);
// 泛型自引用基类(class ParamNode(GSListNode[ParamNode]))不报未定义。
const paramNodeErrors = diagnostics.filter((d) => d.message.includes('ParamNode'));
assert.strictEqual(
paramNodeErrors.length,
0,
`unexpected ParamNode errors: ${JSON.stringify(paramNodeErrors)}`
);
program.dispose();
serviceProvider.dispose();
});
test('os/__init__.vp 中 os._win32 子模块访问不报未知属性', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(OS_INIT_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 命中场景:os/__init__.vp L6 `import os._win32` 后,L25 `os._win32.exists(path)`。
// Viper 中 `import os._win32` 应把子模块 _win32 注册为 os 模块的属性。
const win32Errors = diagnostics.filter(
(d) => d.message.includes('_win32') && (d.message.includes('known attribute') || d.message.includes('属性'))
);
assert.strictEqual(
win32Errors.length,
0,
`unexpected os._win32 errors: ${JSON.stringify(win32Errors)}`
);
program.dispose();
serviceProvider.dispose();
});
test('requests.vp 中 str = None 默认值不展开为 str | None,无 Optional 误报', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(REQUESTS_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 命中场景:requests.vp L258 `def post(url: str, data: str = None, ...)`。
// Viper 下 str 是 i8*(可空指针),None 是空指针,不应展开为 str | None,
// 否则 data[0] 等下标访问会误报 reportOptionalSubscript。
const optionalErrors = diagnostics.filter(
(d) => d.message.includes('OptionalSubscript') || d.message.includes('is not subscriptable')
);
assert.strictEqual(
optionalErrors.length,
0,
`unexpected optional subscript errors: ${JSON.stringify(optionalErrors)}`
);
// 不应有任何 category=0/1 的错误。
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
assert.strictEqual(hardErrors.length, 0, `unexpected hard errors: ${JSON.stringify(hardErrors)}`);
program.dispose();
serviceProvider.dispose();
});
test('__sha256.vp 中 CArray 传给 CPtr 参数(数组名退化)不报类型错误', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(SHA256_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 命中场景:__sha256.vp L95/100/111 `self.transform(self.buf)`,
// self.buf 是 `t.CArray[t.CUInt8T, SHA256_BLOCK_LEN]`,transform 的
// block 参数是 `t.CUInt8T | t.CPtr`(折叠为 CPtr[CUInt8T])。
// C 语义下数组名退化为指向首元素的指针,应允许 CArray → CPtr 传参。
const cptrErrors = diagnostics.filter(
(d) =>
d.message.includes('cannot be assigned to parameter "block"') ||
d.message.includes('参数 "block"') ||
(d.message.includes('CArray') && d.message.includes('CPtr'))
);
assert.strictEqual(cptrErrors.length, 0, `unexpected CArray->CPtr errors: ${JSON.stringify(cptrErrors)}`);
program.dispose();
serviceProvider.dispose();
});
test('__sha512.vp 中 CPtr 传给 CArray 参数(形参调整)不报类型错误', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(SHA512_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 命中场景:__sha512.vp L119 `self.transform(data)`,
// data 是 `t.CUInt8T | t.CPtr`(折叠为 CPtr[CUInt8T]),transform 的
// block 参数是 `t.CArray[t.CUInt8T, SHA512_BLOCK_LEN]`。
// C 语义下函数参数声明为数组时自动调整为指针(parameter adjustment),
// 应允许 CPtr → CArray 形参传参。
const cptrErrors = diagnostics.filter(
(d) =>
d.message.includes('cannot be assigned to parameter "block"') ||
d.message.includes('参数 "block"') ||
(d.message.includes('CArray') && d.message.includes('CPtr'))
);
assert.strictEqual(cptrErrors.length, 0, `unexpected CPtr->CArray errors: ${JSON.stringify(cptrErrors)}`);
program.dispose();
serviceProvider.dispose();
});
test('ast/__init__.vp 中派生类实例传给基类指针(Expression→CPtr[AST])不报类型错误', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(AST_INIT_PATH);
const diagnostics = getDiagnostics(program, configOptions, uri);
// 命中场景:ast/__init__.vp L105 `return Expression(pool, body)`,
// Expression 继承自 AST,函数返回类型是 `AST | t.CPtr`(折叠为 CPtr[AST])。
// C 语义下派生类指针可隐式 upcast 为基类指针(CPtr[Expression] → CPtr[AST])。
const exprErrors = diagnostics.filter(
(d) => d.message.includes('Expression') && (d.message.includes('返回类型') || d.message.includes('assignable'))
);
assert.strictEqual(exprErrors.length, 0, `unexpected Expression->CPtr[AST] errors: ${JSON.stringify(exprErrors)}`);
program.dispose();
serviceProvider.dispose();
});
/**
* 语义 token 辅助:解码 SemanticTokens 相对编码,返回 line 级 token 映射。
* 返回 Map<line, Array<{start, text, tokenType, tokenModifiers}>>
*/
function getSemanticTokens(program: Program, uri: any): Map<number, any[]> {
const { SemanticTokenProvider } = require('../languageService/semanticTokenProvider');
const { LogTracker } = require('../common/logTracker');
const provider = new SemanticTokenProvider(new LogTracker());
const result = provider.provideFull(program, uri);
assert(result, 'no semantic tokens produced');
const sourceFile = program.getSourceFile(uri);
const lines = sourceFile!.getParseResults()!.text.split('\n');
const map = new Map<number, any[]>();
let cursorLine = 0;
let cursorChar = 0;
for (let i = 0; i < result.data.length; i += 5) {
const deltaLine = result.data[i];
const deltaStart = result.data[i + 1];
const length = result.data[i + 2];
const tokenType = result.data[i + 3];
const tokenMod = result.data[i + 4];
cursorLine += deltaLine;
cursorChar = deltaLine === 0 ? cursorChar + deltaStart : deltaStart;
const lineText = lines[cursorLine] || '';
const text = lineText.substring(cursorChar, cursorChar + length);
const entry = { start: cursorChar, text, tokenType, tokenModifiers: tokenMod };
if (!map.has(cursorLine)) {
map.set(cursorLine, []);
}
map.get(cursorLine)!.push(entry);
}
return map;
}
test('ast/__init__.vp 中不存在的 from .visitor import ASTVisitor 标黄且标白(undefined)', () => {
const { program, serviceProvider, uri, configOptions } = buildProgram(AST_INIT_PATH);
// 1. 诊断:不存在的模块 `.visitor` 应标黄(warning,cat=1=Warning)
const diagnostics = getDiagnostics(program, configOptions, uri);
const visitorDiag = diagnostics.find(
(d) => d.range === 'L51' && d.message.includes('visitor')
);
assert(visitorDiag, `expected unresolved '.visitor' diagnostic, got: ${JSON.stringify(diagnostics)}`);
assert.strictEqual(visitorDiag.category, 1, `expected warning(1) for .visitor, got cat=${visitorDiag.category}`);
// 2. 语义 token:`visitor` 和 `ASTVisitor` 都应为 undefined(白色,type=11)
const tokensByLine = getSemanticTokens(program, uri);
const line51 = tokensByLine.get(50); // L51 → 行索引 50
assert(line51, 'expected tokens on line 51');
const visitorToken = line51.find((t) => t.text === 'visitor');
assert(visitorToken, 'expected "visitor" token on line 51');
assert.strictEqual(visitorToken.tokenType, 11, `"visitor" should be undefined(11), got ${visitorToken.tokenType}`);
const astVisitorToken = line51.find((t) => t.text === 'ASTVisitor');
assert(astVisitorToken, 'expected "ASTVisitor" token on line 51');
assert.strictEqual(
astVisitorToken.tokenType,
11,
`"ASTVisitor" should be undefined(11), got ${astVisitorToken.tokenType}`
);
program.dispose();
serviceProvider.dispose();
});
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "vipeye",
"displayName": "Vipeye",
"description": "Type checker for the Viper language",
"version": "1.1.411",
"version": "1.1.412",
"license": "MIT",
"author": {
"name": "TermiNexus"
+10
View File
@@ -1,5 +1,15 @@
# Change Log
## 1.1.412
- 语义高亮:不存在的模块名与未解析的导入符号(如 `from .visitor import ASTVisitor`)标为 undefined(白色)
- 诊断:Viper 方言下 `reportMissingImports` 降级为 warning(黄色波浪线),与 `reportAttributeAccessIssue` 一致
- 语义高亮:import 别名按实际类型着色(类/函数/模块/常量)
- 语义高亮:`t.CDefine` 常量显示为常量色(variable + readonly)
- 类型检查:CArray → CPtr 数组名退化、CPtr → CArray 形参调整
- 类型检查:派生类实例 → 基类指针 upcast(`CPtr[Expression]` → `CPtr[AST]`)
- 类型检查:`os._win32` 等子模块合并解析,多子模块访问不再误报未知属性
## 0.1.0
- Initial release of Vipeye (Viper language support for VS Code)
+36 -6
View File
@@ -1,13 +1,43 @@
# Vipeye
Vipeye is a VS Code extension that provides language server features for the Viper programming language.
Vipeye 是面向 Viper 语言的 VS Code 扩展,提供完整的语言服务支持。
## Features
Viper 是一门基于 CPython 语法的 AOT 静态类型语言(编译器为 TransPyC / TPV),与 Python 的语义完全不同。Vipeye 在 Pyright 基础上深度改造而来,现已全面切换为 Viper 语义。
- Syntax highlighting for Viper (.vp, .vpi) files
- Project file (.vpj) support
- Type checking and diagnostics
- Code completion and hover information
## 功能特性
- **语法高亮**:`.vp` / `.vpi` 文件的完整 Viper 语法高亮(关键字、类型、装饰器 `@`、f-string、常量、未定义符号等语义化区分)
- **语义高亮**:基于 LSP 3.16 SemanticTokens 协议,区分已定义/未定义符号、类/函数/变量/模块/参数/常量
- **类型检查与诊断**:遵循 Viper 语义的类型推断与静态检查
- 指针语义:`CPtr` / `CArray`(数组名退化、形参调整、派生类指针 upcast)
- 类型别名与 C 类型系统:`t.CInt` / `t.CTypedef` / `t.CDefine` / `t.CEnum` 等
- Viper 特有的 `T | t.CPtr` 指针折叠表示
- 全局变量访问规则(读/写/global 声明判定)
- **项目支持**:`.vpj` 项目配置文件、`viperconfig.json` 配置
- **代码补全与悬停信息**
- **导航**:定义跳转、查找引用、重命名
## 项目架构
- `vscode-vipeye`:LSP 客户端扩展(本包)
- `vipeye`:Viper 语言命令行类型检查器(pyright 命令行工具改造)
- `vipeye-internal`:核心分析引擎(类型推断、语义高亮、诊断)
- `vipeye-typeserver`:类型服务进程
- Viper 核心扩展 `vscode-viper-core`:管理 `.vpj` 项目配置与工具链
## 配置
在 `.vscode/settings.json` 或 `viperconfig.json` 中配置:
```json
{
"viper.analysis.typeCheckingMode": "standard",
"viper.analysis.extraPaths": ["d:/path/to/includes"],
"viper.analysis.diagnosticSeverityOverrides": {
"reportMissingImports": "warning"
}
}
```
## License
+7 -5
View File
@@ -2,7 +2,7 @@
"name": "vscode-vipeye",
"displayName": "Vipeye",
"description": "VS Code language support for Viper (LSP client)",
"version": "1.1.411",
"version": "1.1.412",
"private": true,
"license": "MIT",
"author": {
@@ -76,6 +76,10 @@
"variable": [
"variable"
],
"variable.readonly": [
"variable.other.constant",
"constant"
],
"function": [
"entity.name.function.viper",
"entity.name.function"
@@ -111,9 +115,7 @@
"variable.other.property",
"variable.other.member"
],
"undefined": [
"variable"
]
"undefined": []
}
}
],
@@ -1377,7 +1379,7 @@
"boolean"
],
"description": "Diagnostics for undefined variables.",
"default": "error",
"default": "warning",
"enum": [
"none",
"information",