From a4b9bbae907a7518cdd4e9fe5cddcc18d539297d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 9 Oct 2020 17:25:04 -0700 Subject: [PATCH] Handle noEmit and noEmitOnError with SemanticDiagnosticsBuilder (#40880) * Add test that fails * Handle noEmit on semantic builder's emit as well * Add test for tsbuildinfo text verification * Fix noEmit handling for tsbuildinfo emit with SemanticDiagnosticBuilder * Add test for noEmitOnError with SemanticDiagnosticsBuilder * Fix tsbuildinfo emit with SemanticDiagnosticsBuilder on noEmitOnError * Update src/compiler/builder.ts Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> * Update src/compiler/builder.ts Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> --- src/compiler/builder.ts | 73 +++++++---- src/compiler/program.ts | 7 ++ src/compiler/types.ts | 2 + src/testRunner/unittests/tscWatch/watchApi.ts | 115 ++++++++++++++++++ 4 files changed, 174 insertions(+), 23 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 01088d362f3fe..8d09baa270540 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -1019,31 +1019,57 @@ namespace ts { * in that order would be used to write the files */ function emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult { + let restorePendingEmitOnHandlingNoEmitSuccess = false; + let savedAffectedFilesPendingEmit; + let savedAffectedFilesPendingEmitKind; + let savedAffectedFilesPendingEmitIndex; + // Backup and restore affected pendings emit state for non emit Builder if noEmitOnError is enabled and emitBuildInfo could be written in case there are errors + // This ensures pending files to emit is updated in tsbuildinfo + // Note that when there are no errors, emit proceeds as if everything is emitted as it is callers reponsibility to write the files to disk if at all (because its builder that doesnt track files to emit) + if (kind !== BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram && + !targetSourceFile && + !outFile(state.compilerOptions) && + !state.compilerOptions.noEmit && + state.compilerOptions.noEmitOnError) { + restorePendingEmitOnHandlingNoEmitSuccess = true; + savedAffectedFilesPendingEmit = state.affectedFilesPendingEmit && state.affectedFilesPendingEmit.slice(); + savedAffectedFilesPendingEmitKind = state.affectedFilesPendingEmitKind && new Map(state.affectedFilesPendingEmitKind); + savedAffectedFilesPendingEmitIndex = state.affectedFilesPendingEmitIndex; + } + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile); - const result = handleNoEmitOptions(builderProgram, targetSourceFile, writeFile, cancellationToken); - if (result) return result; - if (!targetSourceFile) { - // Emit and report any errors we ran into. - let sourceMaps: SourceMapEmitResult[] = []; - let emitSkipped = false; - let diagnostics: Diagnostic[] | undefined; - let emittedFiles: string[] = []; - - let affectedEmitResult: AffectedFileResult; - while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) { - emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; - diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics); - emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles); - sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps); - } - return { - emitSkipped, - diagnostics: diagnostics || emptyArray, - emittedFiles, - sourceMaps - }; + } + const result = handleNoEmitOptions(builderProgram, targetSourceFile, writeFile, cancellationToken); + if (result) return result; + + if (restorePendingEmitOnHandlingNoEmitSuccess) { + state.affectedFilesPendingEmit = savedAffectedFilesPendingEmit; + state.affectedFilesPendingEmitKind = savedAffectedFilesPendingEmitKind; + state.affectedFilesPendingEmitIndex = savedAffectedFilesPendingEmitIndex; + } + + // Emit only affected files if using builder for emit + if (!targetSourceFile && kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + // Emit and report any errors we ran into. + let sourceMaps: SourceMapEmitResult[] = []; + let emitSkipped = false; + let diagnostics: Diagnostic[] | undefined; + let emittedFiles: string[] = []; + + let affectedEmitResult: AffectedFileResult; + while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) { + emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; + diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics); + emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles); + sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps); } + return { + emitSkipped, + diagnostics: diagnostics || emptyArray, + emittedFiles, + sourceMaps + }; } return Debug.checkDefined(state.program).emit(targetSourceFile, writeFile || maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles, customTransformers); } @@ -1069,7 +1095,8 @@ namespace ts { } // Add file to affected file pending emit to handle for later emit time - if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + // Apart for emit builder do this for tsbuildinfo, do this for non emit builder when noEmit is set as tsbuildinfo is written and reused between emitters + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram || state.compilerOptions.noEmit || state.compilerOptions.noEmitOnError) { addToAffectedFilesPendingEmit(state, (affected as SourceFile).resolvedPath, BuilderFileEmit.Full); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 2579bf7941e3d..1e41cdbf37aaf 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -937,6 +937,7 @@ namespace ts { getOptionsDiagnostics, getGlobalDiagnostics, getSemanticDiagnostics, + getCachedSemanticDiagnostics, getSuggestionDiagnostics, getDeclarationDiagnostics, getBindAndCheckDiagnostics, @@ -1693,6 +1694,12 @@ namespace ts { return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken); } + function getCachedSemanticDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] | undefined { + return sourceFile + ? cachedBindAndCheckDiagnosticsForFile.perFile?.get(sourceFile.path) + : cachedBindAndCheckDiagnosticsForFile.allDiagnostics; + } + function getBindAndCheckDiagnostics(sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { return getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f85b0a73694c8..0ce9c2afa7539 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3762,6 +3762,8 @@ namespace ts { /* @internal */ getDiagnosticsProducingTypeChecker(): TypeChecker; /* @internal */ dropDiagnosticsProducingTypeChecker(): void; + /* @internal */ getCachedSemanticDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] | undefined; + /* @internal */ getClassifiableNames(): Set<__String>; getTypeCatalog(): readonly Type[]; diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts index 0e648aaa1295c..5e32bb8c16164 100644 --- a/src/testRunner/unittests/tscWatch/watchApi.ts +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -128,4 +128,119 @@ namespace ts.tscWatch { checkProgramActualFiles(watch.getProgram().getProgram(), [mainFile.path, otherFile.path, libFile.path, other2]); }); }); + + describe("unittests:: tsc-watch:: watchAPI:: when watchHost uses createSemanticDiagnosticsBuilderProgram", () => { + function getWatch(config: File, optionsToExtend: CompilerOptions | undefined, sys: System, createProgram: CreateProgram) { + const watchCompilerHost = createWatchCompilerHost(config.path, optionsToExtend, sys, createProgram); + return createWatchProgram(watchCompilerHost); + } + + function setup(createProgram: CreateProgram, configText: string) { + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: configText + }; + const mainFile: File = { + path: `${projectRoot}/main.ts`, + content: "export const x = 10;" + }; + const otherFile: File = { + path: `${projectRoot}/other.ts`, + content: "export const y = 10;" + }; + const sys = createWatchedSystem([config, mainFile, otherFile, libFile]); + const watch = getWatch(config, { noEmit: true }, sys, createProgram); + return { sys, watch, mainFile, otherFile, config }; + } + + function verifyOutputs(sys: System, emitSys: System) { + for (const output of [`${projectRoot}/main.js`, `${projectRoot}/main.d.ts`, `${projectRoot}/other.js`, `${projectRoot}/other.d.ts`, `${projectRoot}/tsconfig.tsbuildinfo`]) { + assert.strictEqual(sys.readFile(output), emitSys.readFile(output), `Output file text for ${output}`); + } + } + + function verifyBuilder(config: File, sys: System, emitSys: System, createProgram: CreateProgram, createEmitProgram: CreateProgram, optionsToExtend?: CompilerOptions) { + const watch = getWatch(config, /*optionsToExtend*/ optionsToExtend, sys, createProgram); + const emitWatch = getWatch(config, /*optionsToExtend*/ optionsToExtend, emitSys, createEmitProgram); + verifyOutputs(sys, emitSys); + watch.close(); + emitWatch.close(); + } + + it("verifies that noEmit is handled on createSemanticDiagnosticsBuilderProgram and typechecking happens only on affected files", () => { + const { sys, watch, mainFile, otherFile } = setup(createSemanticDiagnosticsBuilderProgram, "{}"); + checkProgramActualFiles(watch.getProgram().getProgram(), [mainFile.path, otherFile.path, libFile.path]); + sys.appendFile(mainFile.path, "\n// SomeComment"); + sys.runQueuedTimeoutCallbacks(); + const program = watch.getProgram().getProgram(); + assert.deepEqual(program.getCachedSemanticDiagnostics(program.getSourceFile(mainFile.path)), []); + // Should not retrieve diagnostics for other file thats not changed + assert.deepEqual(program.getCachedSemanticDiagnostics(program.getSourceFile(otherFile.path)), /*expected*/ undefined); + }); + + it("noEmit with composite writes the tsbuildinfo with pending affected files correctly", () => { + const configText = JSON.stringify({ compilerOptions: { composite: true } }); + const { sys, watch, config, mainFile } = setup(createSemanticDiagnosticsBuilderProgram, configText); + const { sys: emitSys, watch: emitWatch } = setup(createEmitAndSemanticDiagnosticsBuilderProgram, configText); + verifyOutputs(sys, emitSys); + + watch.close(); + emitWatch.close(); + + // Emit on both sys should result in same output + verifyBuilder(config, sys, emitSys, createEmitAndSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram); + + // Change file + sys.appendFile(mainFile.path, "\n// SomeComment"); + emitSys.appendFile(mainFile.path, "\n// SomeComment"); + + // Verify noEmit results in same output + verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, { noEmit: true }); + + // Emit on both sys should result in same output + verifyBuilder(config, sys, emitSys, createEmitAndSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram); + + // Change file + sys.appendFile(mainFile.path, "\n// SomeComment"); + emitSys.appendFile(mainFile.path, "\n// SomeComment"); + + // Emit on both the builders should result in same files + verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram); + }); + + it("noEmitOnError with composite writes the tsbuildinfo with pending affected files correctly", () => { + const config: File = { + path: `${projectRoot}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { composite: true } }) + }; + const mainFile: File = { + path: `${projectRoot}/main.ts`, + content: "export const x: string = 10;" + }; + const otherFile: File = { + path: `${projectRoot}/other.ts`, + content: "export const y = 10;" + }; + const sys = createWatchedSystem([config, mainFile, otherFile, libFile]); + const emitSys = createWatchedSystem([config, mainFile, otherFile, libFile]); + + // Verify noEmit results in same output + verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, { noEmitOnError: true }); + + // Change file + sys.appendFile(mainFile.path, "\n// SomeComment"); + emitSys.appendFile(mainFile.path, "\n// SomeComment"); + + // Verify noEmit results in same output + verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, { noEmitOnError: true }); + + // Fix error + const fixed = "export const x = 10;"; + sys.appendFile(mainFile.path, fixed); + emitSys.appendFile(mainFile.path, fixed); + + // Emit on both the builders should result in same files + verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, { noEmitOnError: true }); + }); + }); }