From d9559869e7810d286ddde4e8fd15e21f757beb16 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 22 Sep 2026 16:13:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(vipeye-internal):=20=E6=96=B0=E5=A2=9E=20[?= =?UTF-8?q?SIZE]T=20=E6=95=B0=E7=BB=84=E7=B1=BB=E5=9E=8B=E8=AF=AD=E6=B3=95?= =?UTF-8?q?=EF=BC=88=E8=84=B1=E7=B3=96=E4=B8=BA=20t.CArray[T,=20SIZE]?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `[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) --- .../src/analyzer/parseTreeUtils.ts | 14 + .../src/analyzer/typeEvaluator.ts | 57 +++- .../src/languageService/hoverProvider.ts | 37 ++- .../vipeye-internal/src/parser/parseNodes.ts | 9 + packages/vipeye-internal/src/parser/parser.ts | 73 ++++- .../src/tests/viperBracketArrayType.test.ts | 281 ++++++++++++++++++ 6 files changed, 456 insertions(+), 15 deletions(-) create mode 100644 packages/vipeye-internal/src/tests/viperBracketArrayType.test.ts diff --git a/packages/vipeye-internal/src/analyzer/parseTreeUtils.ts b/packages/vipeye-internal/src/analyzer/parseTreeUtils.ts index a5e76556..ef69ee62 100644 --- a/packages/vipeye-internal/src/analyzer/parseTreeUtils.ts +++ b/packages/vipeye-internal/src/analyzer/parseTreeUtils.ts @@ -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) + '[' + diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index c26c830f..525b8f43 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -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) { diff --git a/packages/vipeye-internal/src/languageService/hoverProvider.ts b/packages/vipeye-internal/src/languageService/hoverProvider.ts index c2468a63..3656b3de 100644 --- a/packages/vipeye-internal/src/languageService/hoverProvider.ts +++ b/packages/vipeye-internal/src/languageService/hoverProvider.ts @@ -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) { diff --git a/packages/vipeye-internal/src/parser/parseNodes.ts b/packages/vipeye-internal/src/parser/parseNodes.ts index 78a14191..bfc1850c 100644 --- a/packages/vipeye-internal/src/parser/parseNodes.ts +++ b/packages/vipeye-internal/src/parser/parseNodes.ts @@ -1495,6 +1495,11 @@ export interface IndexNode extends ParseNodeBase { 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 { d: { token: IdentifierToken; value: string; + // Viper 扩展:`[SIZE]T` 数组语法脱糖(见 parser._parseViperBracketArrayType) + // 时合成出来的 `t` 名:零长度、不对应源码字符。文件未 import t 时其解析 + // 失败须静默退化为 Unknown,不报"未定义 t"。 + isViperBracketArraySynthetic?: boolean; }; } diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index 912884b8..ae5560c9 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -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) 嵌套表示: diff --git a/packages/vipeye-internal/src/tests/viperBracketArrayType.test.ts b/packages/vipeye-internal/src/tests/viperBracketArrayType.test.ts new file mode 100644 index 00000000..d55457a6 --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperBracketArrayType.test.ts @@ -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 { + const evaluator = program.evaluator as TypeEvaluator | undefined; + assert(evaluator); + const ev = evaluator; + const result = new Map(); + + 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); +});