diff --git a/packages/vipeye-internal/src/analyzer/binder.ts b/packages/vipeye-internal/src/analyzer/binder.ts index 79fff4d..adfd89d 100644 --- a/packages/vipeye-internal/src/analyzer/binder.ts +++ b/packages/vipeye-internal/src/analyzer/binder.ts @@ -631,9 +631,24 @@ export class Binder extends ParseTreeWalker { // Don't walk the body of the function until we're done analyzing // the current scope. + // Viper 扩展:泛型类的方法需要能引用类的类型参数(如 `self.value = T(v)`)。 + // Python 默认把方法的 parent scope 设为最近的非 Class scope(跳过 Class 与 + // typeParamScope),导致方法内 `T` 解析到模块级符号而非本类 type param。 + // 当 enclosingClass 声明了 typeParams(PEP 695 `class Box[T]`)时,将方法 + // parent 设为 Class scope,使查找链 Function → Class → typeParamScope 可达 + // 类型参数。类成员(如 `value: T`)仍通过 self 访问,不受影响。 + let methodParentScope: Scope | undefined; + if ( + containingClassNode && + containingClassNode.d.typeParams && + IsViperDialect(this._fileInfo.executionEnvironment.dialect) + ) { + methodParentScope = AnalyzerNodeInfo.getScope(containingClassNode); + } + this._createNewScope( ScopeType.Function, - typeParamScope ?? this._getNonClassParentScope(), + methodParentScope ?? typeParamScope ?? this._getNonClassParentScope(), /* proxyScope */ undefined, /* chainedModuleLevelScopeLookup */ undefined, () => { diff --git a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts new file mode 100644 index 0000000..a5dbdc3 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts @@ -0,0 +1,162 @@ +/* + * viperGenericTypeVarCall.test.ts + * + * 验证 Viper 泛型类方法中 `T(v)`(用类型变量做 C 风格强转)不应报 + * "Object of type TypeVar is not callable" / "Attribute __call__ is unknown"。 + * + * 场景:TransPyC/Test/GenericTest/App/main.vp + * class Box[T](): + * value: T + * def __init__(self, v: T): + * self.value = T(v) # ← T(v) 类型变量调用 + * + * 运行:npx jest src/tests/viperGenericTypeVarCall.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 GENERIC_TEST = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\Test\\GenericTest\\App\\main.vp'; + +function buildProgram(targetPath: string) { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(path.dirname(targetPath)); + 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('GenericTest 泛型类 Box 中 T(v) 类型变量调用不报 not callable', () => { + const { program, serviceProvider, uri, configOptions } = buildProgram(GENERIC_TEST); + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== GenericTest not-callable diagnostics ==='); + const notCallable = diagnostics.filter( + (d) => d.includes('not callable') || d.includes('not callable') || d.includes('不可调用') + ); + notCallable.forEach((d) => console.log(` ${d}`)); + + assert.strictEqual( + notCallable.length, + 0, + `unexpected not-callable errors: ${JSON.stringify(notCallable)}` + ); + + program.dispose(); + serviceProvider.dispose(); +}); + +test('最小复现:泛型类方法中 T(v) 类型变量调用不报 not callable', () => { + 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 Box[T](): + value: T + + def __init__(self, v: T): + self.value = T(v) + + def get(self) -> T: + return self.value + +def make_box(): + b = Box(42) + return b.get() +`; + const testDir = path.join(TRANSPYC_INCLUDES, '__typevar_call_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'typevar.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('=== typevar.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + const notCallable = diagnostics.filter( + (d) => d.includes('not callable') || d.includes('不可调用') || d.includes('__call__') + ); + assert.strictEqual( + notCallable.length, + 0, + `unexpected not-callable errors: ${JSON.stringify(diagnostics)}` + ); + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); +});