From 4d5a0d203bc5918d7ca5d4e5873625fc42a8e96b Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 27 Sep 2026 13:03:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(vipeye-internal):=20=5F=5Fattribute=5F=5F?= =?UTF-8?q?=20=E5=85=8D=20import=20t/c=20=E4=B8=8E=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A0=8F=E5=88=86=E6=9E=90=E8=BF=9B=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - __attribute__ 脱糖出的 t / c 模块根改为「隐式模块根」(零长度合成名 + isViperImplicitModule 标记),作用域链取不到时回退 builtins 直接查 symbolTable,故不再要求源码 import t / import c;手写裸 t / c 仍报未定义。 补上 builtins.vpi 缺失的 import c(原先只有 import t,装饰器路径一直报 「未定义 c」)。 - 新增左下角状态栏分析进度:逐文件回调 → 后台线程节流 50ms 回传 analysisProgress 消息 → 前台 sendNotification('viper/analysisStatus') → 客户端 StatusBarItem 显示 `C:\xxx\1.vp (4/90) 4.44% loading...`, 分析结束自动隐藏,让用户能区分「插件在忙」与「插件卡死」。 - viperAttribute.test.ts 补回被误删的 hover 用例并新增 2 个回归用例; jest 794 → 796 通过。 --- .gitignore | 2 + .../vipeye-internal/src/analyzer/analysis.ts | 35 ++++++++++- .../src/analyzer/backgroundAnalysisProgram.ts | 11 +++- .../vipeye-internal/src/analyzer/program.ts | 30 ++++++++- .../vipeye-internal/src/analyzer/service.ts | 10 ++- .../src/analyzer/typeEvaluator.ts | 63 ++++++++++++++++--- .../src/backgroundAnalysisBase.ts | 48 +++++++++++++- .../vipeye-internal/src/languageServerBase.ts | 12 +++- .../languageService/viperAttributeUtils.ts | 14 +++-- .../vipeye-internal/src/parser/parseNodes.ts | 8 +++ packages/vipeye-internal/src/parser/parser.ts | 17 ++++- .../src/tests/viperAttribute.test.ts | 58 +++++++++++++++++ .../typeshed-fallback/stdlib/builtins.vpi | 6 ++ packages/vscode-vipeye/CHANGELOG.md | 2 + packages/vscode-vipeye/src/extension.ts | 47 ++++++++++++++ 15 files changed, 339 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 1baa5afc..35ad4b7a 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,8 @@ packages/vipeye-internal/src/tests/__viper_attribute__/ packages/vipeye-internal/src/tests/__viper_attribute_completion__/ packages/vipeye-internal/src/tests/__viper_attribute_hover__/ packages/vipeye-internal/src/tests/__viper_attribute_prefix__/ +packages/vipeye-internal/src/tests/__viper_attribute_noimport__/ +packages/vipeye-internal/src/tests/__viper_attribute_implicit_scope__/ packages/vipeye-internal/src/tests/__viper_volatile__/ packages/vipeye-internal/src/tests/__viper_volatile_bad__/ packages/vipeye-internal/src/tests/__viper_callable__/ diff --git a/packages/vipeye-internal/src/analyzer/analysis.ts b/packages/vipeye-internal/src/analyzer/analysis.ts index 7a677420..b60d19db 100644 --- a/packages/vipeye-internal/src/analyzer/analysis.ts +++ b/packages/vipeye-internal/src/analyzer/analysis.ts @@ -37,6 +37,26 @@ export interface RequiringAnalysisCount { cells: number; } +/** + * Viper 扩展:分析进度的快照,供 LSP 客户端在状态栏显示 + * 「C:\xxx\1.vp (4/90) 0.04% loading...」这类进度信息。 + */ +export interface AnalysisProgress { + /** 当前正在分析的文件路径(文件系统路径,Windows 下形如 C:\xxx\1.vp)。 */ + currentFile: string | undefined; + + /** 已分析的用户文件数。 */ + analyzed: number; + + /** 需要分析的用户文件总数。 */ + total: number; + + /** 是否已分析完毕(true 表示可以收起状态栏进度项)。 */ + done: boolean; +} + +export type AnalysisProgressCallback = (progress: AnalysisProgress) => void; + export type AnalysisCompleteCallback = (results: AnalysisResults) => void; export function analyzeProgram( @@ -45,7 +65,8 @@ export function analyzeProgram( configOptions: ConfigOptions, callback: AnalysisCompleteCallback | undefined, console: ConsoleInterface, - token: CancellationToken + token: CancellationToken, + progressCallback?: AnalysisProgressCallback ): boolean { let moreToAnalyze = false; @@ -55,7 +76,17 @@ export function analyzeProgram( throwIfCancellationRequested(token); const duration = new Duration(); - moreToAnalyze = program.analyze(maxTime, token); + // 逐文件上报进度(analyze 每检查完一个文件就回一次), + // 让客户端状态栏能实时反映「正在分析哪个文件」。 + moreToAnalyze = program.analyze( + maxTime, + token, + progressCallback ? () => progressCallback({ ...program.getAnalysisProgress(), done: false }) : undefined + ); + + if (progressCallback) { + progressCallback({ ...program.getAnalysisProgress(), done: !moreToAnalyze }); + } const requiringAnalysisCount = program.getFilesToAnalyzeCount(); diff --git a/packages/vipeye-internal/src/analyzer/backgroundAnalysisProgram.ts b/packages/vipeye-internal/src/analyzer/backgroundAnalysisProgram.ts index bd4060dd..4a038e43 100644 --- a/packages/vipeye-internal/src/analyzer/backgroundAnalysisProgram.ts +++ b/packages/vipeye-internal/src/analyzer/backgroundAnalysisProgram.ts @@ -17,7 +17,7 @@ import { ServiceProvider } from '../common/serviceProvider'; import '../common/serviceProviderExtensions'; import { Range } from '../common/textRange'; import { Uri } from '../common/uri/uri'; -import { AnalysisCompleteCallback, analyzeProgram } from './analysis'; +import { AnalysisCompleteCallback, AnalysisProgressCallback, analyzeProgram } from './analysis'; import { ImportResolver } from './importResolver'; import { MaxAnalysisTime, OpenFileOptions, Program } from './program'; import { TypeStubWriter } from './typeStubWriter'; @@ -33,6 +33,7 @@ export class BackgroundAnalysisProgram { private _program: Program; private _disposed = false; private _onAnalysisCompletion: AnalysisCompleteCallback | undefined; + private _onAnalysisProgress: AnalysisProgressCallback | undefined; private _preEditAnalysis: IBackgroundAnalysis | undefined; constructor( @@ -157,6 +158,11 @@ export class BackgroundAnalysisProgram { this._backgroundAnalysis?.setCompletionCallback(callback); } + setAnalysisProgressCallback(callback?: AnalysisProgressCallback) { + this._onAnalysisProgress = callback; + this._backgroundAnalysis?.setAnalysisProgressCallback(callback); + } + startAnalysis(token: CancellationToken): boolean { if (this._backgroundAnalysis) { this._backgroundAnalysis.startAnalysis(token); @@ -169,7 +175,8 @@ export class BackgroundAnalysisProgram { this._configOptions, this._onAnalysisCompletion, this._serviceProvider.console(), - token + token, + this._onAnalysisProgress ); } diff --git a/packages/vipeye-internal/src/analyzer/program.ts b/packages/vipeye-internal/src/analyzer/program.ts index 982ea285..d230d2db 100644 --- a/packages/vipeye-internal/src/analyzer/program.ts +++ b/packages/vipeye-internal/src/analyzer/program.ts @@ -160,6 +160,10 @@ export class Program { private _editModeTracker = new EditModeTracker(); private _sourceFileFactory: ISourceFileFactory; + // Viper 扩展:LSP 状态栏进度所需的「当前正在分析的文件路径」。 + // 在 analyze() 的循环里逐文件更新,见 getAnalysisProgress()。 + private _currentAnalysisFilePath: string | undefined; + // Viper 扩展:includes/builtins.vp(真实源码,非 .vpi 存根)的 typedef 符号 // 合并状态。key =(typeshed builtins 存根 + 执行环境 + 被分析文件目录), // 避免对同一环境/目录下的文件重复扫描/合并;不同目录或不同执行环境会各自尝试。 @@ -714,7 +718,11 @@ export class Program { // whether the method needs to be called again to complete the // analysis. In interactive mode, the timeout is always limited // to the smaller value to maintain responsiveness. - analyze(maxTime?: MaxAnalysisTime, token: CancellationToken = CancellationToken.None): boolean { + analyze( + maxTime?: MaxAnalysisTime, + token: CancellationToken = CancellationToken.None, + onProgress?: () => void + ): boolean { return this._runEvaluatorWithCancellationToken(token, () => { const elapsedTime = new Duration(); @@ -727,6 +735,8 @@ export class Program { // Check the open files. for (const sourceFileInfo of openFiles) { + this._currentAnalysisFilePath = sourceFileInfo.uri.getFilePath(); + onProgress?.(); if (this._checkTypes(sourceFileInfo)) { if (elapsedTime.getDurationInMilliseconds() > effectiveMaxTime) { return true; @@ -751,6 +761,8 @@ export class Program { continue; } + this._currentAnalysisFilePath = sourceFileInfo.uri.getFilePath(); + onProgress?.(); if (this._checkTypes(sourceFileInfo)) { if (elapsedTime.getDurationInMilliseconds() > effectiveMaxTime) { return true; @@ -763,6 +775,22 @@ export class Program { }); } + /** + * Viper 扩展:返回分析进度的快照,供 LSP 客户端在状态栏显示 + * 「正在分析哪个文件 / 已分析多少 / 共多少个用户文件」。 + * 计数与 AnalysisResults.requiringAnalysisCount 同源(都走 getFilesToAnalyzeCount), + * 因此百分比不会与实际分析进度脱节。 + */ + getAnalysisProgress(): { currentFile: string | undefined; analyzed: number; total: number } { + const total = this.getUserFileCount(); + const pending = this.getFilesToAnalyzeCount().files; + return { + currentFile: this._currentAnalysisFilePath, + analyzed: Math.min(total, Math.max(0, total - pending)), + total, + }; + } + // Performs parsing and analysis of a single file in the program. If the file is not part of // the program returns false to indicate analysis was not performed. analyzeFile(fileUri: Uri, token: CancellationToken = CancellationToken.None): boolean { diff --git a/packages/vipeye-internal/src/analyzer/service.ts b/packages/vipeye-internal/src/analyzer/service.ts index 3b5d60d4..e911caa6 100644 --- a/packages/vipeye-internal/src/analyzer/service.ts +++ b/packages/vipeye-internal/src/analyzer/service.ts @@ -52,7 +52,7 @@ import { isFile, tryStat, } from '../common/uri/uriUtils'; -import { AnalysisCompleteCallback } from './analysis'; +import { AnalysisCompleteCallback, AnalysisProgressCallback } from './analysis'; import { BackgroundAnalysisProgram, BackgroundAnalysisProgramFactory, @@ -340,6 +340,14 @@ export class AnalyzerService { this._backgroundAnalysisProgram.setCompletionCallback(callback); } + /** + * Viper 扩展:注册分析进度回调(逐文件上报「正在分析哪个文件」), + * 由 languageServerBase 转成客户端状态栏进度通知。 + */ + setAnalysisProgressCallback(callback: AnalysisProgressCallback | undefined): void { + this._backgroundAnalysisProgram.setAnalysisProgressCallback(callback); + } + setOptions(commandLineOptions: CommandLineOptions): void { this._commandLineOptions = commandLineOptions; diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index 907aea53..a46c7173 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -5379,6 +5379,23 @@ export function createTypeEvaluator( // 它们是 builtins stub 中的别名,正常 Viper 工程均可解析; // 解析不到时静默退化为 Unknown,不报「未定义」(与 [SIZE]T 同一取舍)。 type = UnknownType.create(); + } else if (node.d.isViperImplicitModule) { + // Viper 扩展:`__attribute__` 脱糖出的「隐式模块根」名 `t` / `c` + // (见 parser._createViperImplicitModuleName)。源码里并不存在这两个 + // 名字,也不要求源码 `import t` / `import c` —— 编译器对属性的识别是 + // AST 级的,同样不看符号可见性。故在词法作用域里取不到时回退到 + // builtins 作用域的同名绑定;仍取不到才报未定义。 + const implicitModuleType = getViperImplicitModuleType(node, name); + if (implicitModuleType) { + type = implicitModuleType; + } else { + addDiagnostic( + DiagnosticRule.reportUndefinedVariable, + LocMessage.symbolIsUndefined().format({ name }), + node + ); + type = UnknownType.create(); + } } else { // Viper 扩展:前向引用支持(收紧版)。 // Viper 是 AOT 语言,函数/类的定义顺序无关(类似 C 语言的前向声明), @@ -9442,12 +9459,41 @@ export function createTypeEvaluator( return undefined; } + // Viper 扩展:解析 `__attribute__` 脱糖出的「隐式模块根」名(`t` / `c`)。 + // 这类名字源码里并不存在(见 parser._createViperImplicitModuleName),也不要求 + // 源码 `import t` / `import c` —— 编译器对属性的识别是 AST 级的,同样不看符号 + // 可见性。故在词法作用域里取不到时沿 scope 链(含 builtins 作用域)查找同名绑定: + // builtins.vpi 通过 `from t import ...` / `from c import ...` 绑定了 `t` / `c`, + // 但 import-only 绑定对下游不可见(isExternallyHidden),故这里直接查符号表, + // 而不是走 lookUpSymbolRecursive。 + // 用户源码里手写的裸 `t` / `c` 不带该标记,仍照旧报未定义。 + function getViperImplicitModuleType(node: ParseNode, name: string): Type | undefined { + let scope = ScopeUtils.getScopeForNode(node); + while (scope) { + const symbol = scope.lookUpSymbol(name); + if (symbol) { + const effectiveType = getEffectiveTypeOfSymbolForUsage( + symbol, + /* usageNode */ undefined, + /* useLastDecl */ true + ).type; + if (effectiveType) { + return effectiveType; + } + } + scope = scope.parent; + } + return undefined; + } + // Viper 扩展:解析 `t.attr.` / `t.attr.(...)` 属性标记 → t.CAttr 实例。 // // 形态(与 TransPyC HandlesAssign._TAttrName / TransPyV ClassGenBase._tattr_name // 同判据:只看最外层属性名): // t.attr.packed (Attribute:t.attr.llvm.nounwind → 最外层 nounwind) // t.attr.section(".x") (Call:取 func 的最外层属性名) + // 根 `t` 可以是手写的(需 import t)或 `__attribute__` 脱糖出的隐式模块根 + // (isViperImplicitModule,不要求 import)。 // 这类表达式出现在类型注解联合中(`T | t.attr.section(".x")`、 // `T | t.attr.llvm.nounwind`),或由 parser 从 `__attribute__(...)` 修饰器脱糖而来; // 注解位置求值为 CAttr 标记——与 CConst / CVolatile 同族的修饰符 @@ -9470,14 +9516,17 @@ export function createTypeEvaluator( } const moduleSymbolWithScope = lookUpSymbolRecursive(cur, cur.d.value, /* honorCodeFlow */ true); - if (!moduleSymbolWithScope) { - return undefined; + let moduleType: Type | undefined; + if (moduleSymbolWithScope) { + moduleType = getEffectiveTypeOfSymbolForUsage( + moduleSymbolWithScope.symbol, + /* usageNode */ undefined, + /* useLastDecl */ true + ).type; + } else if (cur.d.isViperImplicitModule) { + // `__attribute__` 脱糖出的隐式 `t`:源码未 import t 时回退到 builtins 绑定。 + moduleType = getViperImplicitModuleType(cur, cur.d.value); } - const moduleType = getEffectiveTypeOfSymbolForUsage( - moduleSymbolWithScope.symbol, - /* usageNode */ undefined, - /* useLastDecl */ true - ).type; if (!moduleType || !isModule(moduleType)) { return undefined; } diff --git a/packages/vipeye-internal/src/backgroundAnalysisBase.ts b/packages/vipeye-internal/src/backgroundAnalysisBase.ts index a1207e00..08fb0f94 100644 --- a/packages/vipeye-internal/src/backgroundAnalysisBase.ts +++ b/packages/vipeye-internal/src/backgroundAnalysisBase.ts @@ -11,6 +11,8 @@ import { MessageChannel, MessagePort, Worker, parentPort, threadId, workerData } import { AnalysisCompleteCallback, + AnalysisProgress, + AnalysisProgressCallback, AnalysisResults, RequiringAnalysisCount, analyzeProgram, @@ -51,6 +53,7 @@ import { ProgramView } from './common/extensibility'; export interface IBackgroundAnalysis extends Disposable { setProgramView(program: Program): void; setCompletionCallback(callback?: AnalysisCompleteCallback): void; + setAnalysisProgressCallback(callback?: AnalysisProgressCallback): void; setImportResolver(importResolver: ImportResolver): void; setConfigOptions(configOptions: ConfigOptions): void; setTrackedFiles(fileUris: Uri[]): void; @@ -84,6 +87,9 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis { private _worker: Worker | undefined; private _onAnalysisCompletion: AnalysisCompleteCallback = nullCallback; + private _onAnalysisProgress: AnalysisProgressCallback = () => { + /* empty */ + }; private _messageChannel: MessageChannel; protected program: ProgramView | undefined; @@ -115,6 +121,14 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis { this._onAnalysisCompletion = callback ?? nullCallback; } + setAnalysisProgressCallback(callback?: AnalysisProgressCallback) { + this._onAnalysisProgress = + callback ?? + (() => { + /* empty */ + }); + } + setImportResolver(importResolver: ImportResolver) { this.enqueueRequest({ requestType: 'setImportResolver', data: serialize(importResolver.host.kind) }); } @@ -322,6 +336,11 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis { break; } + case 'analysisProgress': { + this._onAnalysisProgress(deserialize(msg.data)); + break; + } + default: debug.fail(`${msg.requestType} is not expected. Message structure: ${JSON.stringify(msg)}`); } @@ -344,6 +363,11 @@ export class BackgroundAnalysisBase implements IBackgroundAnalysis { break; } + case 'analysisProgress': { + this._onAnalysisProgress(deserialize(msg.data)); + break; + } + case 'analysisPaused': { // Analysis request has completed, but there is more to // analyze, so queue another message to resume later. @@ -607,13 +631,25 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase protected handleResumeAnalysis(port: MessagePort, token: CancellationToken) { // Report results at the interval of the max analysis time. const maxTime = { openFilesTimeInMs: 50, noOpenFilesTimeInMs: 200 }; + + // Viper 扩展:把逐文件的分析进度回传前台(节流到 50ms 一次,避免 + // 大量文件时 IPC 消息过密)。收尾那次(done)一定要发。 + let lastProgressSent = 0; const moreToAnalyze = analyzeProgram( this.program, maxTime, this._configOptions, (result) => this.onAnalysisCompletion(port, result), this.getConsole(), - token + token, + (progress) => { + const now = Date.now(); + if (!progress.done && now - lastProgressSent < 50) { + return; + } + lastProgressSent = now; + this.onAnalysisProgress(port, progress); + } ); if (moreToAnalyze) { @@ -770,6 +806,14 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase port.postMessage({ requestType: 'analysisResult', data: serialize(result) }); } + /** + * Viper 扩展:把「正在分析哪个文件 / 已分析多少」回传前台线程, + * 由 languageServerBase 转成 `viper/analysisStatus` 通知给客户端状态栏。 + */ + protected onAnalysisProgress(port: MessagePort, progress: AnalysisProgress) { + port.postMessage({ requestType: 'analysisProgress', data: serialize(progress) }); + } + private _onMessageWrapper(msg: BackgroundRequest) { try { return this.onMessage(msg); @@ -881,7 +925,7 @@ export interface BackgroundRequest { sharedUsageBuffer?: SharedArrayBuffer; } -export type BackgroundResponseKind = 'log' | 'analysisResult' | 'analysisPaused' | 'analysisDone'; +export type BackgroundResponseKind = 'log' | 'analysisResult' | 'analysisProgress' | 'analysisPaused' | 'analysisDone'; export interface BackgroundResponse { requestType: BackgroundResponseKind; diff --git a/packages/vipeye-internal/src/languageServerBase.ts b/packages/vipeye-internal/src/languageServerBase.ts index 078c6ba9..75a37837 100644 --- a/packages/vipeye-internal/src/languageServerBase.ts +++ b/packages/vipeye-internal/src/languageServerBase.ts @@ -84,7 +84,7 @@ import { } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { AnalysisResults } from './analyzer/analysis'; +import { AnalysisProgress, AnalysisResults } from './analyzer/analysis'; import { BackgroundAnalysisProgram, InvalidatedReason } from './analyzer/backgroundAnalysisProgram'; import { ImportResolver } from './analyzer/importResolver'; import { MaxAnalysisTime } from './analyzer/program'; @@ -369,9 +369,19 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis }); service.setCompletionCallback((results) => this.onAnalysisCompletedHandler(service.fs, results)); + service.setAnalysisProgressCallback((progress) => this.sendAnalysisStatus(progress)); return service; } + /** + * Viper 扩展:把「正在分析哪个文件 / 已分析多少 / 共多少」通知给客户端, + * 供其在左下角状态栏显示 `C:\xxx\1.vp (4/90) 0.04% loading...`, + * 让用户能判断语言服务是在工作还是卡死。 + */ + protected sendAnalysisStatus(progress: AnalysisProgress) { + this.connection.sendNotification('viper/analysisStatus', progress); + } + async getWorkspaces(): Promise { const workspaces = this.workspaceFactory.items(); for (const workspace of workspaces) { diff --git a/packages/vipeye-internal/src/languageService/viperAttributeUtils.ts b/packages/vipeye-internal/src/languageService/viperAttributeUtils.ts index 571f3e1a..4fd7937f 100644 --- a/packages/vipeye-internal/src/languageService/viperAttributeUtils.ts +++ b/packages/vipeye-internal/src/languageService/viperAttributeUtils.ts @@ -12,7 +12,6 @@ * 没有可命中的节点。 */ -import { ScopeType } from '../analyzer/scope'; import { getScopeForNode } from '../analyzer/scopeUtils'; import { Symbol, SymbolTable } from '../analyzer/symbol'; import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; @@ -77,12 +76,15 @@ export function getViperAttributeSymbols( evaluator: TypeEvaluator, scopeNode: ParseNode ): { symbolTable: SymbolTable; attrClass: ClassType } | undefined { - const scope = getScopeForNode(scopeNode); - if (!scope || scope.type !== ScopeType.Module) { - return undefined; + // 取 `t` 模块:优先本文件的 `import t`,否则沿 scope 链回退到 builtins 作用域 + // (builtins.vpi 通过 `from t import ...` 绑定了 `t`)—— 与 `__attribute__` + // 脱糖出的隐式 `t` 一致(源码不 import t 时属性名补全/悬停仍应可用)。 + let scope = getScopeForNode(scopeNode); + let tSymbol: Symbol | undefined; + while (scope && !tSymbol) { + tSymbol = scope.lookUpSymbol('t'); + scope = scope.parent; } - - const tSymbol = scope.lookUpSymbol('t'); if (!tSymbol) { return undefined; } diff --git a/packages/vipeye-internal/src/parser/parseNodes.ts b/packages/vipeye-internal/src/parser/parseNodes.ts index e91d37c9..776c4d3e 100644 --- a/packages/vipeye-internal/src/parser/parseNodes.ts +++ b/packages/vipeye-internal/src/parser/parseNodes.ts @@ -1704,6 +1704,14 @@ export interface NameNode extends ParseNodeBase { // 但脱糖目标是 builtins stub 中的别名,不要求源码 import t; // 名字解析不到时静默退化为 Unknown。 isViperTypeDescriptor?: boolean; + + // Viper 扩展:`__attribute__` 脱糖(见 parser._desugarViperMagicAttribute / + // _makeViperAttrMarker)时合成出来的 `t` / `c` **模块根**名:零长度、 + // 不对应源码字符。编译器的属性识别是 AST 级的(不要求源码 import t / import c), + // 故这里也允许隐式解析:该名在词法作用域链上取不到时回退到 builtins 作用域 + // 的同名绑定(builtins.vpi 通过 `from t import ...` / `from c import ...` + // 绑定了它们),而不是报"未定义 t"。 + isViperImplicitModule?: boolean; }; } diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index c8818761..d594bd65 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -5435,9 +5435,11 @@ export class Parser { // - 已是 `t.attr.X` / `c.Attribute(...)` 形态 → 原样保留(允许混写) // 与 TPC/TPV 的 includes/ast/parser.vp `_MakeTAttrMarker` 逐条对应; // 成员名复用实参自身的 token(真实位置),故属性名上的跳转/悬停仍指向 t.vpi。 + // 根 `t` 是合成的「隐式模块根」(见 _createViperImplicitModuleName), + // 不要求源码 `import t`。 private _makeViperAttrMarker(argExpr: ExpressionNode): ExpressionNode { const attrRoot = MemberAccessNode.create( - NameNode.create(IdentifierToken.create(argExpr.start, 0, 't', /* comments */ undefined)), + this._createViperImplicitModuleName(argExpr.start, 't'), NameNode.create(IdentifierToken.create(argExpr.start, 0, 'attr', /* comments */ undefined)) ); @@ -5454,6 +5456,17 @@ export class Parser { return argExpr; } + // Viper 扩展:合成 zero-length 的「隐式模块根」名(`t` / `c`)。 + // `__attribute__` 的脱糖节点里引用 `t` / `c`,但**不要求源码 import t / import c** + // —— 与编译器一致(TPC/TPV 的属性识别是 AST 级的,不看符号是否可见)。 + // 该名在词法作用域里解析不到时,typeEvaluator 会回退到 builtins 作用域的同名 + // 模块绑定(见 NameNode.d.isViperImplicitModule)。 + private _createViperImplicitModuleName(start: number, name: string): NameNode { + const nameNode = NameNode.create(IdentifierToken.create(start, 0, name, /* comments */ undefined)); + nameNode.d.isViperImplicitModule = true; + return nameNode; + } + // Viper 扩展:把 `@__attribute__(...)` 装饰器脱糖为 // `@c.Attribute(t.attr.a, t.attr.b)`——与 TPC/TPV 的 // includes/ast/parser.vp `_DesugarMagicAttribute` 逐字段同形,因此 class 与 def @@ -5468,7 +5481,7 @@ export class Parser { } const attributeCallee = MemberAccessNode.create( - NameNode.create(IdentifierToken.create(expr.start, 0, 'c', /* comments */ undefined)), + this._createViperImplicitModuleName(expr.start, 'c'), NameNode.create(IdentifierToken.create(expr.start, 0, 'Attribute', /* comments */ undefined)) ); diff --git a/packages/vipeye-internal/src/tests/viperAttribute.test.ts b/packages/vipeye-internal/src/tests/viperAttribute.test.ts index 28d6c4fa..2f1dd2f5 100644 --- a/packages/vipeye-internal/src/tests/viperAttribute.test.ts +++ b/packages/vipeye-internal/src/tests/viperAttribute.test.ts @@ -325,6 +325,64 @@ def f() -> int: } }); +test('__attribute__ 不要求 import t / import c(隐式模块根)', () => { + const code = `@__attribute__(section(".text.wo"), always_inline) +def decorated(a: int) -> int: + return a + +@__attribute__(packed, aligned(16)) +class Packed: + x: int + +AttrConst: __attribute__(section(".PRVERSN")) const int = 1 +`; + const { program, configOptions, uri, serviceProvider, testDir } = buildProgramForCode(code, '__viper_attribute_noimport__'); + + try { + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== __attribute__ 免 import 诊断 ==='); + diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`)); + + const realDiagnostics = diagnostics.filter((d) => d.category === 0 || d.category === 1); + assert.strictEqual( + realDiagnostics.length, + 0, + `__attribute__ 脱糖出的 t / c 不应报未定义:${JSON.stringify(realDiagnostics)}` + ); + } finally { + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testDir, { recursive: true, force: true }); + } +}); + +test('隐式模块根只作用于 __attribute__ 脱糖:手写裸 t / c 仍报未定义', () => { + const code = `Bad: t.CInt = 1 + +def use_c() -> int: + return c.SomeThing +`; + const { program, configOptions, uri, serviceProvider, testDir } = buildProgramForCode( + code, + '__viper_attribute_implicit_scope__' + ); + + try { + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== 手写裸 t / c 诊断 ==='); + diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`)); + + const undefinedT = diagnostics.filter((d) => d.message.includes('未定义') && d.message.includes('t')); + const undefinedC = diagnostics.filter((d) => d.message.includes('未定义') && d.message.includes('c')); + assert(undefinedT.length > 0, `手写裸 t 应仍报未定义:${JSON.stringify(diagnostics)}`); + assert(undefinedC.length > 0, `手写裸 c 应仍报未定义:${JSON.stringify(diagnostics)}`); + } finally { + program.dispose(); + serviceProvider.dispose(); + require('fs').rmSync(testDir, { recursive: true, force: true }); + } +}); + test('hover 在 __attribute__(...) 括号内提示全部属性名', () => { // 第 2 行第 22 列落在实参 `section` 上。 const code = `import t diff --git a/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi b/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi index 0a7a887c..108dfc37 100644 --- a/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi +++ b/packages/vipeye-internal/typeshed-fallback/stdlib/builtins.vpi @@ -2,6 +2,12 @@ import _ast import _sitebuiltins import _typeshed import sys +# Viper 扩展:`__attribute__` 脱糖出的隐式模块根 `c` / `t` +# (见 parseNodes.NameNode.d.isViperImplicitModule)。这两个 import 是 +# import-only 绑定(isExternallyHidden),普通名字解析查不到,仅供 +# typeEvaluator.getViperImplicitModuleType 直接查 symbolTable 时回退使用, +# 因此源码里手写裸 `t` / `c` 仍然照旧报未定义。 +import c import t import types from _collections_abc import dict_items, dict_keys, dict_values diff --git a/packages/vscode-vipeye/CHANGELOG.md b/packages/vscode-vipeye/CHANGELOG.md index 8d0000ca..dba5436c 100644 --- a/packages/vscode-vipeye/CHANGELOG.md +++ b/packages/vscode-vipeye/CHANGELOG.md @@ -27,6 +27,8 @@ - `__attribute__` is highlighted as a type modifier (same scope as `const` / `volatile`): the TextMate grammar matches it when followed by `(`, in both the decorator and the modifier spelling - `__attribute__` is also offered by completion (typing `__a` lists it) and **hovering inside `__attribute__(...)`** shows the full list of available attribute names (`section` / `aligned` / `always_inline` / `weak` / …), taken from the `attr` class of the `t` stub. Both share one helper (`viperAttributeUtils`) for locating the parentheses and enumerating `t.attr`, so the completion list and the hover list cannot drift apart - Lowercase **`callable[[A, B], R]`** is now supported in a type annotation and means exactly `t.Callable[[A, B], R]` — all three spellings (`callable[...]`, `Callable[...]`, `t.Callable[...]`) and all the usual argument forms (`callable[[int, str], bool]`, `callable[..., bool]`, `callable[[], bool]`, as a parameter, return or variable annotation) evaluate to the same callable type. The parser rewrites the bare name `callable` to `Callable` **only in type-annotation position**, so the built-in function is untouched: `callable(x)` / `if callable(x):` still behave as before. `Callable` is declared in the built-in `builtins.vpi` stub (`Callable: t.CTypedef = t.Callable`, next to `rawptr` / `CConst`), so no `import t` is required. Previously `callable[[int], bool]` silently degraded to the type of the `callable` *function* (the Viper-dialect rule that ignores explicit type arguments on a callable), and bare `Callable` was reported as undefined +- `__attribute__` no longer requires the file to `import t` / `import c`: the `t` / `c` module roots that `__attribute__(...)` desugars into are synthesized as **implicit module names** (zero-length, marked on the AST node — the compiler's attribute handling is AST-level and never checks symbol visibility either). When such a synthetic name cannot be resolved through the lexical scope chain, it falls back to the same-named binding in the built-in `builtins.vpi` stub; the fallback only applies to the desugared nodes, so a hand-written bare `t` / `c` in a file without the import still reports "未定义" +- **Analysis progress in the status bar**: while the language server builds the dependency graph and type-checks the workspace, the bottom-left status bar now shows `$(sync~spin) C:\xxx\1.vp (4/90) 4.44% loading...` — the file currently being analyzed, how many files have been processed out of how many, and the percentage. The worker reports per-file progress (throttled to one message every 50 ms) through a new `analysisProgress` background message and a new `viper/analysisStatus` notification; the client shows a left-aligned `StatusBarItem` and hides it as soon as analysis finishes (or when there is nothing to analyze). Previously the only feedback was the work-done progress bar for open-files analysis, which said nothing about which file was being processed, so a long full-workspace pass was indistinguishable from a hung server ## 1.1.425 diff --git a/packages/vscode-vipeye/src/extension.ts b/packages/vscode-vipeye/src/extension.ts index af92f6ec..44d0b192 100644 --- a/packages/vscode-vipeye/src/extension.ts +++ b/packages/vscode-vipeye/src/extension.ts @@ -19,6 +19,8 @@ import { Position, ProgressLocation, Range, + StatusBarAlignment, + StatusBarItem, TextEditor, TextEditorEdit, Uri, @@ -82,6 +84,41 @@ function isDevelopmentCopyLoaded(): boolean { }); } +/** + * 服务端 `viper/analysisStatus` 通知的载荷,对应 vipeye-internal 的 + * `AnalysisProgress`(依赖图构建 / import 分析阶段的逐文件进度)。 + */ +interface AnalysisStatusNotification { + /** 当前正在分析的文件路径(Windows 下形如 C:\xxx\1.vp)。 */ + currentFile: string | undefined; + + /** 已分析的用户文件数。 */ + analyzed: number; + + /** 需要分析的用户文件总数。 */ + total: number; + + /** 分析是否已结束(true 时收起状态栏进度项)。 */ + done: boolean; +} + +/** + * 左下角状态栏的分析进度项:显示 `C:\xxx\1.vp (4/90) 4.44% loading...`。 + * 依赖图构建 / import 分析期间没有其他可见反馈,用户无法区分「插件在忙」 + * 与「插件卡死」,故这里给出实时进度;分析结束后自动隐藏。 + */ +function UpdateAnalysisStatusItem(item: StatusBarItem, progress: AnalysisStatusNotification): void { + if (progress.done || progress.total <= 0) { + item.hide(); + return; + } + + const percent = Math.min(100, (progress.analyzed / progress.total) * 100).toFixed(2); + item.text = `$(sync~spin) ${progress.currentFile ?? '...'} (${progress.analyzed}/${progress.total}) ${percent}% loading...`; + item.tooltip = `Viper:正在分析 ${progress.total} 个文件(依赖图构建 / import 分析)`; + item.show(); +} + export async function activate(context: ExtensionContext) { // 调试宿主让路:launch.json 用 --extensionDevelopmentPath 加载开发版时,VS Code 不会 // 自动停用已安装的发布版,两者在同一扩展宿主内同时激活并注册同名 'viper.*' 命令, @@ -185,6 +222,16 @@ export async function activate(context: ExtensionContext) { const client = new LanguageClient('vipeye', 'Vipeye', serverOptions, clientOptions); languageClient = client; + // 左下角状态栏的分析进度项:显示正在分析哪个文件(`... (4/90) 4.44% loading...`), + // 让用户知道依赖图构建 / import 分析是否在进行,而不是卡死了。 + const analysisStatusItem = window.createStatusBarItem(StatusBarAlignment.Left, 100); + context.subscriptions.push(analysisStatusItem); + context.subscriptions.push( + client.onNotification('viper/analysisStatus', (progress: AnalysisStatusNotification) => { + UpdateAnalysisStatusItem(analysisStatusItem, progress); + }) + ); + // Register our custom commands. const textEditorCommands = [Commands.orderImports]; textEditorCommands.forEach((commandName) => {