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:
TermiNexus committed 2026-09-22 16:13:43 +08:00
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;
};
}
+72 -1
View File
@@ -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);
});