修复了 CExtern CExport 保留机制等
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
This commit is contained in:
1 parent
b3c84af8a7
commit
024366a85a
12 files changed
+675
-3
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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*'}"
|
||||
|
||||
@@ -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 之前的版本中,多个异常类型必须用括号括起来",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -77,6 +77,7 @@
|
||||
"variable"
|
||||
],
|
||||
"function": [
|
||||
"entity.name.function.viper",
|
||||
"entity.name.function"
|
||||
],
|
||||
"class": [
|
||||
|
||||
Reference in new issue
Block a user