feat(vipeye-internal): __attribute__ 免 import t/c 与状态栏分析进度

- __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 通过。
This commit is contained in:
TermiNexus committed 2026-09-27 13:03:03 +08:00
1 parent baec16e540
commit 4d5a0d203b
15 files changed
+339 -24

No files matched your search

+2
View File
@@ -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__/
@@ -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();
@@ -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
);
}
@@ -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 {
@@ -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;
@@ -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.<name>` / `t.attr.<name>(...)` 属性标记 → 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;
}
@@ -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<AnalysisProgress>(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<AnalysisProgress>(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;
@@ -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<Workspace[]> {
const workspaces = this.workspaceFactory.items();
for (const workspace of workspaces) {
@@ -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;
}
@@ -1704,6 +1704,14 @@ export interface NameNode extends ParseNodeBase<ParseNodeType.Name> {
// 但脱糖目标是 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;
};
}
+15 -2
View File
@@ -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))
);
@@ -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
@@ -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
+2
View File
@@ -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
+47
View File
@@ -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) => {