diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 69f60bb1708..ddbb015a2ee 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -22,7 +22,7 @@ namespace ts { * This api is only for internal use */ /*@internal*/ - getFilesAffectedBy(programOfThisState: Program, path: Path): ReadonlyArray; + getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken): ReadonlyArray; } /** @@ -86,7 +86,7 @@ namespace ts { * Get the files affected by the source file. * This is dependent on whether its a module emit or not and hence function expression */ - let getEmitDependentFilesAffectedBy: (programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map | undefined) => ReadonlyArray; + let getEmitDependentFilesAffectedBy: (programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined) => ReadonlyArray; /** * Cache of semantic diagnostics for files with their Path being the key @@ -272,38 +272,65 @@ namespace ts { /** * Gets the files affected by the path from the program */ - function getFilesAffectedBy(programOfThisState: Program, path: Path, cacheToUpdateSignature?: Map): ReadonlyArray { + function getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, cacheToUpdateSignature?: Map): ReadonlyArray { + // Since the operation could be cancelled, the signatures are always stored in the cache + // They will be commited once it is safe to use them + // eg when calling this api from tsserver, if there is no cancellation of the operation + // In the other cases the affected files signatures are commited only after the iteration through the result is complete + const signatureCache = cacheToUpdateSignature || createMap(); const sourceFile = programOfThisState.getSourceFileByPath(path); if (!sourceFile) { return emptyArray; } - if (!updateShapeSignature(programOfThisState, sourceFile, cacheToUpdateSignature)) { + if (!updateShapeSignature(programOfThisState, sourceFile, signatureCache, cancellationToken)) { return [sourceFile]; } - return getEmitDependentFilesAffectedBy(programOfThisState, sourceFile, cacheToUpdateSignature); + const result = getEmitDependentFilesAffectedBy(programOfThisState, sourceFile, signatureCache, cancellationToken); + if (!cacheToUpdateSignature) { + // Commit all the signatures in the signature cache + updateSignaturesFromCache(signatureCache); + } + return result; } - function getNextAffectedFile(programOfThisState: Program): SourceFile | Program | undefined { + /** + * Updates the signatures from the cache + * This should be called whenever it is safe to commit the state of the builder + */ + function updateSignaturesFromCache(signatureCache: Map) { + signatureCache.forEach((signature, path) => { + fileInfos.get(path).signature = signature; + hasCalledUpdateShapeSignature.set(path, true); + }); + } + + /** + * This function returns the next affected file to be processed. + * Note that until doneAffected is called it would keep reporting same result + * This is to allow the callers to be able to actually remove affected file only when the operation is complete + * eg. if during diagnostics check cancellation token ends up cancelling the request, the affected file should be retained + */ + function getNextAffectedFile(programOfThisState: Program, cancellationToken: CancellationToken | undefined): SourceFile | Program | undefined { while (true) { if (affectedFiles) { while (affectedFilesIndex < affectedFiles.length) { const affectedFile = affectedFiles[affectedFilesIndex]; - affectedFilesIndex++; if (!seenAffectedFiles.has(affectedFile.path)) { // Set the next affected file as seen and remove the cached semantic diagnostics - seenAffectedFiles.set(affectedFile.path, true); semanticDiagnosticsPerFile.delete(affectedFile.path); return affectedFile; } + seenAffectedFiles.set(affectedFile.path, true); + affectedFilesIndex++; } // Remove the changed file from the change set changedFilesSet.delete(currentChangedFilePath); currentChangedFilePath = undefined; // Commit the changes in file signature - currentAffectedFilesSignatures.forEach((signature, path) => fileInfos.get(path).signature = signature); + updateSignaturesFromCache(currentAffectedFilesSignatures); currentAffectedFilesSignatures.clear(); affectedFiles = undefined; } @@ -320,21 +347,37 @@ namespace ts { // so operations are performed directly on program, return program if (compilerOptions.outFile || compilerOptions.out) { Debug.assert(semanticDiagnosticsPerFile.size === 0); - changedFilesSet.clear(); return programOfThisState; } // Get next batch of affected files + currentAffectedFilesSignatures.clear(); + affectedFiles = getFilesAffectedBy(programOfThisState, nextKey.value as Path, cancellationToken, currentAffectedFilesSignatures); currentChangedFilePath = nextKey.value as Path; + semanticDiagnosticsPerFile.delete(currentChangedFilePath); affectedFilesIndex = 0; - affectedFiles = getFilesAffectedBy(programOfThisState, nextKey.value as Path, currentAffectedFilesSignatures); + } + } + + /** + * This is called after completing operation on the next affected file. + * The operations here are postponed to ensure that cancellation during the iteration is handled correctly + */ + function doneWithAffectedFile(programOfThisState: Program, affected: SourceFile | Program) { + if (affected === programOfThisState) { + changedFilesSet.clear(); + } + else { + seenAffectedFiles.set((affected).path, true); + affectedFilesIndex++; } } /** * Returns the result with affected file */ - function toAffectedFileResult(result: T, affected: SourceFile | Program): AffectedFileResult { + function toAffectedFileResult(programOfThisState: Program, result: T, affected: SourceFile | Program): AffectedFileResult { + doneWithAffectedFile(programOfThisState, affected); return { result, affected }; } @@ -343,7 +386,7 @@ namespace ts { * Returns undefined when iteration is complete */ function emitNextAffectedFile(programOfThisState: Program, writeFileCallback: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult { - const affectedFile = getNextAffectedFile(programOfThisState); + const affectedFile = getNextAffectedFile(programOfThisState, cancellationToken); if (!affectedFile) { // Done return undefined; @@ -351,6 +394,7 @@ namespace ts { else if (affectedFile === programOfThisState) { // When whole program is affected, do emit only once (eg when --out or --outFile is specified) return toAffectedFileResult( + programOfThisState, programOfThisState.emit(/*targetSourceFile*/ undefined, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers), programOfThisState ); @@ -359,6 +403,7 @@ namespace ts { // Emit the affected file const targetSourceFile = affectedFile as SourceFile; return toAffectedFileResult( + programOfThisState, programOfThisState.emit(targetSourceFile, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers), targetSourceFile ); @@ -370,7 +415,7 @@ namespace ts { */ function getSemanticDiagnosticsOfNextAffectedFile(programOfThisState: Program, cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult> { while (true) { - const affectedFile = getNextAffectedFile(programOfThisState); + const affectedFile = getNextAffectedFile(programOfThisState, cancellationToken); if (!affectedFile) { // Done return undefined; @@ -378,6 +423,7 @@ namespace ts { else if (affectedFile === programOfThisState) { // When whole program is affected, get all semantic diagnostics (eg when --out or --outFile is specified) return toAffectedFileResult( + programOfThisState, programOfThisState.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken), programOfThisState ); @@ -387,10 +433,12 @@ namespace ts { const targetSourceFile = affectedFile as SourceFile; if (ignoreSourceFile && ignoreSourceFile(targetSourceFile)) { // Get next affected file + doneWithAffectedFile(programOfThisState, targetSourceFile); continue; } return toAffectedFileResult( + programOfThisState, getSemanticDiagnosticsOfFile(programOfThisState, targetSourceFile, cancellationToken), targetSourceFile ); @@ -505,16 +553,14 @@ namespace ts { * Returns if the shape of the signature has changed since last emit * Note that it also updates the current signature as the latest signature for the file */ - function updateShapeSignature(program: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map | undefined) { + function updateShapeSignature(program: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined) { Debug.assert(!!sourceFile); // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate - if (hasCalledUpdateShapeSignature.has(sourceFile.path)) { + if (hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) { return false; } - Debug.assert(!cacheToUpdateSignature || !cacheToUpdateSignature.has(sourceFile.path)); - hasCalledUpdateShapeSignature.set(sourceFile.path, true); const info = fileInfos.get(sourceFile.path); Debug.assert(!!info); @@ -522,29 +568,19 @@ namespace ts { let latestSignature: string; if (sourceFile.isDeclarationFile) { latestSignature = sourceFile.version; - setLatestSigature(); } else { - const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true); + const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { latestSignature = options.computeHash(emitOutput.outputFiles[0].text); - setLatestSigature(); } else { latestSignature = prevSignature; } } + cacheToUpdateSignature.set(sourceFile.path, latestSignature); return !prevSignature || latestSignature !== prevSignature; - - function setLatestSigature() { - if (cacheToUpdateSignature) { - cacheToUpdateSignature.set(sourceFile.path, latestSignature); - } - else { - info.signature = latestSignature; - } - } } /** @@ -652,7 +688,7 @@ namespace ts { /** * When program emits modular code, gets the files affected by the sourceFile whose shape has changed */ - function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map | undefined) { + function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined) { if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) { return getAllFilesExcludingDefaultLibraryFile(programOfThisState, sourceFileWithUpdatedShape); } @@ -675,7 +711,7 @@ namespace ts { if (!seenFileNamesMap.has(currentPath)) { const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath); seenFileNamesMap.set(currentPath, currentSourceFile); - if (currentSourceFile && updateShapeSignature(programOfThisState, currentSourceFile, cacheToUpdateSignature)) { + if (currentSourceFile && updateShapeSignature(programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken)) { queue.push(...getReferencedByPaths(currentPath)); } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 4088a6951a1..213b1fc9601 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1162,7 +1162,7 @@ namespace ts { // This is because in the -out scenario all files need to be emitted, and therefore all // files need to be type checked. And the way to specify that all files need to be type // checked is to not pass the file to getEmitResolver. - const emitResolver = getDiagnosticsProducingTypeChecker().getEmitResolver((options.outFile || options.out) ? undefined : sourceFile); + const emitResolver = getDiagnosticsProducingTypeChecker().getEmitResolver((options.outFile || options.out) ? undefined : sourceFile, cancellationToken); performance.mark("beforeEmit"); diff --git a/src/harness/unittests/builder.ts b/src/harness/unittests/builder.ts index a9c16c59fbd..64f43be1376 100644 --- a/src/harness/unittests/builder.ts +++ b/src/harness/unittests/builder.ts @@ -41,9 +41,39 @@ namespace ts { program = updateProgramFile(program, "/b.ts", "namespace B { export const x = 1; }"); assertChanges(["/b.js", "/a.js"]); }); + + it("keeps the file in affected files if cancellation token throws during the operation", () => { + const files: NamedSourceText[] = [ + { name: "/a.ts", text: SourceText.New("", 'import { b } from "./b";', "") }, + { name: "/b.ts", text: SourceText.New("", ' import { c } from "./c";', "export const b = c;") }, + { name: "/c.ts", text: SourceText.New("", "", "export const c = 0;") }, + { name: "/d.ts", text: SourceText.New("", "", "export const dd = 0;") }, + { name: "/e.ts", text: SourceText.New("", "", "export const ee = 0;") }, + ]; + + let program = newProgram(files, ["/d.ts", "/e.ts", "/a.ts"], {}); + const assertChanges = makeAssertChangesWithCancellationToken(() => program); + // No cancellation + assertChanges(["/d.js", "/e.js", "/c.js", "/b.js", "/a.js"]); + + // cancel when emitting a.ts + program = updateProgramFile(program, "/a.ts", "export function foo() { }"); + assertChanges(["/a.js"], 0); + // Change d.ts and verify previously pending a.ts is emitted as well + program = updateProgramFile(program, "/d.ts", "export function bar() { }"); + assertChanges(["/a.js", "/d.js"]); + + // Cancel when emitting b.js + program = updateProgramFile(program, "/b.ts", "export class b { foo() { c + 1; } }"); + program = updateProgramFile(program, "/d.ts", "export function bar2() { }"); + assertChanges(["/d.js", "/b.js", "/a.js"], 1); + // Change e.ts and verify previously b.js as well as a.js get emitted again since previous change was consumed completely but not d.ts + program = updateProgramFile(program, "/e.ts", "export function bar3() { }"); + assertChanges(["/b.js", "/a.js", "/e.js"]); + }); }); - function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { + function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { const builder = createEmitAndSemanticDiagnosticsBuilder({ getCanonicalFileName: identity, computeHash: identity @@ -59,6 +89,46 @@ namespace ts { }; } + function makeAssertChangesWithCancellationToken(getProgram: () => Program): (fileNames: ReadonlyArray, cancelAfterEmitLength?: number) => void { + const builder = createEmitAndSemanticDiagnosticsBuilder({ + getCanonicalFileName: identity, + computeHash: identity + }); + let cancel = false; + const cancellationToken: CancellationToken = { + isCancellationRequested: () => cancel, + throwIfCancellationRequested: () => { + if (cancel) { + throw new OperationCanceledException(); + } + }, + }; + return (fileNames, cancelAfterEmitLength?: number) => { + cancel = false; + let operationWasCancelled = false; + const program = getProgram(); + builder.updateProgram(program); + const outputFileNames: string[] = []; + try { + // tslint:disable-next-line no-empty + do { + assert.isFalse(cancel); + if (outputFileNames.length === cancelAfterEmitLength) { + cancel = true; + } + } while (builder.emitNextAffectedFile(program, fileName => outputFileNames.push(fileName), cancellationToken)); + } + catch (e) { + assert.isFalse(operationWasCancelled); + assert.isTrue(e instanceof OperationCanceledException, e.toString()); + operationWasCancelled = true; + } + assert.equal(cancel, operationWasCancelled); + assert.equal(operationWasCancelled, fileNames.length > cancelAfterEmitLength); + assert.deepEqual(outputFileNames, fileNames.slice(0, cancelAfterEmitLength)); + }; + } + function updateProgramFile(program: ProgramWithSourceTexts, fileName: string, fileContent: string): ProgramWithSourceTexts { return updateProgram(program, program.getRootFileNames(), program.getCompilerOptions(), files => { updateProgramText(files, fileName, fileContent); diff --git a/src/server/project.ts b/src/server/project.ts index 3a5785163d2..9b9dbb99436 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -458,7 +458,7 @@ namespace ts.server { }); } this.builder.updateProgram(this.program); - return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path), + return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path, this.cancellationToken), sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); }