Files
Viper_VSCode/packages/vipeye-internal/src/analyzer/backgroundAnalysisProgram.ts
T
TermiNexus 4d5a0d203b 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 通过。
2026-09-27 13:03:03 +08:00

320 lines
11 KiB
TypeScript

/*
* BackgroundAnalysisProgram.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*
* Applies operations to both the foreground program and a background
* analysis running in a worker process.
*/
import { CancellationToken } from 'vscode-languageserver';
import { IBackgroundAnalysis, RefreshOptions } from '../backgroundAnalysisBase';
import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions';
import { Diagnostic } from '../common/diagnostic';
import { FileDiagnostics } from '../common/diagnosticSink';
import { ServiceProvider } from '../common/serviceProvider';
import '../common/serviceProviderExtensions';
import { Range } from '../common/textRange';
import { Uri } from '../common/uri/uri';
import { AnalysisCompleteCallback, AnalysisProgressCallback, analyzeProgram } from './analysis';
import { ImportResolver } from './importResolver';
import { MaxAnalysisTime, OpenFileOptions, Program } from './program';
import { TypeStubWriter } from './typeStubWriter';
export enum InvalidatedReason {
Reanalyzed,
SourceWatcherChanged,
LibraryWatcherChanged,
LibraryWatcherContentOnlyChanged,
}
export class BackgroundAnalysisProgram {
private _program: Program;
private _disposed = false;
private _onAnalysisCompletion: AnalysisCompleteCallback | undefined;
private _onAnalysisProgress: AnalysisProgressCallback | undefined;
private _preEditAnalysis: IBackgroundAnalysis | undefined;
constructor(
protected readonly serviceId: string,
private readonly _serviceProvider: ServiceProvider,
private _configOptions: ConfigOptions,
private _importResolver: ImportResolver,
private _backgroundAnalysis?: IBackgroundAnalysis,
private readonly _maxAnalysisTime?: MaxAnalysisTime,
private readonly _disableChecker?: boolean,
program?: Program
) {
this._program =
program ??
new Program(
this.importResolver,
this.configOptions,
this._serviceProvider,
undefined,
this._disableChecker,
serviceId
);
this._backgroundAnalysis?.setProgramView(this._program);
}
get serviceProvider() {
return this._serviceProvider;
}
get configOptions() {
return this._configOptions;
}
get importResolver() {
return this._importResolver;
}
get program() {
return this._program;
}
get host() {
return this._importResolver.host;
}
get backgroundAnalysis() {
return this._backgroundAnalysis;
}
hasSourceFile(fileUri: Uri): boolean {
return !!this._program.getSourceFile(fileUri);
}
setConfigOptions(configOptions: ConfigOptions) {
this._configOptions = configOptions;
this._backgroundAnalysis?.setConfigOptions(configOptions);
this._program.setConfigOptions(configOptions);
}
setImportResolver(importResolver: ImportResolver) {
this._importResolver = importResolver;
this._backgroundAnalysis?.setImportResolver(importResolver);
this._program.setImportResolver(importResolver);
this.configOptions.getExecutionEnvironments().forEach((e) => this._ensurePartialStubPackages(e));
}
setTrackedFiles(fileUris: Uri[]) {
this._backgroundAnalysis?.setTrackedFiles(fileUris);
const diagnostics = this._program.setTrackedFiles(fileUris);
this._reportDiagnosticsForRemovedFiles(diagnostics);
}
setAllowedThirdPartyImports(importNames: string[]) {
this._backgroundAnalysis?.setAllowedThirdPartyImports(importNames);
this._program.setAllowedThirdPartyImports(importNames);
}
setFileOpened(fileUri: Uri, version: number | null, contents: string, options: OpenFileOptions) {
this._backgroundAnalysis?.setFileOpened(fileUri, version, contents, options);
this._program.setFileOpened(fileUri, version, contents, options);
}
getChainedUri(fileUri: Uri): Uri | undefined {
return this._program.getChainedUri(fileUri);
}
updateChainedUri(fileUri: Uri, chainedUri: Uri | undefined) {
this._backgroundAnalysis?.updateChainedUri(fileUri, chainedUri);
this._program.updateChainedUri(fileUri, chainedUri);
}
updateOpenFileContents(uri: Uri, version: number | null, contents: string, options: OpenFileOptions) {
this._backgroundAnalysis?.setFileOpened(uri, version, contents, options);
this._program.setFileOpened(uri, version, contents, options);
this.markFilesDirty([uri], /* evenIfContentsAreSame */ true);
}
setFileClosed(fileUri: Uri) {
this._backgroundAnalysis?.setFileClosed(fileUri);
const diagnostics = this._program.setFileClosed(fileUri);
this._reportDiagnosticsForRemovedFiles(diagnostics);
}
addInterimFile(fileUri: Uri) {
this._backgroundAnalysis?.addInterimFile(fileUri);
this._program.addInterimFile(fileUri);
}
markAllFilesDirty(evenIfContentsAreSame: boolean) {
this._backgroundAnalysis?.markAllFilesDirty(evenIfContentsAreSame);
this._program.markAllFilesDirty(evenIfContentsAreSame);
}
markFilesDirty(fileUris: Uri[], evenIfContentsAreSame: boolean) {
this._backgroundAnalysis?.markFilesDirty(fileUris, evenIfContentsAreSame);
this._program.markFilesDirty(fileUris, evenIfContentsAreSame);
}
setCompletionCallback(callback?: AnalysisCompleteCallback) {
this._onAnalysisCompletion = callback;
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);
return false;
}
return analyzeProgram(
this._program,
this._maxAnalysisTime,
this._configOptions,
this._onAnalysisCompletion,
this._serviceProvider.console(),
token,
this._onAnalysisProgress
);
}
async analyzeFile(fileUri: Uri, token: CancellationToken): Promise<boolean> {
if (this._backgroundAnalysis) {
return this._backgroundAnalysis.analyzeFile(fileUri, token);
}
return this._program.analyzeFile(fileUri, token);
}
async analyzeFileAndGetDiagnostics(fileUri: Uri, token: CancellationToken): Promise<Diagnostic[]> {
if (this._backgroundAnalysis) {
return this._backgroundAnalysis.analyzeFileAndGetDiagnostics(fileUri, token);
}
return this._program.analyzeFileAndGetDiagnostics(fileUri, token);
}
libraryUpdated(): boolean {
return false;
}
async getDiagnosticsForRange(fileUri: Uri, range: Range, token: CancellationToken): Promise<Diagnostic[]> {
if (this._backgroundAnalysis) {
return this._backgroundAnalysis.getDiagnosticsForRange(fileUri, range, token);
}
return this._program.getDiagnosticsForRange(fileUri, range);
}
async writeTypeStub(
targetImportUri: Uri,
targetIsSingleFile: boolean,
stubUri: Uri,
token: CancellationToken
): Promise<any> {
if (this._backgroundAnalysis) {
return this._backgroundAnalysis.writeTypeStub(targetImportUri, targetIsSingleFile, stubUri, token);
}
return new TypeStubWriter(this._program).writeTypeStub(
{
targetImportPath: targetImportUri,
targetIsSingleFile,
outputPath: stubUri,
},
token
);
}
invalidateAndForceReanalysis(reason: InvalidatedReason, refreshOptions?: RefreshOptions) {
this._backgroundAnalysis?.invalidateAndForceReanalysis(reason);
// Make sure the import resolver doesn't have invalid
// cached entries.
this._importResolver.invalidateCache();
// If we have specific changed file URIs and it's a content-only change, mark only those files dirty.
// Otherwise, mark all files with one or more errors dirty.
if (
refreshOptions?.changedFileUris &&
refreshOptions.changedFileUris.size > 0 &&
reason === InvalidatedReason.LibraryWatcherContentOnlyChanged
) {
// Convert UriMap keys to array for markFilesDirty
const changedFileUris = Array.from(refreshOptions.changedFileUris.keys());
this.markFilesDirty(changedFileUris, /* evenIfContentsAreSame */ true);
} else {
// Mark all files with one or more errors dirty.
this._program.markAllFilesDirty(/* evenIfContentsAreSame */ true);
}
}
restart() {
this._backgroundAnalysis?.restart();
}
dispose() {
if (this._disposed) {
return;
}
this._disposed = true;
this._program.dispose();
this._backgroundAnalysis?.shutdown();
this._backgroundAnalysis?.dispose();
}
enterEditMode() {
// Turn off analysis while in edit mode.
this._preEditAnalysis = this._backgroundAnalysis;
this._backgroundAnalysis = undefined;
// Forward this request to the program.
this._program.enterEditMode();
}
exitEditMode() {
this._backgroundAnalysis = this._preEditAnalysis;
this._preEditAnalysis = undefined;
return this._program.exitEditMode();
}
private _ensurePartialStubPackages(execEnv: ExecutionEnvironment) {
this._backgroundAnalysis?.ensurePartialStubPackages(execEnv.root?.toString());
return this._importResolver.ensurePartialStubPackages(execEnv);
}
private _reportDiagnosticsForRemovedFiles(fileDiags: FileDiagnostics[]) {
if (fileDiags.length === 0) {
return;
}
// If analysis is running in the foreground process, report any
// diagnostics that resulted from the close operation (used to
// clear diagnostics that are no longer of interest).
if (!this._backgroundAnalysis && this._onAnalysisCompletion) {
this._onAnalysisCompletion({
diagnostics: fileDiags,
filesInProgram: this._program.getFileCount(),
requiringAnalysisCount: this._program.getFilesToAnalyzeCount(),
checkingOnlyOpenFiles: this._program.isCheckingOnlyOpenFiles(),
fatalErrorOccurred: false,
configParseErrorOccurred: false,
elapsedTime: 0,
reason: 'tracking',
});
}
}
}
export type BackgroundAnalysisProgramFactory = (
serviceId: string,
serviceProvider: ServiceProvider,
configOptions: ConfigOptions,
importResolver: ImportResolver,
backgroundAnalysis?: IBackgroundAnalysis,
maxAnalysisTime?: MaxAnalysisTime
) => BackgroundAnalysisProgram;