From 60ad73da1fd828c829c8adf7eec03cc9bc6e550f Mon Sep 17 00:00:00 2001 From: TypeScript Bot Date: Wed, 14 Oct 2020 21:56:04 -0400 Subject: [PATCH] Cherry-pick PR #40880 into release-4.0 (#41107) Component commits: ca35546663 Add test that fails c7b5005aca Handle noEmit on semantic builder's emit as well 6a05abdff8 Add test for tsbuildinfo text verification 8bae5210ee Fix noEmit handling for tsbuildinfo emit with SemanticDiagnosticBuilder 0fd4a08b75 Add test for noEmitOnError with SemanticDiagnosticsBuilder 4fa1d78a6c Fix tsbuildinfo emit with SemanticDiagnosticsBuilder on noEmitOnError a3968e7574 Update src/compiler/builder.ts Co-authored-by: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> 174b00a9a6 Update src/compiler/builder.ts Co-authored-by: Sheetal Nandi --- 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 7765a75331281..6ef84502688a9 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 304f89f56c3ff..5a0300a53e53c 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -935,6 +935,7 @@ namespace ts { getOptionsDiagnostics, getGlobalDiagnostics, getSemanticDiagnostics, + getCachedSemanticDiagnostics, getSuggestionDiagnostics, getDeclarationDiagnostics, getBindAndCheckDiagnostics, @@ -1648,6 +1649,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 959ac410d294e..aa25879ae1f38 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3727,6 +3727,8 @@ namespace ts { /* @internal */ getDiagnosticsProducingTypeChecker(): TypeChecker; /* @internal */ dropDiagnosticsProducingTypeChecker(): void; + /* @internal */ getCachedSemanticDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] | undefined; + /* @internal */ getClassifiableNames(): Set<__String>; getNodeCount(): number; diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts index 6dcf1b30f7b35..1265fac6ed234 100644 --- a/src/testRunner/unittests/tscWatch/watchApi.ts +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -123,4 +123,119 @@ namespace ts.tscWatch { checkProgramActualFiles(watch.getProgram().getProgram(), [mainFile.path, otherFile.path, libFile.path]); }); }); + + 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 }); + }); + }); }