From 024366a85aa918794d44a8b95a952d2d877ead40 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 4 Aug 2026 16:28:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=20CExtern=20CExport?= =?UTF-8?q?=20=E4=BF=9D=E7=95=99=E6=9C=BA=E5=88=B6=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vipeye-internal/src/analyzer/checker.ts | 28 ++ .../src/analyzer/typeEvaluator.ts | 26 ++ .../vipeye-internal/src/analyzer/types.ts | 12 +- .../src/common/configOptions.ts | 6 + .../languageService/semanticTokenProvider.ts | 24 +- .../src/localization/localize.ts | 1 + .../src/localization/package.nls.en-us.json | 4 + .../src/localization/package.nls.zh-cn.json | 1 + .../src/tests/viperBeforeInit.test.ts | 278 ++++++++++++++++++ .../src/tests/viperExternReturn.test.ts | 148 ++++++++++ .../src/tests/viperIfElseRedecl.test.ts | 149 ++++++++++ packages/vscode-vipeye/package.json | 1 + 12 files changed, 675 insertions(+), 3 deletions(-) create mode 100644 packages/vipeye-internal/src/tests/viperBeforeInit.test.ts create mode 100644 packages/vipeye-internal/src/tests/viperExternReturn.test.ts create mode 100644 packages/vipeye-internal/src/tests/viperIfElseRedecl.test.ts diff --git a/packages/vipeye-internal/src/analyzer/checker.ts b/packages/vipeye-internal/src/analyzer/checker.ts index f5e016ae..e48f273e 100644 --- a/packages/vipeye-internal/src/analyzer/checker.ts +++ b/packages/vipeye-internal/src/analyzer/checker.ts @@ -416,6 +416,20 @@ export class Checker extends ParseTreeWalker { const functionTypeResult = this._evaluator.getTypeOfFunction(node); const containingClassNode = ParseTreeUtils.getEnclosingClass(node, /* stopAtFunction */ true); + // Viper 扩展:`__before_init__` 由编译器自动生成(见 TransPyC wiki/06-oop.md), + // 所有类都隐式存在该默认成员方法,不允许用户手动定义。 + if ( + containingClassNode && + node.d.name.d.value === '__before_init__' && + IsViperDialect(this._fileInfo.executionEnvironment.dialect) + ) { + this._evaluator.addDiagnostic( + DiagnosticRule.reportGeneralTypeIssues, + LocMessage.viperBeforeInitReserved(), + node.d.name + ); + } + if (functionTypeResult) { // Track whether we have seen a *args: P.args parameter. Named // parameters after this need to be flagged as an error. @@ -3660,6 +3674,20 @@ export class Checker extends ParseTreeWalker { duplicateIsOk = true; } + // Viper 扩展:允许互斥分支(if/else 等不同 suite)中 + // 的同名变量声明。与函数分支的处理逻辑一致:两个声明 + // 不在同一个 statement suite(例如一个在 "if" 分支、 + // 另一个在 "else" 或函数体后续),运行时只执行其一, + // 不应判定为遮蔽。socket.vp accept 方法中 + // `client_fd: u64`(if 分支)与 `client_fd: INT` + // (else 路径)即为此场景。 + const suite1 = ParseTreeUtils.getEnclosingSuite(primaryDecl.node); + const suite2 = ParseTreeUtils.getEnclosingSuite(otherDecl.node); + const isInSameStatementList = suite1 === suite2; + if (!isInSameStatementList) { + duplicateIsOk = true; + } + if (primaryDecl.type === DeclarationType.TypeParam) { // The error will be reported elsewhere if a type parameter is // involved, so don't report it here. diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index a97b32ac..a10b9fce 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -2604,6 +2604,32 @@ export function createTypeEvaluator( diag.addAddendum(subDiag); } + // Viper 扩展:`__before_init__` 是编译器自动生成的默认成员方法 + // (在 __init__ 之前自动填充成员默认值,见 TransPyC wiki/06-oop.md), + // 所有类都隐式存在,无需在源码中定义。因此当成员查找失败且目标 + // 成员名是 `__before_init__` 时,合成一个 `() -> None` 的方法, + // 避免报 "attribute is unknown"。同时该成员不允许手动定义,手动 + // 定义会在 checker 中报错。 + if ( + memberName === '__before_init__' && + isClass(objectType) && + errorNode && + IsViperDialect(AnalyzerNodeInfo.getFileInfo(errorNode).executionEnvironment.dialect) && + (flags & MemberAccessFlags.SkipAttributeAccessOverride) === 0 + ) { + // 合成 `() -> None` 方法。注意:成员访问 `obj.__before_init__()` + // 已绑定实例,因此签名不包含 self 参数(与 __getattr__ 等 + // 动态成员的绑定行为一致),避免调用时误报 "missing self"。 + const beforeInitType = FunctionType.createSynthesizedInstance('__before_init__'); + beforeInitType.shared.declaredReturnType = getNoneType(); + + return { + type: beforeInitType, + isIncomplete: false, + isAsymmetricAccessor: false, + }; + } + return undefined; } diff --git a/packages/vipeye-internal/src/analyzer/types.ts b/packages/vipeye-internal/src/analyzer/types.ts index 79ddc3f4..59b967e7 100644 --- a/packages/vipeye-internal/src/analyzer/types.ts +++ b/packages/vipeye-internal/src/analyzer/types.ts @@ -4136,12 +4136,20 @@ export function combineTypes(subtypes: Type[], options?: CombineTypesOptions): T // 例外:State 不过滤——State 是声明标记(C extern 语义),非类型修饰符。 // 保留 State 使 isPureCModifierType 能识别声明性函数返回类型 // (如 BOOL | t.State: pass 的 extern 声明,过滤后仍为 BOOL | State 联合类型)。 - // 若过滤后无剩余类型,则保留原数组(避免空数组异常)。 + // 关键:过滤后若仅剩一个子类型,则保留修饰符(不丢弃),使 + // `u64 | CExtern | CExport` 这类"数据 + 声明标记"的返回类型 + // 仍为联合,isPureCModifierType 可识别为声明性函数(允许 pass/None)。 + // 否则 CExtern/CExport 被过滤后只剩 u64,会误报 + // "must return value on all code paths"。 if (subtypes.length > 1) { const nonModifierTypes = subtypes.filter( (t) => !isCModifierType(t) || (isClass(t) && t.shared.name === 'State') ); - if (nonModifierTypes.length > 0 && nonModifierTypes.length < subtypes.length) { + if ( + nonModifierTypes.length > 0 && + nonModifierTypes.length < subtypes.length && + nonModifierTypes.length > 1 + ) { subtypes = nonModifierTypes; } } diff --git a/packages/vipeye-internal/src/common/configOptions.ts b/packages/vipeye-internal/src/common/configOptions.ts index eb96be9f..828580f4 100644 --- a/packages/vipeye-internal/src/common/configOptions.ts +++ b/packages/vipeye-internal/src/common/configOptions.ts @@ -901,6 +901,9 @@ export function getStandardDiagnosticRuleSet(dialect?: LanguageDialect): Diagnos // 禁用 strictParameterNoneValue,使 "T = None" 自动扩展为 "T | None"(PEP 484), // 避免 short: str = None 这类合法的 Viper 指针默认值被误报为类型不兼容。 diagSettings.strictParameterNoneValue = false; + // Viper 扩展:不存在的成员访问(属性未知)用黄色波浪线(warning)提示, + // 而非默认的红色 error。LSP 诊断级别 warning 对应 VS Code 的黄色波浪线。 + diagSettings.reportAttributeAccessIssue = 'warning'; } return diagSettings; @@ -1026,6 +1029,9 @@ export function getStrictDiagnosticRuleSet(dialect?: LanguageDialect): Diagnosti // 禁用 strictParameterNoneValue,使 "T = None" 自动扩展为 "T | None"(PEP 484), // 避免 short: str = None 这类合法的 Viper 指针默认值被误报为类型不兼容。 diagSettings.strictParameterNoneValue = false; + // Viper 扩展:不存在的成员访问(属性未知)用黄色波浪线(warning)提示, + // 而非默认的红色 error。LSP 诊断级别 warning 对应 VS Code 的黄色波浪线。 + diagSettings.reportAttributeAccessIssue = 'warning'; } return diagSettings; diff --git a/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts b/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts index 9ccc90ab..a88dc0df 100644 --- a/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts +++ b/packages/vipeye-internal/src/languageService/semanticTokenProvider.ts @@ -592,7 +592,15 @@ class SemanticTokenWalker extends ParseTreeWalker { ? SemanticTokenModifiers.declaration | SemanticTokenModifiers.definition : 0; - if (!declInfo || !declInfo.decls || declInfo.decls.length === 0) { + // Viper 扩展:`__before_init__` 由编译器自动生成,所有类隐式存在。 + // 成员访问 `obj.__before_init__()` 时应显示为方法(函数颜色)。 + // 注意:必须在 declInfo 查询之前优先处理,因为 CPtr 解引用转发 + // 可能让 getDeclInfoForNameNode 返回 CPtr.__getattr__ 的 variable + // 声明(VariableDeclaration),导致误显示为变量颜色。 + if (nameText === '__before_init__' && this._isViperMemberAccess(node)) { + tokenType = SemanticTokenTypes.function; + tokenModifiers = 0; + } else if (!declInfo || !declInfo.decls || declInfo.decls.length === 0) { // 未定义符号 → 白色(默认前景) tokenType = SemanticTokenTypes.undefined; } else { @@ -775,4 +783,18 @@ class SemanticTokenWalker extends ParseTreeWalker { return false; } + + /** + * 判断 NameNode 是否是 Viper 成员访问(obj.__before_init__)中的成员名。 + * `__before_init__` 是 Viper 编译器自动生成的方法,成员访问时无真实 + * symbol/declaration,但应显示为函数颜色而非未定义白色。 + */ + private _isViperMemberAccess(node: NameNode): boolean { + const parent = node.parent; + return ( + parent !== undefined && + parent.nodeType === ParseNodeType.MemberAccess && + (parent as MemberAccessNode).d.member === node + ); + } } diff --git a/packages/vipeye-internal/src/localization/localize.ts b/packages/vipeye-internal/src/localization/localize.ts index 969e2009..17f1b76d 100644 --- a/packages/vipeye-internal/src/localization/localize.ts +++ b/packages/vipeye-internal/src/localization/localize.ts @@ -463,6 +463,7 @@ export namespace Localizer { export const enumMemberSet = () => new ParameterizedString<{ name: string }>(getRawString('Diagnostic.enumMemberSet')); export const enumMemberTypeAnnotation = () => getRawString('Diagnostic.enumMemberTypeAnnotation'); + export const viperBeforeInitReserved = () => getRawString('Diagnostic.viperBeforeInitReserved'); export const exceptionGroupIncompatible = () => getRawString('Diagnostic.exceptionGroupIncompatible'); export const exceptGroupMismatch = () => getRawString('Diagnostic.exceptGroupMismatch'); export const exceptGroupRequiresType = () => getRawString('Diagnostic.exceptGroupRequiresType'); 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 0562963f..9bb6f9b7 100644 --- a/packages/vipeye-internal/src/localization/package.nls.en-us.json +++ b/packages/vipeye-internal/src/localization/package.nls.en-us.json @@ -354,6 +354,10 @@ "message": "Type annotations are not allowed for enum members", "comment": "{Locked='enum'}" }, + "viperBeforeInitReserved": { + "message": "\"__before_init__\" is auto-generated by the compiler and cannot be defined manually", + "comment": "{Locked='__before_init__'}" + }, "exceptionGroupIncompatible": { "message": "Exception group syntax (\"except*\") requires Python 3.11 or newer", "comment": "{Locked='except*'}" 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 2795e372..a573faf5 100644 --- a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json +++ b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json @@ -161,6 +161,7 @@ "enumMemberDelete": "无法删除 Enum 成员 \"{name}\"", "enumMemberSet": "无法分配 Enum 成员 \"{name}\"", "enumMemberTypeAnnotation": "enum 成员不允许使用类型批注", + "viperBeforeInitReserved": "\"__before_init__\" 由编译器自动生成,不允许手动定义", "exceptGroupMismatch": "Try 语句不能同时包含 \"except\" 和 \"except*\"", "exceptGroupRequiresType": "异常组语法(\"except*\")需要异常类型", "exceptRequiresParens": "在 Python 3.14 之前的版本中,多个异常类型必须用括号括起来", diff --git a/packages/vipeye-internal/src/tests/viperBeforeInit.test.ts b/packages/vipeye-internal/src/tests/viperBeforeInit.test.ts new file mode 100644 index 00000000..c9d062cd --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperBeforeInit.test.ts @@ -0,0 +1,278 @@ +/* + * viperBeforeInit.test.ts + * + * 验证 Viper 扩展:`__before_init__` 合成语义。 + * + * 场景(用户反馈): + * - `viperio.Buf` 上调用 `buf.__before_init__()` 应合法(不报 + * "Cannot access attribute '__before_init__' for class 'Buf'"), + * 且该方法显示为函数(hover 显示 (self) -> None)。 + * - 所有类(结构体)默认存在 `__before_init__`,返回值为空(None)。 + * - 用户手动定义 `__before_init__` 应报错(不允许手动定义)。 + * - 不存在的成员(如 `buf.nonexistent`)取值/赋值应为未知类型并报错。 + * + * 运行:npx jest src/tests/viperBeforeInit.test.ts --runInBand + */ + +import assert from 'assert'; +import * as path from 'path'; + +import { ImportResolver } from '../analyzer/importResolver'; +import { Program } from '../analyzer/program'; +import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; +import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; +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'; +import { CallNode, ClassNode, FunctionNode } from '../parser/parseNodes'; + +// 允许 vipershed-fallback(t.vpi/c.vpi 存根)被定位。 +(global as any).__rootDirectory = path.resolve(); + +const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; +const VIPERIO_PATH = path.join(TRANSPYC_INCLUDES, 'viperio.vp'); +const MEMHUB_PATH = path.join(TRANSPYC_INCLUDES, 'memhub.vp'); + +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, execEnv, uri, configOptions }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map( + (diag) => `[category=${diag.category}] ${diag.message}` + ); +} + +test('Buf.__before_init__() 调用合法且返回 None(用户场景)', () => { + const { program, serviceProvider, uri, configOptions } = buildProgram(MEMHUB_PATH); + + // memhub.vp 中 alloc_buf 调用了 buf.__before_init__() + const diagnostics = getDiagnostics(program, configOptions, uri); + + const beforeInitErrors = diagnostics.filter( + (d) => d.includes('__before_init__') && d.includes('Cannot access attribute') + ); + console.log('=== memhub.vp __before_init__ errors ==='); + beforeInitErrors.forEach((d) => console.log(` ${d}`)); + // 不应有 __before_init__ 属性访问错误 + assert.strictEqual(beforeInitErrors.length, 0, `unexpected __before_init__ errors: ${JSON.stringify(beforeInitErrors)}`); + + // Viper 扩展:`buf.__before_init__()` 是已绑定成员调用,不应报 "missing self"。 + // 合成方法签名为 `() -> None`(成员访问已绑定实例)。 + const missingSelfErrors = diagnostics.filter( + (d) => d.includes('__before_init__') && d.includes('Argument missing for parameter') + ); + console.log('=== memhub.vp __before_init__ missing-self errors ==='); + missingSelfErrors.forEach((d) => console.log(` ${d}`)); + assert.strictEqual( + missingSelfErrors.length, + 0, + `unexpected "missing self" errors: ${JSON.stringify(missingSelfErrors)}` + ); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('viperio.Buf 上调用 __before_init__ 返回 None 且可调用', () => { + const { program, serviceProvider, uri, configOptions } = buildProgram(VIPERIO_PATH); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== viperio.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 解析 Buf 类,手动调用 evaluator 求值 buf.__before_init__() + const sourceFile = program.getSourceFile(uri); + const parseResults = sourceFile!.getParseResults(); + assert(parseResults, 'parse results not found'); + + const evaluator = program.evaluator!; + + // 收集 Buf 类定义 + let bufClassNode: ClassNode | undefined; + class ClassFinder extends ParseTreeWalker { + override visitClass(node: ClassNode) { + if (node.d.name.d.value === 'Buf') { + bufClassNode = node; + return false; + } + return true; + } + } + new ClassFinder().walk(parseResults.parserOutput.parseTree); + assert(bufClassNode, 'Buf class not found'); + + // 求值 Buf 类型并模拟 buf.__before_init__() 访问 + const bufType = evaluator.getTypeOfClass(bufClassNode); + console.log(`=== Buf class type: ${bufType ? program.printType(bufType.classType) : '(undefined)'}`); + + // 通过 getTypeOfMember 无法直接模拟,这里验证 viperio.vp 本身无属性错误即可, + // 真实场景(memhub.vp 调用)已由上一个测试覆盖。 + const attrErrors = diagnostics.filter((d) => d.includes('Cannot access attribute')); + assert.strictEqual(attrErrors.length, 0, `unexpected attribute errors: ${JSON.stringify(attrErrors)}`); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('手动定义 __before_init__ 应报错(不允许手动定义)', () => { + 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]; + + // 写一个临时 .vp 文件,手动定义 __before_init__ + const code = `import t\n\nclass Foo:\n x: t.CInt\n def __before_init__(self):\n self.x = 0\n`; + const testDir = path.join(TRANSPYC_INCLUDES, '__before_init_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'manual_def.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('=== manual_def.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const reservedErrors = diagnostics.filter((d) => d.includes('__before_init__')); + assert.ok( + reservedErrors.length > 0, + `expected "cannot be defined manually" diagnostic, got: ${JSON.stringify(diagnostics)}` + ); + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); + +test('语义令牌:__before_init__ 成员访问显示函数颜色(非变量)', () => { + const { program, serviceProvider, execEnv, configOptions } = buildProgram(VIPERIO_PATH); + try { + // 写一个临时 .vp 文件,其中 buf 是 Buf | t.CPtr,调用 buf.__before_init__() + const code = `import t +import viperio + +def test(buf: viperio.Buf | t.CPtr): + buf.__before_init__() +`; + const testDir = path.join(TRANSPYC_INCLUDES, '__before_init_semtok__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'usage.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + // 使用 SemanticTokenProvider 生成语义 token + const { SemanticTokenProvider } = require('../languageService/semanticTokenProvider'); + const { LogTracker } = require('../common/logTracker'); + const provider = new SemanticTokenProvider(new LogTracker()); + const result = provider.provideFull(program, uri); + assert(result, 'semantic tokens not generated'); + console.log(`=== semantic tokens count: ${result.data.length / 5}`); + + // 解码 token:data 格式 [deltaLine, deltaStart, length, tokenType, tokenModifiers] + const lines = program.getSourceFile(uri)!.getParseResults()!.text.split('\n'); + let beforeInitTokenType: number | undefined; + let cursorLine = 0; + let cursorChar = 0; + const allTokens: string[] = []; + 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]; + cursorLine += deltaLine; + cursorChar = deltaLine === 0 ? cursorChar + deltaStart : deltaStart; + const lineText = lines[cursorLine] || ''; + const tokenText = lineText.substring(cursorChar, cursorChar + length); + allTokens.push(`L${cursorLine + 1}:${cursorChar} "${tokenText}" (type=${tokenType})`); + if (tokenText === '__before_init__') { + beforeInitTokenType = tokenType; + console.log(`=== __before_init__ at line ${cursorLine + 1}, tokenType=${tokenType}`); + } + } + console.log(`=== all tokens (${allTokens.length}):`); + allTokens.forEach((t) => console.log(` ${t}`)); + + // SemanticTokenTypes(本项目自定义枚举): + // 0=namespace, 1=class, 2=function, 3=variable, 4=parameter, + // 5=type, 6=builtin, 7=keyword, 8=string, 9=number, + // 10=comment, 11=undefined, 12=decorator, 13=property + assert( + beforeInitTokenType !== undefined, + '__before_init__ token not found in semantic tokens' + ); + console.log(`=== __before_init__ tokenType=${beforeInitTokenType} (expect 2=function)`); + assert( + beforeInitTokenType === 2, + `__before_init__ should be function color, got tokenType=${beforeInitTokenType}` + ); + + fs.rmSync(testDir, { recursive: true, force: true }); + } finally { + program.dispose(); + } + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperExternReturn.test.ts b/packages/vipeye-internal/src/tests/viperExternReturn.test.ts new file mode 100644 index 00000000..0ff517ee --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperExternReturn.test.ts @@ -0,0 +1,148 @@ +/* + * viperExternReturn.test.ts + * + * 验证 Viper 扩展:声明性函数(返回类型含 t.CExtern/t.CExport/t.State 等 + * C 修饰符标记,函数体为 pass)不应报 reportReturnType "must return value"。 + * + * 场景(用户反馈):socket.vp 85 行 + * def socket(family: INT, type: INT, protocol: INT) -> u64 | t.CExtern | t.CExport: pass + * + * 运行:npx jest src/tests/viperExternReturn.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 TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; +const SOCKET_PATH = path.join(TRANSPYC_INCLUDES, 'socket.vp'); + +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, execEnv, uri, configOptions }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map( + (diag) => `[category=${diag.category}] ${diag.message}` + ); +} + +test('socket.vp 中声明性函数 socket() 返回类型含 CExtern/CExport 不报 returnMissing', () => { + const { program, serviceProvider, uri, configOptions } = buildProgram(SOCKET_PATH); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== socket.vp return diagnostics ==='); + const returnDiags = diagnostics.filter((d) => d.includes('must return value') || d.includes('return') || d.includes('返回')); + returnDiags.forEach((d) => console.log(` ${d}`)); + + // socket.vp 85 行 def socket() -> u64 | t.CExtern | t.CExport: pass + const missingReturn = diagnostics.filter((d) => d.includes('must return value') || d.includes('所有代码路径')); + assert.strictEqual( + missingReturn.length, + 0, + `unexpected returnMissing errors: ${JSON.stringify(returnDiags)}` + ); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('最小复现:u64 | t.CExtern | t.CExport 声明性函数不报 returnMissing', () => { + 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 + +def socket(family: t.CInt, type: t.CInt, protocol: t.CInt) -> t.CUInt64T | t.CExtern | t.CExport: pass +`; + const testDir = path.join(TRANSPYC_INCLUDES, '__extern_return_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'extern.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('=== extern.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const missingReturn = diagnostics.filter( + (d) => d.includes('must return value') || d.includes('所有代码路径') + ); + assert.strictEqual( + missingReturn.length, + 0, + `unexpected returnMissing errors: ${JSON.stringify(diagnostics)}` + ); + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); diff --git a/packages/vipeye-internal/src/tests/viperIfElseRedecl.test.ts b/packages/vipeye-internal/src/tests/viperIfElseRedecl.test.ts new file mode 100644 index 00000000..acec962f --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperIfElseRedecl.test.ts @@ -0,0 +1,149 @@ +/* + * viperIfElseRedecl.test.ts + * + * 验证 Viper 扩展:if/else 互斥分支中的同名变量声明不应被判定为遮蔽 + * (reportRedeclaration 误报),与 socket.vp 中 accept 方法的场景一致。 + * + * 场景(用户反馈): + * def accept(...): + * if platmacro.IS_WINDOWS: + * client_fd: u64 = w32.winsock2.accept(...) + * ... + * client_fd: INT = accept(...) # ← 339 行,被报 "obscured by a declaration of the same name" + * + * 运行:npx jest src/tests/viperIfElseRedecl.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 TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes'; +const SOCKET_PATH = path.join(TRANSPYC_INCLUDES, 'socket.vp'); + +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, execEnv, uri, configOptions }; +} + +function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): string[] { + const sourceFile = program.getSourceFile(uri); + assert(sourceFile, 'source file not found'); + return (sourceFile.getDiagnostics(configOptions) || []).map( + (diag) => `[category=${diag.category}] ${diag.message}` + ); +} + +test('socket.vp accept 方法中 if/else 分支同名 client_fd 不报遮蔽', () => { + const { program, serviceProvider, uri, configOptions } = buildProgram(SOCKET_PATH); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== socket.vp client_fd diagnostics ==='); + const clientFdDiags = diagnostics.filter( + (d) => d.includes('client_fd') || d.includes('obscured') + ); + clientFdDiags.forEach((d) => console.log(` ${d}`)); + + // 不应有 "obscured" 遮蔽错误(if/else 互斥分支的同名变量声明是合法的) + const obscuredErrors = clientFdDiags.filter((d) => d.includes('obscured')); + assert.strictEqual( + obscuredErrors.length, + 0, + `unexpected obscured errors: ${JSON.stringify(clientFdDiags)}` + ); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('简单 if/else 互斥分支同名变量不同型不报遮蔽', () => { + 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\n\ndef test(x: t.CInt):\n if x > 0:\n client_fd: t.CUInt64T = t.CUInt64T(1)\n return client_fd\n client_fd: t.CInt = t.CInt(2)\n return client_fd\n`; + const testDir = path.join(TRANSPYC_INCLUDES, '__ifelse_redecl_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'ifelse.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('=== ifelse.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const obscuredErrors = diagnostics.filter((d) => d.includes('obscured')); + assert.strictEqual( + obscuredErrors.length, + 0, + `unexpected obscured errors: ${JSON.stringify(diagnostics)}` + ); + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +}); diff --git a/packages/vscode-vipeye/package.json b/packages/vscode-vipeye/package.json index cc5fbef8..88ff02b1 100644 --- a/packages/vscode-vipeye/package.json +++ b/packages/vscode-vipeye/package.json @@ -77,6 +77,7 @@ "variable" ], "function": [ + "entity.name.function.viper", "entity.name.function" ], "class": [