Skip to content

Commit 9b2d44a

Browse files
Merge pull request #3561 from Microsoft/cancellableDiagnostics
Make it possible to cancel requests to get diagnostics.
2 parents e009d68 + 3a26cd2 commit 9b2d44a

File tree

10 files changed

+184
-104
lines changed

10 files changed

+184
-104
lines changed

src/compiler/checker.ts

+45-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ namespace ts {
2222
}
2323

2424
export function createTypeChecker(host: TypeCheckerHost, produceDiagnostics: boolean): TypeChecker {
25+
// Cancellation that controls whether or not we can cancel in the middle of type checking.
26+
// In general cancelling is *not* safe for the type checker. We might be in the middle of
27+
// computing something, and we will leave our internals in an inconsistent state. Callers
28+
// who set the cancellation token should catch if a cancellation exception occurs, and
29+
// should throw away and create a new TypeChecker.
30+
//
31+
// Currently we only support setting the cancellation token when getting diagnostics. This
32+
// is because diagnostics can be quite expensive, and we want to allow hosts to bail out if
33+
// they no longer need the information (for example, if the user started editing again).
34+
let cancellationToken: CancellationToken;
35+
2536
let Symbol = objectAllocator.getSymbolConstructor();
2637
let Type = objectAllocator.getTypeConstructor();
2738
let Signature = objectAllocator.getSignatureConstructor();
@@ -194,10 +205,10 @@ namespace ts {
194205

195206
return checker;
196207

197-
function getEmitResolver(sourceFile?: SourceFile) {
208+
function getEmitResolver(sourceFile: SourceFile, cancellationToken: CancellationToken) {
198209
// Ensure we have all the type information in place for this file so that all the
199210
// emitter questions of this resolver will return the right information.
200-
getDiagnostics(sourceFile);
211+
getDiagnostics(sourceFile, cancellationToken);
201212
return emitResolver;
202213
}
203214

@@ -13028,8 +13039,24 @@ namespace ts {
1302813039
}
1302913040

1303013041
function checkSourceElement(node: Node): void {
13031-
if (!node) return;
13032-
switch (node.kind) {
13042+
if (!node) {
13043+
return;
13044+
}
13045+
13046+
let kind = node.kind;
13047+
if (cancellationToken) {
13048+
// Only bother checking on a few construct kinds. We don't want to be excessivly
13049+
// hitting the cancellation token on every node we check.
13050+
switch (kind) {
13051+
case SyntaxKind.ModuleDeclaration:
13052+
case SyntaxKind.ClassDeclaration:
13053+
case SyntaxKind.InterfaceDeclaration:
13054+
case SyntaxKind.FunctionDeclaration:
13055+
cancellationToken.throwIfCancellationRequested();
13056+
}
13057+
}
13058+
13059+
switch (kind) {
1303313060
case SyntaxKind.TypeParameter:
1303413061
return checkTypeParameter(<TypeParameterDeclaration>node);
1303513062
case SyntaxKind.Parameter:
@@ -13305,7 +13332,20 @@ namespace ts {
1330513332
}
1330613333
}
1330713334

13308-
function getDiagnostics(sourceFile?: SourceFile): Diagnostic[] {
13335+
function getDiagnostics(sourceFile: SourceFile, ct: CancellationToken): Diagnostic[] {
13336+
try {
13337+
// Record the cancellation token so it can be checked later on during checkSourceElement.
13338+
// Do this in a finally block so we can ensure that it gets reset back to nothing after
13339+
// this call is done.
13340+
cancellationToken = ct;
13341+
return getDiagnosticsWorker(sourceFile);
13342+
}
13343+
finally {
13344+
cancellationToken = undefined;
13345+
}
13346+
}
13347+
13348+
function getDiagnosticsWorker(sourceFile: SourceFile): Diagnostic[] {
1330913349
throwIfNonDiagnosticsProducing();
1331013350
if (sourceFile) {
1331113351
checkSourceFile(sourceFile);

src/compiler/core.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -805,4 +805,4 @@ namespace ts {
805805
Debug.assert(false, message);
806806
}
807807
}
808-
}
808+
}

src/compiler/program.ts

+71-33
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,14 @@ namespace ts {
104104
};
105105
}
106106

107-
export function getPreEmitDiagnostics(program: Program, sourceFile?: SourceFile): Diagnostic[] {
108-
let diagnostics = program.getOptionsDiagnostics().concat(
109-
program.getSyntacticDiagnostics(sourceFile),
110-
program.getGlobalDiagnostics(),
111-
program.getSemanticDiagnostics(sourceFile));
107+
export function getPreEmitDiagnostics(program: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[] {
108+
let diagnostics = program.getOptionsDiagnostics(cancellationToken).concat(
109+
program.getSyntacticDiagnostics(sourceFile, cancellationToken),
110+
program.getGlobalDiagnostics(cancellationToken),
111+
program.getSemanticDiagnostics(sourceFile, cancellationToken));
112112

113113
if (program.getCompilerOptions().declaration) {
114-
diagnostics.concat(program.getDeclarationDiagnostics(sourceFile));
114+
diagnostics.concat(program.getDeclarationDiagnostics(sourceFile, cancellationToken));
115115
}
116116

117117
return sortAndDeduplicateDiagnostics(diagnostics);
@@ -233,10 +233,15 @@ namespace ts {
233233
return noDiagnosticsTypeChecker || (noDiagnosticsTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ false));
234234
}
235235

236-
function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback): EmitResult {
236+
function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult {
237+
return runWithCancellationToken(() => emitWorker(this, sourceFile, writeFileCallback, cancellationToken));
238+
}
239+
240+
function emitWorker(program: Program, sourceFile: SourceFile, writeFileCallback: WriteFileCallback, cancellationToken: CancellationToken): EmitResult {
237241
// If the noEmitOnError flag is set, then check if we have any errors so far. If so,
238-
// immediately bail out.
239-
if (options.noEmitOnError && getPreEmitDiagnostics(this).length > 0) {
242+
// immediately bail out. Note that we pass 'undefined' for 'sourceFile' so that we
243+
// get any preEmit diagnostics, not just the ones
244+
if (options.noEmitOnError && getPreEmitDiagnostics(program, /*sourceFile:*/ undefined, cancellationToken).length > 0) {
240245
return { diagnostics: [], sourceMaps: undefined, emitSkipped: true };
241246
}
242247

@@ -265,53 +270,86 @@ namespace ts {
265270
return filesByName.get(fileName);
266271
}
267272

268-
function getDiagnosticsHelper(sourceFile: SourceFile, getDiagnostics: (sourceFile: SourceFile) => Diagnostic[]): Diagnostic[] {
273+
function getDiagnosticsHelper(
274+
sourceFile: SourceFile,
275+
getDiagnostics: (sourceFile: SourceFile, cancellationToken: CancellationToken) => Diagnostic[],
276+
cancellationToken: CancellationToken): Diagnostic[] {
269277
if (sourceFile) {
270-
return getDiagnostics(sourceFile);
278+
return getDiagnostics(sourceFile, cancellationToken);
271279
}
272280

273281
let allDiagnostics: Diagnostic[] = [];
274282
forEach(program.getSourceFiles(), sourceFile => {
275-
addRange(allDiagnostics, getDiagnostics(sourceFile));
283+
if (cancellationToken) {
284+
cancellationToken.throwIfCancellationRequested();
285+
}
286+
addRange(allDiagnostics, getDiagnostics(sourceFile, cancellationToken));
276287
});
277288

278289
return sortAndDeduplicateDiagnostics(allDiagnostics);
279290
}
280291

281-
function getSyntacticDiagnostics(sourceFile?: SourceFile): Diagnostic[] {
282-
return getDiagnosticsHelper(sourceFile, getSyntacticDiagnosticsForFile);
292+
function getSyntacticDiagnostics(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] {
293+
return getDiagnosticsHelper(sourceFile, getSyntacticDiagnosticsForFile, cancellationToken);
283294
}
284295

285-
function getSemanticDiagnostics(sourceFile?: SourceFile): Diagnostic[] {
286-
return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile);
296+
function getSemanticDiagnostics(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] {
297+
return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken);
287298
}
288299

289-
function getDeclarationDiagnostics(sourceFile?: SourceFile): Diagnostic[] {
290-
return getDiagnosticsHelper(sourceFile, getDeclarationDiagnosticsForFile);
300+
function getDeclarationDiagnostics(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] {
301+
return getDiagnosticsHelper(sourceFile, getDeclarationDiagnosticsForFile, cancellationToken);
291302
}
292303

293-
function getSyntacticDiagnosticsForFile(sourceFile: SourceFile): Diagnostic[] {
304+
function getSyntacticDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] {
294305
return sourceFile.parseDiagnostics;
295306
}
296307

297-
function getSemanticDiagnosticsForFile(sourceFile: SourceFile): Diagnostic[] {
298-
let typeChecker = getDiagnosticsProducingTypeChecker();
308+
function runWithCancellationToken<T>(func: () => T): T {
309+
try {
310+
return func();
311+
}
312+
catch (e) {
313+
if (e instanceof OperationCanceledException) {
314+
// We were canceled while performing the operation. Because our type checker
315+
// might be a bad state, we need to throw it away.
316+
//
317+
// Note: we are overly agressive here. We do not actually *have* to throw away
318+
// the "noDiagnosticsTypeChecker". However, for simplicity, i'd like to keep
319+
// the lifetimes of these two TypeCheckers the same. Also, we generally only
320+
// cancel when the user has made a change anyways. And, in that case, we (the
321+
// program instance) will get thrown away anyways. So trying to keep one of
322+
// these type checkers alive doesn't serve much purpose.
323+
noDiagnosticsTypeChecker = undefined;
324+
diagnosticsProducingTypeChecker = undefined;
325+
}
299326

300-
Debug.assert(!!sourceFile.bindDiagnostics);
301-
let bindDiagnostics = sourceFile.bindDiagnostics;
302-
let checkDiagnostics = typeChecker.getDiagnostics(sourceFile);
303-
let programDiagnostics = diagnostics.getDiagnostics(sourceFile.fileName);
327+
throw e;
328+
}
329+
}
330+
331+
function getSemanticDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] {
332+
return runWithCancellationToken(() => {
333+
let typeChecker = getDiagnosticsProducingTypeChecker();
304334

305-
return bindDiagnostics.concat(checkDiagnostics).concat(programDiagnostics);
335+
Debug.assert(!!sourceFile.bindDiagnostics);
336+
let bindDiagnostics = sourceFile.bindDiagnostics;
337+
let checkDiagnostics = typeChecker.getDiagnostics(sourceFile, cancellationToken);
338+
let programDiagnostics = diagnostics.getDiagnostics(sourceFile.fileName);
339+
340+
return bindDiagnostics.concat(checkDiagnostics).concat(programDiagnostics);
341+
});
306342
}
307343

308-
function getDeclarationDiagnosticsForFile(sourceFile: SourceFile): Diagnostic[] {
309-
if (!isDeclarationFile(sourceFile)) {
310-
let resolver = getDiagnosticsProducingTypeChecker().getEmitResolver(sourceFile);
311-
// Don't actually write any files since we're just getting diagnostics.
312-
var writeFile: WriteFileCallback = () => { };
313-
return ts.getDeclarationDiagnostics(getEmitHost(writeFile), resolver, sourceFile);
314-
}
344+
function getDeclarationDiagnosticsForFile(sourceFile: SourceFile, cancellationToken: CancellationToken): Diagnostic[] {
345+
return runWithCancellationToken(() => {
346+
if (!isDeclarationFile(sourceFile)) {
347+
let resolver = getDiagnosticsProducingTypeChecker().getEmitResolver(sourceFile, cancellationToken);
348+
// Don't actually write any files since we're just getting diagnostics.
349+
var writeFile: WriteFileCallback = () => { };
350+
return ts.getDeclarationDiagnostics(getEmitHost(writeFile), resolver, sourceFile);
351+
}
352+
});
315353
}
316354

317355
function getOptionsDiagnostics(): Diagnostic[] {

src/compiler/types.ts

+17-13
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,15 @@ namespace ts {
12901290
(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void): void;
12911291
}
12921292

1293+
export class OperationCanceledException { }
1294+
1295+
export interface CancellationToken {
1296+
isCancellationRequested(): boolean;
1297+
1298+
/** @throws OperationCanceledException if isCancellationRequested is true */
1299+
throwIfCancellationRequested(): void;
1300+
}
1301+
12931302
export interface Program extends ScriptReferenceHost {
12941303
/**
12951304
* Get a list of files in the program
@@ -1306,13 +1315,13 @@ namespace ts {
13061315
* used for writing the JavaScript and declaration files. Otherwise, the writeFile parameter
13071316
* will be invoked when writing the JavaScript and declaration files.
13081317
*/
1309-
emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback): EmitResult;
1318+
emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
13101319

1311-
getOptionsDiagnostics(): Diagnostic[];
1312-
getGlobalDiagnostics(): Diagnostic[];
1313-
getSyntacticDiagnostics(sourceFile?: SourceFile): Diagnostic[];
1314-
getSemanticDiagnostics(sourceFile?: SourceFile): Diagnostic[];
1315-
getDeclarationDiagnostics(sourceFile?: SourceFile): Diagnostic[];
1320+
getOptionsDiagnostics(cancellationToken?: CancellationToken): Diagnostic[];
1321+
getGlobalDiagnostics(cancellationToken?: CancellationToken): Diagnostic[];
1322+
getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[];
1323+
getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[];
1324+
getDeclarationDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[];
13161325

13171326
/**
13181327
* Gets a type checker that can be used to semantically analyze source fils in the program.
@@ -1423,9 +1432,9 @@ namespace ts {
14231432
getJsxIntrinsicTagNames(): Symbol[];
14241433

14251434
// Should not be called directly. Should only be accessed through the Program instance.
1426-
/* @internal */ getDiagnostics(sourceFile?: SourceFile): Diagnostic[];
1435+
/* @internal */ getDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[];
14271436
/* @internal */ getGlobalDiagnostics(): Diagnostic[];
1428-
/* @internal */ getEmitResolver(sourceFile?: SourceFile): EmitResolver;
1437+
/* @internal */ getEmitResolver(sourceFile?: SourceFile, cancellationToken?: CancellationToken): EmitResolver;
14291438

14301439
/* @internal */ getNodeCount(): number;
14311440
/* @internal */ getIdentifierCount(): number;
@@ -2178,14 +2187,9 @@ namespace ts {
21782187
verticalTab = 0x0B, // \v
21792188
}
21802189

2181-
export interface CancellationToken {
2182-
isCancellationRequested(): boolean;
2183-
}
2184-
21852190
export interface CompilerHost {
21862191
getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile;
21872192
getDefaultLibFileName(options: CompilerOptions): string;
2188-
getCancellationToken? (): CancellationToken;
21892193
writeFile: WriteFileCallback;
21902194
getCurrentDirectory(): string;
21912195
getCanonicalFileName(fileName: string): string;

src/harness/fourslash.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,14 @@ module FourSlash {
190190
return "\nMarker: " + currentTestState.lastKnownMarker + "\nChecking: " + msg + "\n\n";
191191
}
192192

193-
export class TestCancellationToken implements ts.CancellationToken {
193+
export class TestCancellationToken implements ts.HostCancellationToken {
194194
// 0 - cancelled
195195
// >0 - not cancelled
196196
// <0 - not cancelled and value denotes number of isCancellationRequested after which token become cancelled
197-
private static NotCancelled: number = -1;
198-
private numberOfCallsBeforeCancellation: number = TestCancellationToken.NotCancelled;
199-
public isCancellationRequested(): boolean {
197+
private static NotCanceled: number = -1;
198+
private numberOfCallsBeforeCancellation: number = TestCancellationToken.NotCanceled;
200199

200+
public isCancellationRequested(): boolean {
201201
if (this.numberOfCallsBeforeCancellation < 0) {
202202
return false;
203203
}
@@ -216,7 +216,7 @@ module FourSlash {
216216
}
217217

218218
public resetCancelled(): void {
219-
this.numberOfCallsBeforeCancellation = TestCancellationToken.NotCancelled;
219+
this.numberOfCallsBeforeCancellation = TestCancellationToken.NotCanceled;
220220
}
221221
}
222222

0 commit comments

Comments
 (0)