feat(vipeye-internal): 新增 [SIZE]T 数组类型语法(脱糖为 t.CArray[T, SIZE])
- `[SIZE]T` 等价 t.CArray[T, SIZE],`[]T` 等价 t.CArray[T](不定长) - 支持整数字面量 / t.CDefine 常量长度、多维嵌套 [2][3]T、指针元素 [4]T* - 支持变量、参数、返回值、类成员与 type X = 注解位置 - 解析层脱糖:AST 与手写 t.CArray[T, SIZE] 完全一致,元素类型/索引返回 T/ 数组名退化为指针/字符数组降级 str·bytes 等语义零分叉 - 下标实参按源码位置排序(AST 子节点不重叠不变量),getTypeArgs 内按语义 顺序重排实参;合成 t/CArray 名为零长度,不产生多余语义高亮 - 不在下标实参内生效,避免把 t.Callable[[], T] 误判为数组语法 - 源文件未 import t 时静默退化为 Unknown(不报未定义 t/成员访问失败) - hover 与诊断按源码原形显示(如 buf: [4]t.CInt)
This commit is contained in:
1 parent
a6299ab878
commit
d9559869e7
6 files changed
+456
-15
No files matched your search
@@ -230,6 +230,20 @@ export function printExpression(node: ExpressionNode, flags = PrintExpressionFla
|
||||
}
|
||||
|
||||
case ParseNodeType.Index: {
|
||||
// Viper 扩展:`[SIZE]T` 数组语法脱糖出的下标节点,按源码原形打印
|
||||
// (items 为 [长度, 元素类型],省略长度时只有 [元素类型]),
|
||||
// 使诊断/悬停显示用户实际写下的数组类型而非脱糖后的 t.CArray[...]。
|
||||
if (node.d.isViperBracketArray === true) {
|
||||
const sizeArg = node.d.items.length > 1 ? node.d.items[0] : undefined;
|
||||
const elementArg = node.d.items[node.d.items.length - 1];
|
||||
return (
|
||||
'[' +
|
||||
(sizeArg ? printArg(sizeArg, flags) : '') +
|
||||
']' +
|
||||
printExpression(elementArg.d.valueExpr, flags)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
printExpression(node.d.leftExpr, flags) +
|
||||
'[' +
|
||||
|
||||
@@ -5358,6 +5358,12 @@ export function createTypeEvaluator(
|
||||
// Handle the special case of "reveal_type" and "reveal_locals".
|
||||
if (name === 'reveal_type' || name === 'reveal_locals') {
|
||||
type = AnyType.create();
|
||||
} else if (node.d.isViperBracketArraySynthetic) {
|
||||
// Viper 扩展:`[SIZE]T` 数组语法脱糖出的合成 `t` 名
|
||||
// (见 parser._parseViperBracketArrayType)。源文件未 import t 时
|
||||
// 该名无法解析,静默退化为 Unknown(不报"未定义 t"),
|
||||
// 数组类型语义随之退化;正常 Viper 源码均带 import t。
|
||||
type = UnknownType.create();
|
||||
} else {
|
||||
// Viper 扩展:前向引用支持(收紧版)。
|
||||
// Viper 是 AOT 语言,函数/类的定义顺序无关(类似 C 语言的前向声明),
|
||||
@@ -6288,7 +6294,10 @@ export function createTypeEvaluator(
|
||||
if (
|
||||
isUnknown(baseType) &&
|
||||
!(baseType.priv.isIncomplete ?? false) &&
|
||||
IsViperDialect(fileInfo.executionEnvironment.dialect)
|
||||
IsViperDialect(fileInfo.executionEnvironment.dialect) &&
|
||||
// Viper 扩展:`[SIZE]T` 脱糖出的合成 t.CArray 成员访问,
|
||||
// 合成 `t` 未解析(源文件未 import t)时静默退化,不报此警告。
|
||||
!isViperBracketArraySyntheticMember(node)
|
||||
) {
|
||||
fileInfo.diagnosticSink.addDiagnosticWithTextRange(
|
||||
'warning',
|
||||
@@ -6749,12 +6758,17 @@ export function createTypeEvaluator(
|
||||
? DiagnosticRule.reportFunctionMemberAccess
|
||||
: DiagnosticRule.reportAttributeAccessIssue;
|
||||
|
||||
addDiagnostic(
|
||||
rule,
|
||||
diagMessage.format({ name: memberName, type: printType(baseType) }) + diag.getString(),
|
||||
node.d.member,
|
||||
diag.getEffectiveTextRange() ?? node.d.member
|
||||
);
|
||||
// Viper 扩展:`[SIZE]T` 数组语法脱糖出的合成 `t.CArray` 成员访问,
|
||||
// 当合成 `t` 未解析(源文件未 import t)时静默退化为 Unknown,
|
||||
// 不再叠加"无法解析成员"诊断(见 parser._parseViperBracketArrayType)。
|
||||
if (!isViperBracketArraySyntheticMember(node)) {
|
||||
addDiagnostic(
|
||||
rule,
|
||||
diagMessage.format({ name: memberName, type: printType(baseType) }) + diag.getString(),
|
||||
node.d.member,
|
||||
diag.getEffectiveTextRange() ?? node.d.member
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is member access on a function, use "Any" so if the
|
||||
@@ -8711,6 +8725,16 @@ export function createTypeEvaluator(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Viper 扩展:判断成员访问是否是 `[SIZE]T` 数组语法脱糖出的合成 `t.CArray`
|
||||
// 成员访问(见 parser._parseViperBracketArrayType)。合成 `t` 名零长度、
|
||||
// 不对应源码字符,未 import t 时其解析失败须静默退化,相关诊断一律跳过。
|
||||
function isViperBracketArraySyntheticMember(node: MemberAccessNode): boolean {
|
||||
return (
|
||||
node.d.leftExpr.nodeType === ParseNodeType.Name &&
|
||||
node.d.leftExpr.d.isViperBracketArraySynthetic === true
|
||||
);
|
||||
}
|
||||
|
||||
// Viper 扩展:REnum 变体字段类型(仅取类型,见 getViperREnumVariantFieldInfo)。
|
||||
function getViperREnumVariantFieldType(renumType: ClassType, memberName: string): Type | undefined {
|
||||
return getViperREnumVariantFieldInfo(renumType, memberName)?.type;
|
||||
@@ -9046,6 +9070,13 @@ export function createTypeEvaluator(
|
||||
let adjFlags = flags | EvalFlags.NoConvertSpecialForm;
|
||||
adjFlags &= ~EvalFlags.TypeFormArg;
|
||||
|
||||
// Viper 扩展:`[SIZE]T` 数组语法脱糖出的下标节点,items 按源码顺序为
|
||||
// [长度, 元素类型](AST 要求子节点按位置递增不重叠),而语义顺序是
|
||||
// [元素类型, 长度](等价 t.CArray[T, SIZE]),故在此反转实参顺序,
|
||||
// 使后续类型参数个数校验、特化完全复用 t.CArray 的既有逻辑。
|
||||
const indexItems =
|
||||
node.d.isViperBracketArray === true ? [...node.d.items].reverse() : node.d.items;
|
||||
|
||||
const allowFinalClassVar = () => {
|
||||
// If the annotation is a variable within the body of a dataclass, a
|
||||
// Final is allowed with a ClassVar annotation. In all other cases,
|
||||
@@ -9123,22 +9154,22 @@ export function createTypeEvaluator(
|
||||
|
||||
// A tuple is treated the same as a list of items in the index.
|
||||
if (
|
||||
node.d.items.length === 1 &&
|
||||
indexItems.length === 1 &&
|
||||
!node.d.trailingComma &&
|
||||
!node.d.items[0].d.name &&
|
||||
node.d.items[0].d.valueExpr.nodeType === ParseNodeType.Tuple
|
||||
!indexItems[0].d.name &&
|
||||
indexItems[0].d.valueExpr.nodeType === ParseNodeType.Tuple
|
||||
) {
|
||||
node.d.items[0].d.valueExpr.d.items.forEach((item, index) => {
|
||||
indexItems[0].d.valueExpr.d.items.forEach((item, index) => {
|
||||
typeArgs.push(getTypeArgTypeResult(item, index));
|
||||
});
|
||||
|
||||
// Set the node's type so it isn't reevaluated later.
|
||||
setTypeResultForNode(node.d.items[0].d.valueExpr, { type: UnknownType.create() });
|
||||
setTypeResultForNode(indexItems[0].d.valueExpr, { type: UnknownType.create() });
|
||||
|
||||
return typeArgs;
|
||||
}
|
||||
|
||||
node.d.items.forEach((arg, index) => {
|
||||
indexItems.forEach((arg, index) => {
|
||||
const typeResult = getTypeArgTypeResult(arg.d.valueExpr, index);
|
||||
|
||||
if (arg.d.argCategory !== ArgCategory.Simple) {
|
||||
|
||||
@@ -51,6 +51,7 @@ import { Uri } from '../common/uri/uri';
|
||||
import {
|
||||
ExpressionNode,
|
||||
FunctionNode,
|
||||
IndexNode,
|
||||
NameNode,
|
||||
ParamCategory,
|
||||
ParameterNode,
|
||||
@@ -240,6 +241,25 @@ function getViperEnumMemberTypeText(declaration: VariableDeclaration | undefined
|
||||
return value === undefined ? `${name}: ${className}` : `${name}: ${className} = ${value}`;
|
||||
}
|
||||
|
||||
// Viper 扩展:判断类型注解是否是/包含 `[SIZE]T` 数组语法(脱糖后的下标节点,
|
||||
// 见 parser._parseViperBracketArrayType)。用于让 hover 按源码原形显示
|
||||
// (如 `[4]t.CInt`)而非脱糖后的 `CArray[CInt, 4]`。联合类型逐操作数判断。
|
||||
function hasViperBracketArrayAnnotation(node: ExpressionNode | undefined): boolean {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
if (node.nodeType === ParseNodeType.Index && (node as IndexNode).d.isViperBracketArray === true) {
|
||||
return true;
|
||||
}
|
||||
if (node.nodeType === ParseNodeType.BinaryOperation) {
|
||||
return (
|
||||
hasViperBracketArrayAnnotation(node.d.leftExpr) ||
|
||||
hasViperBracketArrayAnnotation(node.d.rightExpr)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getVariableTypeText(
|
||||
evaluator: TypeEvaluator,
|
||||
declaration: VariableDeclaration | undefined,
|
||||
@@ -262,6 +282,13 @@ export function getVariableTypeText(
|
||||
return `(${label}) ` + viperEnumMemberText;
|
||||
}
|
||||
|
||||
// Viper 扩展:`[SIZE]T` 数组语法注解按源码原形显示(如 `x: [4]t.CInt`),
|
||||
// 而不是脱糖后的 `CArray[CInt, 4]`,与用户书写形态保持一致。
|
||||
const bracketArrayAnnotation = declaration?.typeAnnotationNode;
|
||||
if (bracketArrayAnnotation && hasViperBracketArrayAnnotation(bracketArrayAnnotation)) {
|
||||
return `(${label}) ${name}: ` + ParseTreeUtils.printExpression(bracketArrayAnnotation);
|
||||
}
|
||||
|
||||
let typeVarName: string | undefined;
|
||||
|
||||
if (type.props?.typeAliasInfo && typeNode.nodeType === ParseNodeType.Name) {
|
||||
@@ -585,7 +612,15 @@ export class HoverProvider {
|
||||
// (parameter) fb: UINT32PTR
|
||||
// (typedef) UINT32PTR = CPtr[CUInt32T]
|
||||
const paramType = this._getType(node);
|
||||
let paramLine = '(parameter) ' + node.d.value + this._getTypeText(node);
|
||||
// Viper 扩展:`[SIZE]T` 数组语法注解按源码原形显示(如 `[4]t.CInt`)。
|
||||
const paramAnnotation = resolvedDecl.node.d.annotation;
|
||||
let paramLine =
|
||||
paramAnnotation && hasViperBracketArrayAnnotation(paramAnnotation)
|
||||
? '(parameter) ' +
|
||||
node.d.value +
|
||||
': ' +
|
||||
ParseTreeUtils.printExpression(paramAnnotation)
|
||||
: '(parameter) ' + node.d.value + this._getTypeText(node);
|
||||
if (paramType?.props?.typeAliasInfo && node.nodeType === ParseNodeType.Name) {
|
||||
const aliasInfo = getTypeAliasInfo(paramType);
|
||||
if (aliasInfo && aliasInfo.shared.name !== node.d.value) {
|
||||
|
||||
@@ -1495,6 +1495,11 @@ export interface IndexNode extends ParseNodeBase<ParseNodeType.Index> {
|
||||
leftExpr: ExpressionNode;
|
||||
items: ArgumentNode[];
|
||||
trailingComma: boolean;
|
||||
// Viper 扩展:`[SIZE]T` 数组语法脱糖出的下标节点(见
|
||||
// parser._parseViperBracketArrayType)。此时 leftExpr 是合成的 `t.CArray`,
|
||||
// items 按源码顺序为 [长度, 元素类型](省略长度时只有 [元素类型]),
|
||||
// 类型求值按 t.CArray[元素类型, 长度] 的语义顺序解读。
|
||||
isViperBracketArray?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1672,6 +1677,10 @@ export interface NameNode extends ParseNodeBase<ParseNodeType.Name> {
|
||||
d: {
|
||||
token: IdentifierToken;
|
||||
value: string;
|
||||
// Viper 扩展:`[SIZE]T` 数组语法脱糖(见 parser._parseViperBracketArrayType)
|
||||
// 时合成出来的 `t` 名:零长度、不对应源码字符。文件未 import t 时其解析
|
||||
// 失败须静默退化为 Unknown,不报"未定义 t"。
|
||||
isViperBracketArraySynthetic?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5020,7 +5020,7 @@ export class Parser {
|
||||
this._addSyntaxError(LocMessage.unpackedSubscriptIllegal(), startToken);
|
||||
}
|
||||
|
||||
let result = this._parseTestExpression(/* allowAssignmentExpression */ false);
|
||||
let result = this._parseViperBracketArrayTypeIfNeeded();
|
||||
|
||||
// Viper 扩展:C 指针类型后缀 T* / T** / T***(须在还原 _isParsingTypeAnnotation 之前,
|
||||
// 使 _parseArithmeticTerm/_parseArithmeticFactor 的星号守卫仍生效)。
|
||||
@@ -5085,6 +5085,77 @@ export class Parser {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Viper 扩展:定长数组类型语法 `[SIZE]T`(等价 t.CArray[T, SIZE])与
|
||||
// 不定长写法 `[]T`(等价 t.CArray[T],与 `t.CArray[T]` 单参数一致)。
|
||||
// 只在"类型注解起点"生效:
|
||||
// - 值表达式里的 `[SIZE]` 仍是 list 字面量;
|
||||
// - 下标实参内(_isParsingIndexTrailer)不生效,否则
|
||||
// `t.Callable[[], t.CInt]` 这类实参列表会被误当成数组类型。
|
||||
// 因此 `list[[4]int]`(数组类型作为泛型实参)暂不支持,需写 t.CArray。
|
||||
private _parseViperBracketArrayTypeIfNeeded(): ExpressionNode {
|
||||
if (
|
||||
this._parseOptions.dialect === 'viper' &&
|
||||
!this._isParsingIndexTrailer &&
|
||||
this._peekTokenType() === TokenType.OpenBracket
|
||||
) {
|
||||
return this._parseViperBracketArrayType();
|
||||
}
|
||||
return this._parseTestExpression(/* allowAssignmentExpression */ false);
|
||||
}
|
||||
|
||||
// Viper 扩展:解析 `[SIZE]T` / `[]T` 并脱糖为等价的 `t.CArray[T, SIZE]` /
|
||||
// `t.CArray[T]` 下标表达式,使类型求值、语义高亮、类型显示完全复用既有
|
||||
// t.CArray 机制(元素类型、索引返回 T、数组名退化为指针、字符数组降级
|
||||
// str/bytes、单参数不定长等),不引入第二套语义。
|
||||
// 元素类型递归解析,故支持 `[2][3]T`(等价 t.CArray[t.CArray[T, 3], 2])、
|
||||
// `[4]T*`(指针后缀)、`[4]T | t.CPtr`(联合)等。
|
||||
private _parseViperBracketArrayType(): ExpressionNode {
|
||||
const openBracketToken = this._getNextToken();
|
||||
assert(openBracketToken.type === TokenType.OpenBracket);
|
||||
|
||||
let sizeExpr: ExpressionNode | undefined;
|
||||
if (this._peekTokenType() !== TokenType.CloseBracket) {
|
||||
sizeExpr = this._parseTestExpression(/* allowAssignmentExpression */ false);
|
||||
}
|
||||
|
||||
const closeBracketToken = this._peekToken();
|
||||
if (!this._consumeTokenIfType(TokenType.CloseBracket)) {
|
||||
this._addSyntaxError(LocMessage.expectedCloseBracket(), this._peekToken());
|
||||
}
|
||||
|
||||
// 元素类型:递归走注解解析,天然支持嵌套 `[2][3]T` 与指针后缀 `[4]T*`。
|
||||
const elementType = this._parseTypeAnnotation();
|
||||
|
||||
// 合成 `t.CArray`:零长度、位置落在 '[' 上,因此不产生任何语义高亮
|
||||
// (semanticTokenProvider 跳过 length <= 0 的名字)也不参与位置命中。
|
||||
const syntheticTNameNode = NameNode.create(
|
||||
IdentifierToken.create(openBracketToken.start, 0, 't', /* comments */ undefined)
|
||||
);
|
||||
syntheticTNameNode.d.isViperBracketArraySynthetic = true;
|
||||
const cArrayNameNode = NameNode.create(
|
||||
IdentifierToken.create(openBracketToken.start, 0, 'CArray', /* comments */ undefined)
|
||||
);
|
||||
const memberAccessNode = MemberAccessNode.create(syntheticTNameNode, cArrayNameNode);
|
||||
|
||||
// items 按**源码顺序**排列([长度] 在 '[' 内、元素类型在 ']' 之后),
|
||||
// 以满足"子节点必须按位置递增且不重叠"的 AST 不变量;
|
||||
// 类型求值在 getTypeArgs 中按语义顺序(元素类型在前)重排实参。
|
||||
const items: ArgumentNode[] = [];
|
||||
if (sizeExpr) {
|
||||
items.push(ArgumentNode.create(/* startToken */ undefined, sizeExpr, ArgCategory.Simple));
|
||||
}
|
||||
items.push(ArgumentNode.create(/* startToken */ undefined, elementType, ArgCategory.Simple));
|
||||
|
||||
const indexNode = IndexNode.create(memberAccessNode, items, /* trailingComma */ false, closeBracketToken);
|
||||
indexNode.d.isViperBracketArray = true;
|
||||
|
||||
// 让整个下标节点的范围覆盖 `[SIZE]T` 全文(含 ']' 之后的元素类型),
|
||||
// 使 hover/定位落在元素类型内时仍能沿父链找到该数组类型节点。
|
||||
extendRange(indexNode, elementType);
|
||||
|
||||
return indexNode;
|
||||
}
|
||||
|
||||
// Viper 扩展:解析 C 指针类型后缀 `T*` / `T**` / `T***`。
|
||||
// 每个 '*' 对应一层指针;'**' 词法化为单个 Power 算子、等价两层指针。
|
||||
// 结果用 UnaryOperationNode(operator=Multiply) 嵌套表示:
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* viperBracketArrayType.test.ts
|
||||
*
|
||||
* 验证 Viper 定长数组类型新语法 `[SIZE]T`(等价 t.CArray[T, SIZE])与
|
||||
* 不定长写法 `[]T`(等价 t.CArray[T]):
|
||||
* - 注解类型与对应的 t.CArray[...] 完全一致(打印结果字符串相同)
|
||||
* - 支持多维嵌套 `[2][3]T`、指针元素 `[4]T*`、t.CDefine 常量长度 `[BUF]T`
|
||||
* - 不再产生"List 表达式"/"语句必须用换行符或分号分隔"等误报
|
||||
* - 语义高亮不出现 undefined(合成 t / CArray 为零长度名字,不参与高亮)
|
||||
*
|
||||
* 运行:npx jest src/tests/viperBracketArrayType.test.ts --runInBand
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import * as path from 'path';
|
||||
import { CancellationToken, MarkupContent, MarkupKind, Position } from 'vscode-languageserver';
|
||||
|
||||
import { ImportResolver } from '../analyzer/importResolver';
|
||||
import { Program } from '../analyzer/program';
|
||||
import { ParseTreeWalker } from '../analyzer/parseTreeWalker';
|
||||
import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes';
|
||||
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 { HoverProvider } from '../languageService/hoverProvider';
|
||||
import { SemanticTokenTypes } from '../languageService/semanticTokenTypes';
|
||||
import { FunctionNode } from '../parser/parseNodes';
|
||||
|
||||
(global as any).__rootDirectory = path.resolve();
|
||||
|
||||
function buildProgram(source: string) {
|
||||
const tempFile = new RealTempFile();
|
||||
const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile);
|
||||
const fs = require('fs');
|
||||
const testRoot = path.join(__dirname, '__bracket_array_type__');
|
||||
fs.rmSync(testRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(testRoot, { recursive: true });
|
||||
const filePath = path.join(testRoot, 'main.vp');
|
||||
fs.writeFileSync(filePath, source, 'utf8');
|
||||
|
||||
const projectRoot = UriEx.file(testRoot);
|
||||
const configOptions = new ConfigOptions(projectRoot);
|
||||
configOptions.internalTestMode = true;
|
||||
configOptions.initializeTypeCheckingMode('standard');
|
||||
|
||||
const execEnv = new ExecutionEnvironment(
|
||||
'python',
|
||||
projectRoot,
|
||||
configOptions.diagnosticRuleSet,
|
||||
configOptions.defaultPythonVersion,
|
||||
configOptions.defaultPythonPlatform,
|
||||
[],
|
||||
/* 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(filePath);
|
||||
program.setTrackedFiles([uri]);
|
||||
while (program.analyze()) {
|
||||
// 循环
|
||||
}
|
||||
|
||||
const sourceFile = program.getSourceFile(uri);
|
||||
const parseResults = sourceFile!.getParseResults();
|
||||
return { program, configOptions, parseResults, uri, serviceProvider };
|
||||
}
|
||||
|
||||
function cleanup(program: Program, serviceProvider: any) {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(path.join(__dirname, '__bracket_array_type__'), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// 收集函数第一个参数注解的打印类型(展开类型别名)
|
||||
function collectFirstParamTypes(program: Program, parseResults: any): Map<string, string> {
|
||||
const evaluator = program.evaluator as TypeEvaluator | undefined;
|
||||
assert(evaluator);
|
||||
const ev = evaluator;
|
||||
const result = new Map<string, string>();
|
||||
|
||||
class ParamCollector extends ParseTreeWalker {
|
||||
override visitFunction(node: FunctionNode) {
|
||||
const firstParam = node.d.params[0];
|
||||
const ann = (firstParam as any)?.d?.annotation;
|
||||
if (ann) {
|
||||
const type = ev.getType(ann as any);
|
||||
result.set(node.d.name.d.value, type ? program.printType(type, { expandTypeAlias: true }) : '(undefined)');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new ParamCollector().walk(parseResults!.parserOutput.parseTree);
|
||||
return result;
|
||||
}
|
||||
|
||||
const SOURCE = `import t
|
||||
|
||||
BUF: t.CDefine = 8
|
||||
|
||||
def fa(x: [4]t.CInt) -> t.CInt:
|
||||
return x[0]
|
||||
|
||||
def fb(x: t.CArray[t.CInt, 4]) -> t.CInt:
|
||||
return x[0]
|
||||
|
||||
def fc(x: []t.CInt) -> t.CInt:
|
||||
return x[0]
|
||||
|
||||
def fd(x: t.CArray[t.CInt]) -> t.CInt:
|
||||
return x[0]
|
||||
|
||||
def fe(x: [2][3]t.CInt) -> t.CInt:
|
||||
return x[0][0]
|
||||
|
||||
def ff(x: [4]int) -> int:
|
||||
return x[0]
|
||||
|
||||
def fg(x: [BUF]t.CInt) -> t.CInt:
|
||||
return x[0]
|
||||
|
||||
def fh(x: [4]t.CInt*) -> t.CInt:
|
||||
return x[0][0]
|
||||
|
||||
def fi(x: t.Callable[[], t.CInt]) -> t.CInt:
|
||||
return 0
|
||||
|
||||
def fj(x: [4]t.CArray[t.CInt, 2]) -> t.CInt:
|
||||
return x[0][0]
|
||||
`;
|
||||
|
||||
test('[SIZE]T 数组语法:类型与 t.CArray[T, SIZE] 完全一致且无诊断', () => {
|
||||
const { program, configOptions, parseResults, uri, serviceProvider } = buildProgram(SOURCE);
|
||||
|
||||
const diagnostics = (program.getSourceFile(uri)?.getDiagnostics(configOptions) || []).map(
|
||||
(d) => `[category=${d.category}][line=${d.range ? d.range.start.line + 1 : -1}] ${d.message.split('\n')[0]}`
|
||||
);
|
||||
const errors = diagnostics.filter((d) => d.startsWith('[category=0]') || d.startsWith('[category=1]'));
|
||||
assert.strictEqual(errors.length, 0, `不应有 error/warning,实际: ${JSON.stringify(errors)}`);
|
||||
|
||||
const types = collectFirstParamTypes(program, parseResults!);
|
||||
// 定长:与 t.CArray[t.CInt, 4] 打印结果相同
|
||||
assert.strictEqual(types.get('fa'), types.get('fb'), `[4]t.CInt 应等于 t.CArray[t.CInt, 4],实际: ${types.get('fa')} vs ${types.get('fb')}`);
|
||||
assert(types.get('fa')!.includes('CArray'), `[4]t.CInt 应为 CArray,实际: ${types.get('fa')}`);
|
||||
// 不定长(省略 SIZE):与 t.CArray[t.CInt] 相同
|
||||
assert.strictEqual(types.get('fc'), types.get('fd'), `[]t.CInt 应等于 t.CArray[t.CInt],实际: ${types.get('fc')} vs ${types.get('fd')}`);
|
||||
// 多维嵌套
|
||||
assert(
|
||||
types.get('fe')!.includes('CArray[CArray['),
|
||||
`[2][3]t.CInt 应为嵌套 CArray,实际: ${types.get('fe')}`
|
||||
);
|
||||
// 元素类型为 int(长度以 Literal 形式保留,与 t.CArray[int, 4] 一致)
|
||||
assert(
|
||||
types.get('ff')!.includes('CArray[int, Literal[4]]'),
|
||||
`[4]int 应为 CArray[int, 4],实际: ${types.get('ff')}`
|
||||
);
|
||||
// 长度为 t.CDefine 常量
|
||||
assert(
|
||||
types.get('fg')!.includes('CArray[CInt, '),
|
||||
`[BUF]t.CInt 长度应为定义常量,实际: ${types.get('fg')}`
|
||||
);
|
||||
// 元素类型为指针
|
||||
assert(
|
||||
types.get('fh')!.includes('CArray[rawptr[') || types.get('fh')!.includes('CArray[CPtr['),
|
||||
`[4]t.CInt* 元素应为指针,实际: ${types.get('fh')}`
|
||||
);
|
||||
// 回归:下标实参以 '[' 开头的注解(t.Callable[[], T])不得被当成数组语法
|
||||
// (解析为函数类型,展开别名后形如 type[() -> CInt])
|
||||
assert(
|
||||
types.get('fi')!.includes('->'),
|
||||
`t.Callable[[], t.CInt] 应正常解析为函数类型,实际: ${types.get('fi')}`
|
||||
);
|
||||
// 元素类型为普通下标类型
|
||||
assert(
|
||||
types.get('fj')!.includes('CArray[CArray['),
|
||||
`[4]t.CArray[t.CInt, 2] 应解析为嵌套 CArray,实际: ${types.get('fj')}`
|
||||
);
|
||||
|
||||
cleanup(program, serviceProvider);
|
||||
});
|
||||
|
||||
test('[SIZE]T 数组语法:语义高亮不出现 undefined', () => {
|
||||
const { program, parseResults, uri, serviceProvider } = buildProgram(SOURCE);
|
||||
|
||||
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 lines = parseResults!.text.split('\n');
|
||||
const tokens: { line: number; text: string; type: number }[] = [];
|
||||
let cursorLine = 0;
|
||||
let cursorChar = 0;
|
||||
for (let i = 0; i < result.data.length; i += 5) {
|
||||
cursorLine += result.data[i];
|
||||
cursorChar = result.data[i] === 0 ? cursorChar + result.data[i + 1] : result.data[i + 1];
|
||||
tokens.push({
|
||||
line: cursorLine + 1,
|
||||
text: (lines[cursorLine] || '').substring(cursorChar, cursorChar + result.data[i + 2]),
|
||||
type: result.data[i + 3],
|
||||
});
|
||||
}
|
||||
|
||||
const undefinedTokens = tokens.filter((t) => t.type === SemanticTokenTypes.undefined);
|
||||
assert.strictEqual(
|
||||
undefinedTokens.length,
|
||||
0,
|
||||
`不应有 undefined 高亮(合成 t/CArray 为零长度,不参与高亮),实际: ${JSON.stringify(undefinedTokens)}`
|
||||
);
|
||||
// `t.CInt` 里的 CInt 仍按类型(class)着色
|
||||
assert(
|
||||
tokens.some((t) => t.text === 'CInt' && t.type === SemanticTokenTypes.class),
|
||||
`CInt 应显示为 class,实际: ${JSON.stringify(tokens.filter((t) => t.text === 'CInt'))}`
|
||||
);
|
||||
// 长度数字仍按 number 着色
|
||||
assert(
|
||||
tokens.some((t) => t.text === '4' && t.type === SemanticTokenTypes.number),
|
||||
`长度 4 应显示为 number,实际: ${JSON.stringify(tokens.filter((t) => t.text === '4'))}`
|
||||
);
|
||||
|
||||
cleanup(program, serviceProvider);
|
||||
});
|
||||
|
||||
test('[SIZE]T 数组语法:hover 按源码原形显示', () => {
|
||||
const source = `import t
|
||||
|
||||
buf: [4]t.CInt = []
|
||||
grid: [2][3]t.CInt = []
|
||||
|
||||
|
||||
def f(x: [8]t.CInt) -> t.CInt:
|
||||
return x[0]
|
||||
`;
|
||||
const { program, uri, serviceProvider } = buildProgram(source);
|
||||
|
||||
const getHoverText = (line: number, character: number): string => {
|
||||
const provider = new HoverProvider(
|
||||
program as any,
|
||||
uri,
|
||||
{ line, character } as Position,
|
||||
MarkupKind.Markdown,
|
||||
CancellationToken.None
|
||||
);
|
||||
const hover = provider.getHover();
|
||||
return hover ? (hover.contents as MarkupContent).value : '(无 hover)';
|
||||
};
|
||||
|
||||
const bufHover = getHoverText(2, 1);
|
||||
assert(bufHover.includes('[4]t.CInt'), `buf hover 应显示源码原形 [4]t.CInt,实际: ${bufHover}`);
|
||||
const gridHover = getHoverText(3, 1);
|
||||
assert(
|
||||
gridHover.includes('[2][3]t.CInt'),
|
||||
`grid hover 应显示源码原形 [2][3]t.CInt,实际: ${gridHover}`
|
||||
);
|
||||
const paramHover = getHoverText(6, 6);
|
||||
assert(paramHover.includes('[8]t.CInt'), `参数 hover 应显示源码原形 [8]t.CInt,实际: ${paramHover}`);
|
||||
|
||||
cleanup(program, serviceProvider);
|
||||
});
|
||||
|
||||
test('[SIZE]T 数组语法:源文件未 import t 时静默退化(不报未定义 t/成员访问失败)', () => {
|
||||
const source = `a: [4]int = []
|
||||
b: [8]bool = []
|
||||
`;
|
||||
const { program, configOptions, uri, serviceProvider } = buildProgram(source);
|
||||
|
||||
const diagnostics = (program.getSourceFile(uri)?.getDiagnostics(configOptions) || []).map(
|
||||
(d) => `[category=${d.category}][line=${d.range ? d.range.start.line + 1 : -1}] ${d.message.split('\n')[0]}`
|
||||
);
|
||||
const errors = diagnostics.filter((d) => d.startsWith('[category=0]') || d.startsWith('[category=1]'));
|
||||
assert.strictEqual(errors.length, 0, `未 import t 时不应报错,实际: ${JSON.stringify(errors)}`);
|
||||
|
||||
cleanup(program, serviceProvider);
|
||||
});
|
||||
Reference in new issue
Block a user