diff --git a/packages/Viper.png b/Viper.png similarity index 100% rename from packages/Viper.png rename to Viper.png diff --git a/packages/Viper.svg b/Viper.svg similarity index 100% rename from packages/Viper.svg rename to Viper.svg diff --git a/packages/vipeye-internal/package.json b/packages/vipeye-internal/package.json index e3ecc30a..703bf073 100644 --- a/packages/vipeye-internal/package.json +++ b/packages/vipeye-internal/package.json @@ -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": [ diff --git a/packages/vipeye-internal/src/analyzer/decorators.ts b/packages/vipeye-internal/src/analyzer/decorators.ts index 43b2814b..24214fcb 100644 --- a/packages/vipeye-internal/src/analyzer/decorators.ts +++ b/packages/vipeye-internal/src/analyzer/decorators.ts @@ -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. diff --git a/packages/vipeye-internal/src/analyzer/patternMatching.ts b/packages/vipeye-internal/src/analyzer/patternMatching.ts index 1751b51a..a57796b6 100644 --- a/packages/vipeye-internal/src/analyzer/patternMatching.ts +++ b/packages/vipeye-internal/src/analyzer/patternMatching.ts @@ -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 []; } diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index ea025e53..47a27f89 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -2655,7 +2655,6 @@ export function createTypeEvaluator( if (!boundMethodResult || boundMethodResult.typeErrors) { return undefined; } - if (isFunctionOrOverloaded(boundMethodResult.type)) { return boundMethodResult.type; } @@ -5343,10 +5342,19 @@ export function createTypeEvaluator( if (name === 'reveal_type' || name === 'reveal_locals') { type = AnyType.create(); } else { - // Viper 扩展:前向引用支持。 - // Viper 是 AOT 语言,结构体/函数不在乎定义顺序(类似 C 语言的前向声明)。 - // 当符号未找到时(可能在更后方定义),不报 undefinedVariable 错误, - // 而是返回 UnknownType,允许后续定义的符号被引用。 + // Viper 扩展:前向引用支持(收紧版)。 + // Viper 是 AOT 语言,函数/类的定义顺序无关(类似 C 语言的前向声明), + // 但绑定器在分析开始前已预扫描全部声明,真实的前向引用目标 + // (如 viperlib.vp 中 sprintf 引用后方的 vsnprintf)会出现在符号表中 + // (symbolWithScope 非空),不会进入此分支。 + // 走到这里的名字(symbolWithScope 为空)在整个文件+可见作用域中 + // 均无任何声明,属于真正的未定义符号(如拼写错误 count1), + // 此时应报 undefined variable,而不是静默返回 Unknown。 + addDiagnostic( + DiagnosticRule.reportUndefinedVariable, + LocMessage.symbolIsUndefined().format({ name }), + node + ); type = UnknownType.create(); } } @@ -8615,7 +8623,7 @@ export function createTypeEvaluator( // 返回特殊化为 Unknown 的 CPtr[Unknown] 实例,使任何 CPtr[X] 特殊化指针 // 都能通过泛型匹配赋值(Unknown 匹配一切类型参数)。 // 查找失败返回 undefined。 - function getViperCPtrType(node: ExpressionNode): Type | undefined { + function getViperCPtrType(node: ParseNode): Type | undefined { const tSymbolWithScope = lookUpSymbolRecursive(node, 't', /* honorCodeFlow */ true); if (tSymbolWithScope) { const tEffectiveType = getEffectiveTypeOfSymbolForUsage( @@ -8640,6 +8648,28 @@ export function createTypeEvaluator( return undefined; } + // Viper 扩展:判断类型是否为 REnum 变体类。 + // Viper 的 REnum(t.REnum)类内部定义嵌套类变体(如 class LLVMType 内的 + // class Int/Ptr/Func/Struct 等)。这些嵌套类不继承 CType,但语义上是 + // REnum 的 tagged-union 变体:match 模式 `LLVMType.Ptr(pointee)` 按字段 + // 顺序解构,构造调用 `LLVMType.Int(bits)` 按字段顺序传参。 + // 判断依据:类声明嵌套在另一个类(外层 REnum)的 suite 中。 + function isViperREnumVariantType(type: Type): boolean { + if (!isClass(type)) { + return false; + } + 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; + } + // Viper 扩展:构造带 t.CChar 类型别名信息的 int 类型(表示 C 的 char 类型)。 // Viper 中 str/bytes 是 i8* 指针,下标访问/迭代/字符运算返回 t.CChar(int 别名)。 // 优先从 t 模块查找 CChar 类型别名以保留显示信息;找不到时回退为 @@ -9285,6 +9315,34 @@ export function createTypeEvaluator( return false; } + // Viper 扩展:判断调用节点是否为 stdarg.va_arg 调用。 + // va_arg 通过 `from stdarg import va_arg` 引入,baseTypeResult 为 FunctionType, + // 其 shared.name 为 'va_arg'。TPV 将 va_arg 视为 builtin,第二参数是类型名。 + function isViperVaArgCall(node: CallNode, baseTypeResult: TypeResult): boolean { + return ( + isFunction(baseTypeResult.type) && + baseTypeResult.type.shared.name === 'va_arg' && + baseTypeResult.type.shared.moduleName === 'stdarg' + ); + } + + // Viper 扩展:判断调用节点是否为 c 模块的 Deref/Load/DerefAs/Set 调用。 + // 这些函数在 c.vpi 存根中声明为 int 参数,但 Viper 语义接受任意指针表达式。 + function isViperCDerefCall(node: CallNode, baseTypeResult: TypeResult): boolean { + const leftExpr = node.d.leftExpr; + if (leftExpr.nodeType !== ParseNodeType.MemberAccess) { + return false; + } + const memberName = leftExpr.d.member.d.value; + if (memberName !== 'Deref' && memberName !== 'Load' && memberName !== 'DerefAs' && memberName !== 'Set') { + return false; + } + return ( + isFunction(baseTypeResult.type) && + baseTypeResult.type.shared.moduleName === 'c' + ); + } + // Viper 扩展:检查类型注解 AST 节点是否包含 CPtr 引用。 // 用于判断循环引用是否被指针打破(如 next: 'Node' | t.CPtr)。 // 递归遍历类型注解表达式,查找名称为 CPtr/CNeedPtr/CAutoPtr/CArrayPtr 的标识符。 @@ -9618,6 +9676,18 @@ export function createTypeEvaluator( } else { typeResult.type = instanceType; } + } else if ( + // Viper 扩展:拦截 REnum 变体类构造调用。 + // Viper 的 REnum 变体(如 LLVMType.Int(bits)、LLVMType.Struct(fields, fcount, name)) + // 没有 __init__,但构造调用按字段声明顺序接收参数(tagged-union 语义)。 + // Pyright 默认对无 __init__ 的类只允许 0 个位置参数,会把 + // `LLVMType.Int(1)` 误报为"应为 0 个位置参数"。 + // 此处跳过参数校验,直接返回变体类的实例类型。 + IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) && + isInstantiableClass(baseTypeResult.type) && + isViperREnumVariantType(baseTypeResult.type) + ) { + typeResult.type = ClassType.cloneAsInstance(baseTypeResult.type); } else if ( // Viper 扩展:拦截 __sizeof__ 调用。 // Viper 中 __sizeof__ 语义是 C 语言的 sizeof(Type) -> int, @@ -9630,6 +9700,48 @@ export function createTypeEvaluator( // Viper 的 __sizeof__ 是类方法(sizeof(Type)),返回类型大小(字节数)。 // 跳过 object.__sizeof__ 的实例方法参数检查,直接返回 int。 typeResult.type = getBuiltInObject(node, 'int'); + } else if ( + // Viper 扩展:拦截 stdarg.va_arg 调用。 + // TPV 将 va_arg 视为 builtin(见 HandlesExprBuiltin._HandleVaArgLlvm), + // 第二参数是类型名(如 t.CInt64T / t.CPtr / t.CInt64T | t.CPtr), + // 而非 CPtr 值。若按 stdarg.vp 的声明 `va_arg(args: t.CPtr, type: t.CPtr)` + // 做普通参数校验,会把类型名当作 CPtr 值检查而误报。 + // 返回类型 = 第二参数类型形式对应的实例类型;无第二参数时返回 Unknown + // (由赋值上下文注解推断,如 `ptr_i: t.CInt = va_arg(ap)`)。 + IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) && + isViperVaArgCall(node, baseTypeResult) + ) { + if (node.d.args.length >= 2) { + const typeExpr = node.d.args[1].d.valueExpr; + const typeFormResult = getTypeOfExpression(typeExpr, EvalFlags.TypeExpression); + const typeFormType = stripTypeForm(typeFormResult.type); + typeResult.type = isInstantiableClass(typeFormType) + ? convertToInstance(typeFormType) + : typeFormType; + } else { + typeResult.type = UnknownType.create(); + } + } else if ( + // Viper 扩展:拦截 c.Deref / c.Load / c.DerefAs / c.Set 调用。 + // TPV 将 Deref 等视为 builtin(见 lib/core/Handles/HandlesSpecialCall.py), + // 指针参数接受任意指针表达式(str/bytes/t.CPtr 等,i8*)。 + // c.vpi 存根中 `Deref(ptr: int)` 的 int 参数声明与 Viper 语义不符 + // (Viper 中 str 是 i8*,str→int 不允许隐式转换), + // 按普通校验会误报 "str 不可分配给 int"。 + // 此处跳过参数检查,返回:Deref → 类型参数 T 或 Unknown;其余 → Unknown。 + IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) && + isViperCDerefCall(node, baseTypeResult) + ) { + if (node.d.leftExpr.nodeType === ParseNodeType.MemberAccess && + node.d.leftExpr.d.member.d.value === 'Deref' && + node.d.args.length >= 1) { + // c.Deref(ptr) → 解引用结果。返回类型为 c.Deref 的 T 类型参数 + // 对应的实例类型(从 c.vpi 的 `def Deref(ptr: int) -> T` 推导)。 + // 简化:返回 Unknown,由赋值上下文推断(如 `v: int = c.Deref(p)`)。 + typeResult.type = UnknownType.create(); + } else { + typeResult.type = UnknownType.create(); + } } else { const callResult = validateCallArgs( node, @@ -20534,7 +20646,27 @@ export function createTypeEvaluator( !isOptionalType(type) && !AnalyzerNodeInfo.getFileInfo(param).diagnosticRuleSet.strictParameterNoneValue ) { - return combineTypes([type, getNoneType()]); + // Viper 扩展:None 在 Viper 中对应 C 的 NULL(空指针),而不是 + // Python 语义下的 Optional 标记。当参数注解本身就是指针类类型 + // (str/bytes 是 i8*、CPtr、CVoid、函数指针、纯 C 修饰符), + // `= None` 只是表示默认值为空指针,无需展开为 `X | None`。 + // 若展开,`app_deps: str = None` 会变成 `str | None`, + // 导致函数体内 app_deps[0] 这类下标访问误报 reportOptionalSubscript。 + // 仅对值类型(int/bool/float 等)保留 PEP 484 展开语义。 + const isViperPtrLike = (() => { + const variants = isUnion(type) ? type.priv.subtypes : [type]; + return variants.some( + (variant) => + isCPtrType(variant) || + isCVoidType(variant) || + isPureCModifierType(variant) || + isFunction(variant) || + (isClassInstance(variant) && ClassType.isBuiltIn(variant, ['str', 'bytes'])) + ); + })(); + if (!isViperPtrLike) { + return combineTypes([type, getNoneType()]); + } } return type; @@ -20701,6 +20833,19 @@ export function createTypeEvaluator( return ClassType.cloneForPacked(type); } + // Viper 扩展:`*args` 在函数体内绑定为 `va_list`(即 `CPtr[Unknown]`, + // 对应 LLVM 的 i8*),而非 Python 语义下的 tuple。TPV 在函数入口对 + // `*args` 变量执行 va_start 并注册为裸指针(见 HandlesFunctions.py: + // `Gen._direct_values[vararg_name] = va_list_ptr`),因此函数体内 + // 直接传递 `args` 给另一个 variadic 函数(如 vsnprintf 的 `ap` 参数) + // 是合法的指针透传,不应报 tuple 类型不兼容。 + if (IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect)) { + const cptrType = getViperCPtrType(node); + if (cptrType) { + return cptrType; + } + } + return makeTupleObject(evaluatorInterface, [{ type, isUnbounded: !isTypeVarTuple(type) }]); } @@ -21276,11 +21421,20 @@ export function createTypeEvaluator( } if (isClass(subtype)) { + // Viper 扩展:`__exit__` 无需异常参数(Viper 无异常机制), + // 签名仅为 `def __exit__(self):`。若按 CPython 惯例传 + // (exc_type, exc_val, exc_tb) 三个参数会报参数过多, + // 导致 magic method 调用失败并误报 "not usable with"。 const anyArg: TypeResult = { type: AnyType.create() }; + const exitArgList: TypeResult[] = IsViperDialect( + AnalyzerNodeInfo.getFileInfo(node.d.expr).executionEnvironment.dialect + ) + ? [] + : [anyArg, anyArg, anyArg]; const exitTypeResult = getTypeOfMagicMethodCall( subtype, exitMethodName, - [anyArg, anyArg, anyArg], + exitArgList, node.d.expr, /* inferenceContext */ undefined, exitDiag @@ -23104,7 +23258,23 @@ export function createTypeEvaluator( } if (usageScopeNode === declScopeNode) { - if (!isFlowPathBetweenNodes(declNode, node)) { + // Viper 扩展:类/函数声明的引用不受代码流可达性限制。 + // Viper 是 AOT 语言,支持前向引用(类似 C 语言的前向声明)。 + // 例如 `class ParamNode(GSListNode[ParamNode])` 中基类类型参数 + // 引用自身(ParamNode 声明节点在引用节点之后,代码流不可达), + // 以及函数体引用后方定义的函数/类。这些都应视为合法引用, + // 跳过代码流过滤,否则会误报未定义。 + const declIsClassOrFunc = + decl.type === DeclarationType.Class || + decl.type === DeclarationType.Function || + decl.type === DeclarationType.TypeAlias; + if ( + !isFlowPathBetweenNodes(declNode, node) && + !( + IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) && + declIsClassOrFunc + ) + ) { // If there was no control flow path from the usage back // to the source, see if the usage node is reachable by // any path. @@ -24102,13 +24272,47 @@ export function createTypeEvaluator( } } - return applyLoaderActionsToModuleType( - moduleType, + // Viper 扩展:合并同一符号的所有多段 import 声明的 implicitImports。 + // 例如 `import os.path` + `import os._win32` + `import os._posix` 时, + // os 符号有多个 Alias 声明,每个声明只记录各自的子模块。Python/Viper + // 的模块加载器会把所有子模块都挂载到父模块命名空间,因此这里把所有 + // 声明的 implicitImports 合并后再应用 loader actions,避免 `os._win32` + // 被误报为"不是模块 os 的已知属性"。 + const baseLoader = resolvedDecl.symbolName && resolvedDecl.submoduleFallback ? resolvedDecl.submoduleFallback - : resolvedDecl, - importLookup - ); + : resolvedDecl; + const mergedImplicitImports = new Map(baseLoader.implicitImports ?? []); + symbol.getDeclarations().forEach((siblingDecl) => { + if (siblingDecl.type !== DeclarationType.Alias) { + return; + } + const siblingResolved = resolveAliasDeclaration(siblingDecl, /* resolveLocalNames */ true, { + allowExternallyHiddenAccess: AnalyzerNodeInfo.getFileInfo(siblingDecl.node).isStubFile, + }); + if (!siblingResolved || siblingResolved.type !== DeclarationType.Alias) { + return; + } + const siblingLoader = + siblingResolved.symbolName && siblingResolved.submoduleFallback + ? siblingResolved.submoduleFallback + : siblingResolved; + siblingLoader.implicitImports?.forEach((action, name) => { + if (!mergedImplicitImports.has(name)) { + mergedImplicitImports.set(name, action); + } + }); + }); + + let mergedLoaderActions = baseLoader; + if (mergedImplicitImports.size > (baseLoader.implicitImports?.size ?? 0)) { + mergedLoaderActions = { + ...baseLoader, + implicitImports: mergedImplicitImports, + }; + } + + return applyLoaderActionsToModuleType(moduleType, mergedLoaderActions, importLookup); } const declaredType = getTypeForDeclaration(resolvedDecl); @@ -25999,6 +26203,59 @@ export function createTypeEvaluator( if (destIsStrBytes) { return true; } + + // Viper 扩展:数组名退化为指向首元素的指针(C 语义 array-to-pointer decay)。 + // 栈上数组 t.CArray[T, N] 作为实参传递时退化为 T*(CPtr[T]),如 + // `transform(block: CPtr[CUInt8T])` 接收 `buf: CArray[CUInt8T, CDefine]`。 + // 目标可以是 CPtr[T] 或 CVoid(void* 通用指针)。 + if (isClassInstance(destType) && (isCPtrType(destType) || isCVoidType(destType))) { + const destTypeArgs = destType.priv.typeArgs; + if (isCVoidType(destType)) { + return true; + } + const srcTypeArgs = (srcType as ClassType).priv.typeArgs; + const srcElemType = srcTypeArgs && srcTypeArgs.length > 0 ? srcTypeArgs[0] : undefined; + const destPointee = + destTypeArgs && destTypeArgs.length > 0 ? destTypeArgs[0] : undefined; + if (srcElemType && destPointee) { + // 数组元素类型与指针目标类型兼容即可(同类或可隐式转换)。 + return assignType( + destPointee, + srcElemType, + diag, + constraints, + flags | AssignTypeFlags.SkipRecursiveTypeCheck, + recursionCount + 1 + ); + } + return srcElemType !== undefined; + } + } + + // Viper 扩展:指针 → CArray 形参调整(C 语义 parameter adjustment,与 + // array-to-pointer decay 互逆)。C 中函数参数声明为数组 T[N] 时自动调整为 + // 指针 T*,因此实参指针(CPtr[T] / void*)可以传给"数组"形参。例如 + // `transform(block: t.CArray[t.CUInt8T, N])` 接收 `data: t.CUInt8T | t.CPtr`。 + if (isCArrayType(destType) && isClassInstance(srcType) && (isCPtrType(srcType) || isCVoidType(srcType))) { + if (isCVoidType(srcType)) { + return true; + } + const srcTypeArgs = srcType.priv.typeArgs; + const srcPointee = srcTypeArgs && srcTypeArgs.length > 0 ? srcTypeArgs[0] : undefined; + const destTypeArgs = (destType as ClassType).priv.typeArgs; + const destElemType = destTypeArgs && destTypeArgs.length > 0 ? destTypeArgs[0] : undefined; + if (srcPointee && destElemType) { + // 指针目标类型与数组元素类型兼容即可。 + return assignType( + destElemType, + srcPointee, + diag, + constraints, + flags | AssignTypeFlags.SkipRecursiveTypeCheck, + recursionCount + 1 + ); + } + return srcPointee !== undefined; } // Viper 扩展:函数名退化为函数指针(C 语义)。 @@ -26191,11 +26448,15 @@ export function createTypeEvaluator( } if (pointeeAsClass) { const inheritanceChain: InheritanceChain = []; - const isDerivedFrom = ClassType.isDerivedFrom( - srcType, - pointeeAsClass, - inheritanceChain - ); + // 统一比较形式:baseClasses 里存的是 instantiable 基类, + // 与 destPointee 统一转成 instantiable 形式再比较,避免 + // isSameGenericClass 因 instance/instantiable 差异误判。 + const srcForCompare = convertToInstantiable(srcType); + const pointeeForCompare = convertToInstantiable(pointeeAsClass); + const isDerivedFrom = + isInstantiableClass(srcForCompare) && + isInstantiableClass(pointeeForCompare) && + ClassType.isDerivedFrom(srcForCompare, pointeeForCompare, inheritanceChain); if (isDerivedFrom) { return true; } diff --git a/packages/vipeye-internal/src/common/configOptions.ts b/packages/vipeye-internal/src/common/configOptions.ts index 828580f4..e1908350 100644 --- a/packages/vipeye-internal/src/common/configOptions.ts +++ b/packages/vipeye-internal/src/common/configOptions.ts @@ -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'; diff --git a/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts b/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts index a88dc0df..326dfaaa 100644 --- a/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts +++ b/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts @@ -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: diff --git a/packages/vipeye-internal/src/tests/viperUnionMemberAccess.test.ts b/packages/vipeye-internal/src/tests/viperUnionMemberAccess.test.ts new file mode 100644 index 00000000..c6d227a0 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperUnionMemberAccess.test.ts @@ -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(); +}); diff --git a/packages/vipeye-internal/src/tests/viperVariadicArgs.test.ts b/packages/vipeye-internal/src/tests/viperVariadicArgs.test.ts new file mode 100644 index 00000000..02ff7551 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperVariadicArgs.test.ts @@ -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> + */ +function getSemanticTokens(program: Program, uri: any): Map { + 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(); + 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(); +}); diff --git a/packages/vipeye/package.json b/packages/vipeye/package.json index f6a57b7d..9ef2991a 100644 --- a/packages/vipeye/package.json +++ b/packages/vipeye/package.json @@ -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" diff --git a/packages/vscode-vipeye/CHANGELOG.md b/packages/vscode-vipeye/CHANGELOG.md index f21570f9..e5080945 100644 --- a/packages/vscode-vipeye/CHANGELOG.md +++ b/packages/vscode-vipeye/CHANGELOG.md @@ -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) diff --git a/packages/vscode-vipeye/README.md b/packages/vscode-vipeye/README.md index 3b90daf1..12ae5090 100644 --- a/packages/vscode-vipeye/README.md +++ b/packages/vscode-vipeye/README.md @@ -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 diff --git a/packages/vscode-vipeye/package.json b/packages/vscode-vipeye/package.json index 88ff02b1..9efaa372 100644 --- a/packages/vscode-vipeye/package.json +++ b/packages/vscode-vipeye/package.json @@ -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",