From 41bc3029167a1fec0aa91e10d052cf0afb52fd48 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 17 Aug 2026 10:32:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(vipeye-internal):=20Viper=20len=20?= =?UTF-8?q?=E4=BC=98=E5=85=88=E8=B0=83=E7=94=A8=20=5F=5Flen=5F=5F=EF=BC=8C?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E4=BD=93=E7=BC=BA=20=5F=5Flen=5F=5F=20?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - len 对用户自定义结构体优先解析 __len__ 返回类型,无 __len__ 报错 - t.CType 原生类型(C 标量/指针/str/bytes)豁免,返回 t.SizeT - 新增本地化消息 viperLenNoLenMethod - viperGenericTypeVarCall 10/10 测试通过 --- .../src/analyzer/typeEvaluator.ts | 78 ++++++++++++++++ .../src/localization/localize.ts | 2 + .../src/localization/package.nls.en-us.json | 1 + .../src/localization/package.nls.zh-cn.json | 1 + .../src/tests/viperGenericTypeVarCall.test.ts | 90 +++++++++++++++++++ 5 files changed, 172 insertions(+) diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index 5d690a68..406f597f 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -9280,6 +9280,50 @@ export function createTypeEvaluator( return false; } + // Viper 扩展:判断调用节点是否为内置 len 调用。 + // Viper 语义下 len 接受任意值(不限定 Sized 协议),返回 t.SizeT(size_t), + // 与 typeshed builtins.vpi 的 Python len 声明(obj: Sized, / -> int)语义不同。 + // 当同一函数名存在多个"版本"定义时,以本特判(Viper 方言实现)优先。 + function isViperLenCall(baseTypeResult: TypeResult): boolean { + return ( + isFunction(baseTypeResult.type) && + baseTypeResult.type.shared.name === 'len' && + baseTypeResult.type.shared.moduleName === 'builtins' + ); + } + + // Viper 扩展:从 t 模块获取 SizeT(size_t)的实例类型。 + // t.vpi 中类型名为 CSizeT;优先查找 'SizeT',失败时回退 'CSizeT'。 + // 查找失败返回 undefined。 + function getViperSizeTType(node: ParseNode): Type | undefined { + const tSymbolWithScope = lookUpSymbolRecursive(node, 't', /* honorCodeFlow */ true); + if (!tSymbolWithScope) { + return undefined; + } + const tEffectiveType = getEffectiveTypeOfSymbolForUsage( + tSymbolWithScope.symbol, + /* usageNode */ undefined, + /* useLastDecl */ true + ).type; + if (!tEffectiveType || !isModule(tEffectiveType)) { + return undefined; + } + const sizeTSymbol = + ModuleType.getField(tEffectiveType, 'SizeT') ?? ModuleType.getField(tEffectiveType, 'CSizeT'); + if (!sizeTSymbol) { + return undefined; + } + const sizeTType = getEffectiveTypeOfSymbolForUsage( + sizeTSymbol, + /* usageNode */ undefined, + /* useLastDecl */ true + ).type; + if (sizeTType && isInstantiableClass(sizeTType)) { + return convertToInstance(sizeTType); + } + return undefined; + } + // Viper 扩展:判断调用节点是否为 stdarg.va_arg 调用。 // va_arg 通过 `from stdarg import va_arg` 引入,baseTypeResult 为 FunctionType, // 其 shared.name 为 'va_arg'。TPV 将 va_arg 视为 builtin,第二参数是类型名。 @@ -9761,6 +9805,40 @@ export function createTypeEvaluator( } else { typeResult.type = UnknownType.create(); } + } else if ( + // Viper 扩展:拦截内置 len 调用。 + // Viper 语义下 len 优先调用 __len__ 魔法函数: + // - 用户自定义结构体类实例:必须有 __len__ 方法,否则报错; + // 返回 __len__ 的返回类型。 + // - t.CType 原生类型(C 标量/指针/str/bytes 等,任意值以 t.CType 表示): + // 豁免检查,直接返回 t.SizeT(size_t)。 + // typeshed builtins.vpi 的 Python len 声明 `len(obj: Sized, /) -> int` + // 与 Viper 语义不符,版本重复时以本特判(Viper 方言实现)优先。 + IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) && + isViperLenCall(baseTypeResult) + ) { + const argExpr = node.d.args.length >= 1 ? node.d.args[0].d.valueExpr : undefined; + const argType = argExpr ? getTypeOfExpression(argExpr).type : UnknownType.create(); + if (isClassInstance(argType) && isViperUserStructValue(argType)) { + // 用户自定义结构体:要求实现 __len__。 + const lenMethodResult = argExpr ? getTypeOfBoundMember(argExpr, argType, '__len__') : undefined; + if (lenMethodResult && isFunction(lenMethodResult.type)) { + const lenRetType = lenMethodResult.type.shared.declaredReturnType; + typeResult.type = + lenRetType && isInstantiableClass(lenRetType) + ? convertToInstance(lenRetType) + : lenRetType ?? UnknownType.create(); + } else { + addDiagnostic( + DiagnosticRule.reportGeneralTypeIssues, + LocMessage.viperLenNoLenMethod().format({ name: argType.shared.name }), + node + ); + typeResult.type = UnknownType.create(); + } + } else { + typeResult.type = getViperSizeTType(node) ?? getBuiltInObject(node, 'int'); + } } else { const callResult = validateCallArgs( node, diff --git a/packages/vipeye-internal/src/localization/localize.ts b/packages/vipeye-internal/src/localization/localize.ts index 63fdb856..18789381 100644 --- a/packages/vipeye-internal/src/localization/localize.ts +++ b/packages/vipeye-internal/src/localization/localize.ts @@ -1182,6 +1182,8 @@ export namespace Localizer { new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperModifierRequiresDef')); export const viperExternBodyInvalid = () => new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperExternBodyInvalid')); + 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'); diff --git a/packages/vipeye-internal/src/localization/package.nls.en-us.json b/packages/vipeye-internal/src/localization/package.nls.en-us.json index b9487649..b3f860a5 100644 --- a/packages/vipeye-internal/src/localization/package.nls.en-us.json +++ b/packages/vipeye-internal/src/localization/package.nls.en-us.json @@ -1620,6 +1620,7 @@ "unexpectedUnindent": "Unindent not expected", "viperModifierRequiresDef": "The modifier \"{name}\" must be used with 'def'", "viperExternBodyInvalid": "The extern function \"{name}\" must have a body of only 'pass' or 'None'", + "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", diff --git a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json index a64690d7..024c230d 100644 --- a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json +++ b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json @@ -618,6 +618,7 @@ "unexpectedUnindent": "不应取消缩进", "viperModifierRequiresDef": "修饰关键字 \"{name}\" 必须与 def 搭配使用", "viperExternBodyInvalid": "extern 函数 \"{name}\" 的函数体必须为 pass 或 None", + "viperLenNoLenMethod": "类型 \"{name}\" 没有 __len__ 方法,不能用于 len()", "unhashableDictKey": "字典密钥必须可哈希", "unhashableSetEntry": "Set 条目必须是可哈希的", "uninitializedAbstractVariables": "抽象基类中定义的变量未在 final 类 \"{classType}\" 中初始化", diff --git a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts index e3a79038..a24f4c7d 100644 --- a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts +++ b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts @@ -716,3 +716,93 @@ def test_builtins_asm_llvmir(): } serviceProvider.dispose(); }); + +test('Viper len 接受任意值并返回 t.SizeT', () => { + 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 code = `import t + +class Vec3: + def __init__(self, x: t.CInt, y: t.CInt, z: t.CInt): + self.x = x + self.y = y + self.z = z + + def __len__(self) -> t.CInt: + return 3 + +class NoLen: + a: t.CInt = 0 + +def test_len_any(): + n: t.CInt = 5 + f: t.CFloat = 3.5 + p: t.CPtr = None + s: str = "hello" + v = Vec3(1, 2, 3) + nl = NoLen() + # len 对 t.CType 原生类型(int/float/指针/str):返回 t.SizeT,不报错 + a: t.CSizeT = len(n) + b: t.CSizeT = len(f) + c: t.CSizeT = len(p) + d: t.CSizeT = len(s) + # len 优先调用 __len__:有 __len__ 的结构体返回其返回类型 + e: t.CSizeT = len(v) + # 无 __len__ 的结构体:报错 + bad = len(nl) +`; + const testDir = path.join(__dirname, '__viper_len_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'len_any.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== len_any.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望恰好 1 个错误级诊断:无 __len__ 的结构体 NoLen 使用 len() 报错; + // t.CType 原生类型(int/float/指针/str)豁免;有 __len__ 的 Vec3 返回其返回类型。 + const errors = diagnostics.filter((d) => d.startsWith('[category=0]')); + assert.strictEqual( + errors.length, + 1, + `expected exactly 1 error (NoLen without __len__), got: ${JSON.stringify(diagnostics)}` + ); + assert.ok( + errors[0].includes('__len__'), + `expected __len__ error message: ${JSON.stringify(errors)}` + ); + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +});