[typescript-language-features] Region-based semantic diagnostics for TypeScript (#208713)

* WIP

* invalidate diagnostics in range

* check whether should use region based diagnostics

* add ts-expect-errors

* make region opt off by default

* bump to expected 5.6

* update comments to refer to 5.6

* make region diagnostics on by default for insiders
This commit is contained in:
Gabriela Araujo Britto
2024-06-19 15:12:57 -07:00
committed by GitHub
parent 7717059b2e
commit 878af0771b
10 changed files with 108 additions and 24 deletions

View File

@@ -124,6 +124,7 @@ export interface TypeScriptServiceConfiguration {
readonly localNodePath: string | null;
readonly globalNodePath: string | null;
readonly workspaceSymbolsExcludeLibrarySymbols: boolean;
readonly enableRegionDiagnostics: boolean;
}
export function areServiceConfigurationsEqual(a: TypeScriptServiceConfiguration, b: TypeScriptServiceConfiguration): boolean {
@@ -162,6 +163,7 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
localNodePath: this.readLocalNodePath(configuration),
globalNodePath: this.readGlobalNodePath(configuration),
workspaceSymbolsExcludeLibrarySymbols: this.readWorkspaceSymbolsExcludeLibrarySymbols(configuration),
enableRegionDiagnostics: this.readEnableRegionDiagnostics(configuration),
};
}
@@ -267,4 +269,8 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu
private readWebTypeAcquisition(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.tsserver.web.typeAcquisition.enabled', false);
}
private readEnableRegionDiagnostics(configuration: vscode.WorkspaceConfiguration): boolean {
return configuration.get<boolean>('typescript.tsserver.enableRegionDiagnostics', true);
}
}

View File

@@ -34,6 +34,7 @@ export const enum DiagnosticKind {
Syntax,
Semantic,
Suggestion,
RegionSemantic,
}
class FileDiagnostics {
@@ -48,7 +49,8 @@ class FileDiagnostics {
public updateDiagnostics(
language: DiagnosticLanguage,
kind: DiagnosticKind,
diagnostics: ReadonlyArray<vscode.Diagnostic>
diagnostics: ReadonlyArray<vscode.Diagnostic>,
ranges: ReadonlyArray<vscode.Range> | undefined
): boolean {
if (language !== this.language) {
this._diagnostics.clear();
@@ -61,6 +63,9 @@ class FileDiagnostics {
return false;
}
if (kind === DiagnosticKind.RegionSemantic) {
return this.updateRegionDiagnostics(diagnostics, ranges!);
}
this._diagnostics.set(kind, diagnostics);
return true;
}
@@ -83,6 +88,23 @@ class FileDiagnostics {
}
}
/**
* @param ranges The ranges whose diagnostics were updated.
*/
private updateRegionDiagnostics(
diagnostics: ReadonlyArray<vscode.Diagnostic>,
ranges: ReadonlyArray<vscode.Range>): boolean {
if (!this._diagnostics.get(DiagnosticKind.Semantic)) {
this._diagnostics.set(DiagnosticKind.Semantic, diagnostics);
return true;
}
const oldDiagnostics = this._diagnostics.get(DiagnosticKind.Semantic)!;
const newDiagnostics = oldDiagnostics.filter(diag => !ranges.some(range => diag.range.intersection(range)));
newDiagnostics.push(...diagnostics);
this._diagnostics.set(DiagnosticKind.Semantic, newDiagnostics);
return true;
}
private getSuggestionDiagnostics(settings: DiagnosticSettings) {
const enableSuggestions = settings.getEnableSuggestions(this.language);
return this.get(DiagnosticKind.Suggestion).filter(x => {
@@ -284,15 +306,16 @@ export class DiagnosticsManager extends Disposable {
file: vscode.Uri,
language: DiagnosticLanguage,
kind: DiagnosticKind,
diagnostics: ReadonlyArray<vscode.Diagnostic>
diagnostics: ReadonlyArray<vscode.Diagnostic>,
ranges: ReadonlyArray<vscode.Range> | undefined,
): void {
let didUpdate = false;
const entry = this._diagnostics.get(file);
if (entry) {
didUpdate = entry.updateDiagnostics(language, kind, diagnostics);
didUpdate = entry.updateDiagnostics(language, kind, diagnostics, ranges);
} else if (diagnostics.length) {
const fileDiagnostics = new FileDiagnostics(file, language);
fileDiagnostics.updateDiagnostics(language, kind, diagnostics);
fileDiagnostics.updateDiagnostics(language, kind, diagnostics, ranges);
this._diagnostics.set(file, fileDiagnostics);
didUpdate = true;
}

View File

@@ -138,7 +138,11 @@ export default class LanguageProvider extends Disposable {
this.client.bufferSyncSupport.requestAllDiagnostics();
}
public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: vscode.Uri, diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[]): void {
public diagnosticsReceived(
diagnosticsKind: DiagnosticKind,
file: vscode.Uri,
diagnostics: (vscode.Diagnostic & { reportUnnecessary: any; reportDeprecated: any })[],
ranges: vscode.Range[] | undefined): void {
if (diagnosticsKind !== DiagnosticKind.Syntax && !this.client.hasCapabilityForResource(file, ClientCapability.Semantic)) {
return;
}
@@ -175,7 +179,7 @@ export default class LanguageProvider extends Disposable {
}
}
return true;
}));
}), ranges);
}
public configFileDiagnosticsReceived(file: vscode.Uri, diagnostics: vscode.Diagnostic[]): void {

View File

@@ -275,12 +275,12 @@ class SyncedBufferMap extends ResourceMap<SyncedBuffer> {
}
class PendingDiagnostics extends ResourceMap<number> {
public getOrderedFileSet(): ResourceMap<void> {
public getOrderedFileSet(): ResourceMap<void | vscode.Range[]> {
const orderedResources = Array.from(this.entries())
.sort((a, b) => a.value - b.value)
.map(entry => entry.resource);
const map = new ResourceMap<void>(this._normalizePath, this.config);
const map = new ResourceMap<void | vscode.Range[]>(this._normalizePath, this.config);
for (const resource of orderedResources) {
map.set(resource, undefined);
}
@@ -292,7 +292,7 @@ class GetErrRequest {
public static executeGetErrRequest(
client: ITypeScriptServiceClient,
files: ResourceMap<void>,
files: ResourceMap<void | vscode.Range[]>,
onDone: () => void
) {
return new GetErrRequest(client, files, onDone);
@@ -303,7 +303,7 @@ class GetErrRequest {
private constructor(
private readonly client: ITypeScriptServiceClient,
public readonly files: ResourceMap<void>,
public readonly files: ResourceMap<void | vscode.Range[]>,
onDone: () => void
) {
if (!this.isErrorReportingEnabled()) {
@@ -313,19 +313,39 @@ class GetErrRequest {
}
const supportsSyntaxGetErr = this.client.apiVersion.gte(API.v440);
const allFiles = coalesce(Array.from(files.entries())
.filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic))
const fileEntries = Array.from(files.entries()).filter(entry => supportsSyntaxGetErr || client.hasCapabilityForResource(entry.resource, ClientCapability.Semantic));
const allFiles = coalesce(fileEntries
.map(entry => client.toTsFilePath(entry.resource)));
if (!allFiles.length) {
this._done = true;
setImmediate(onDone);
} else {
const request = this.areProjectDiagnosticsEnabled()
let request;
if (this.areProjectDiagnosticsEnabled()) {
// Note that geterrForProject is almost certainly not the api we want here as it ends up computing far
// too many diagnostics
? client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token)
: client.executeAsync('geterr', { delay: 0, files: allFiles }, this._token.token);
request = client.executeAsync('geterrForProject', { delay: 0, file: allFiles[0] }, this._token.token);
}
else {
let requestFiles;
if (this.areRegionDiagnosticsEnabled()) {
requestFiles = coalesce(fileEntries
.map(entry => {
const file = client.toTsFilePath(entry.resource);
const ranges = entry.value;
if (file && ranges) {
return typeConverters.Range.toFileRangesRequestArgs(file, ranges);
}
return file;
}));
}
else {
requestFiles = allFiles;
}
request = client.executeAsync('geterr', { delay: 0, files: requestFiles }, this._token.token);
}
request.finally(() => {
if (this._done) {
@@ -350,6 +370,10 @@ class GetErrRequest {
return this.client.configuration.enableProjectDiagnostics && this.client.capabilities.has(ClientCapability.Semantic);
}
private areRegionDiagnosticsEnabled() {
return this.client.configuration.enableRegionDiagnostics && this.client.apiVersion.gte(API.v560);
}
public cancel(): any {
if (!this._done) {
this._token.cancel();
@@ -722,7 +746,9 @@ export default class BufferSyncSupport extends Disposable {
// Add all open TS buffers to the geterr request. They might be visible
for (const buffer of this.syncedBuffers.values()) {
orderedFileSet.set(buffer.resource, undefined);
const editors = vscode.window.visibleTextEditors.filter(editor => editor.document.uri.toString() === buffer.resource.toString());
const visibleRanges = editors.flatMap(editor => editor.visibleRanges);
orderedFileSet.set(buffer.resource, visibleRanges.length ? visibleRanges : undefined);
}
for (const { resource } of orderedFileSet.entries()) {

View File

@@ -78,6 +78,7 @@ export enum EventName {
syntaxDiag = 'syntaxDiag',
semanticDiag = 'semanticDiag',
suggestionDiag = 'suggestionDiag',
regionSemanticDiag = 'regionSemanticDiag',
configFileDiag = 'configFileDiag',
telemetry = 'telemetry',
projectLanguageServiceState = 'projectLanguageServiceState',

View File

@@ -26,14 +26,24 @@ export namespace Range {
Math.max(0, start.line - 1), Math.max(start.offset - 1, 0),
Math.max(0, end.line - 1), Math.max(0, end.offset - 1));
export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({
file,
// @ts-expect-error until ts 5.6
export const toFileRange = (range: vscode.Range): Proto.FileRange => ({
startLine: range.start.line + 1,
startOffset: range.start.character + 1,
endLine: range.end.line + 1,
endOffset: range.end.character + 1
});
export const toFileRangeRequestArgs = (file: string, range: vscode.Range): Proto.FileRangeRequestArgs => ({
file,
...toFileRange(range)
});
// @ts-expect-error until ts 5.6
export const toFileRangesRequestArgs = (file: string, ranges: vscode.Range[]): Proto.FileRangesRequestArgs => ({
file,
ranges: ranges.map(toFileRange)
});
export const toFormattingRequestArgs = (file: string, range: vscode.Range): Proto.FormatRequestArgs => ({
file,
line: range.start.line + 1,

View File

@@ -90,8 +90,8 @@ export default class TypeScriptServiceClientHost extends Disposable {
services,
allModeIds));
this.client.onDiagnosticsReceived(({ kind, resource, diagnostics }) => {
this.diagnosticsReceived(kind, resource, diagnostics);
this.client.onDiagnosticsReceived(({ kind, resource, diagnostics, spans }) => {
this.diagnosticsReceived(kind, resource, diagnostics, spans);
}, null, this._disposables);
this.client.onConfigDiagnosticsReceived(diag => this.configFileDiagnosticsReceived(diag), null, this._disposables);
@@ -236,14 +236,16 @@ export default class TypeScriptServiceClientHost extends Disposable {
private async diagnosticsReceived(
kind: DiagnosticKind,
resource: vscode.Uri,
diagnostics: Proto.Diagnostic[]
diagnostics: Proto.Diagnostic[],
spans: Proto.TextSpan[] | undefined,
): Promise<void> {
const language = await this.findLanguage(resource);
if (language) {
language.diagnosticsReceived(
kind,
resource,
this.createMarkerDatas(diagnostics, language.diagnosticSource));
this.createMarkerDatas(diagnostics, language.diagnosticSource),
spans?.map(span => typeConverters.Range.fromTextSpan(span)));
}
}

View File

@@ -37,6 +37,7 @@ export interface TsDiagnostics {
readonly kind: DiagnosticKind;
readonly resource: vscode.Uri;
readonly diagnostics: Proto.Diagnostic[];
readonly spans?: Proto.TextSpan[];
}
interface ToCancelOnResourceChanged {
@@ -947,7 +948,8 @@ export default class TypeScriptServiceClient extends Disposable implements IType
switch (event.event) {
case EventName.syntaxDiag:
case EventName.semanticDiag:
case EventName.suggestionDiag: {
case EventName.suggestionDiag:
case EventName.regionSemanticDiag: {
// This event also roughly signals that projects have been loaded successfully (since the TS server is synchronous)
this.loadingIndicator.reset();
@@ -956,7 +958,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType
this._onDiagnosticsReceived.fire({
kind: getDiagnosticsKind(event),
resource: this.toResource(diagnosticEvent.body.file),
diagnostics: diagnosticEvent.body.diagnostics
diagnostics: diagnosticEvent.body.diagnostics,
// @ts-expect-error until ts 5.6
spans: diagnosticEvent.body.spans,
});
}
break;
@@ -1261,6 +1265,7 @@ function getDiagnosticsKind(event: Proto.Event) {
case 'syntaxDiag': return DiagnosticKind.Syntax;
case 'semanticDiag': return DiagnosticKind.Semantic;
case 'suggestionDiag': return DiagnosticKind.Suggestion;
case 'regionSemanticDiag': return DiagnosticKind.RegionSemantic;
}
throw new Error('Unknown dignostics kind');
}