Skip to content

Commit f27cf9b

Browse files
authored
Merge pull request microsoft#30971 from Microsoft/dtsSignatureChange
Handle when change in type of dts may result in only dts emit but not js emit
2 parents d4ff58d + 2a29880 commit f27cf9b

File tree

12 files changed

+410
-59
lines changed

12 files changed

+410
-59
lines changed

src/compiler/builder.ts

Lines changed: 93 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,7 @@ namespace ts {
238238

239239
if (oldCompilerOptions && compilerOptionsAffectEmit(compilerOptions, oldCompilerOptions)) {
240240
// Add all files to affectedFilesPendingEmit since emit changed
241-
state.affectedFilesPendingEmit = concatenate(state.affectedFilesPendingEmit, newProgram.getSourceFiles().map(f => f.path));
242-
if (state.affectedFilesPendingEmitIndex === undefined) {
243-
state.affectedFilesPendingEmitIndex = 0;
244-
}
241+
addToAffectedFilesPendingEmit(state, newProgram.getSourceFiles().map(f => f.path));
245242
Debug.assert(state.seenAffectedFiles === undefined);
246243
state.seenAffectedFiles = createMap<true>();
247244
}
@@ -339,7 +336,7 @@ namespace ts {
339336
if (!seenAffectedFiles.has(affectedFile.path)) {
340337
// Set the next affected file as seen and remove the cached semantic diagnostics
341338
state.affectedFilesIndex = affectedFilesIndex;
342-
cleanSemanticDiagnosticsOfAffectedFile(state, affectedFile);
339+
handleDtsMayChangeOfAffectedFile(state, affectedFile, cancellationToken, computeHash);
343340
return affectedFile;
344341
}
345342
seenAffectedFiles.set(affectedFile.path, true);
@@ -406,31 +403,83 @@ namespace ts {
406403
}
407404

408405
/**
409-
* Remove the semantic diagnostics cached from old state for affected File and the files that are referencing modules that export entities from affected file
406+
* Handles semantic diagnostics and dts emit for affectedFile and files, that are referencing modules that export entities from affected file
407+
* This is because even though js emit doesnt change, dts emit / type used can change resulting in need for dts emit and js change
410408
*/
411-
function cleanSemanticDiagnosticsOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile) {
412-
if (removeSemanticDiagnosticsOf(state, affectedFile.path)) {
413-
// If there are no more diagnostics from old cache, done
409+
function handleDtsMayChangeOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash) {
410+
removeSemanticDiagnosticsOf(state, affectedFile.path);
411+
412+
// If affected files is everything except default library, then nothing more to do
413+
if (state.allFilesExcludingDefaultLibraryFile === state.affectedFiles) {
414+
if (!state.cleanedDiagnosticsOfLibFiles) {
415+
state.cleanedDiagnosticsOfLibFiles = true;
416+
const program = Debug.assertDefined(state.program);
417+
const options = program.getCompilerOptions();
418+
forEach(program.getSourceFiles(), f =>
419+
program.isSourceFileDefaultLibrary(f) &&
420+
!skipTypeChecking(f, options) &&
421+
removeSemanticDiagnosticsOf(state, f.path)
422+
);
423+
}
414424
return;
415425
}
416426

417-
// Clean lib file diagnostics if its all files excluding default files to emit
418-
if (state.allFilesExcludingDefaultLibraryFile === state.affectedFiles && !state.cleanedDiagnosticsOfLibFiles) {
419-
state.cleanedDiagnosticsOfLibFiles = true;
427+
forEachReferencingModulesOfExportOfAffectedFile(state, affectedFile, (state, path) => handleDtsMayChangeOf(state, path, cancellationToken, computeHash));
428+
}
429+
430+
/**
431+
* Handle the dts may change, so they need to be added to pending emit if dts emit is enabled,
432+
* Also we need to make sure signature is updated for these files
433+
*/
434+
function handleDtsMayChangeOf(state: BuilderProgramState, path: Path, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash) {
435+
removeSemanticDiagnosticsOf(state, path);
436+
437+
if (!state.changedFilesSet.has(path)) {
420438
const program = Debug.assertDefined(state.program);
421-
const options = program.getCompilerOptions();
422-
if (forEach(program.getSourceFiles(), f =>
423-
program.isSourceFileDefaultLibrary(f) &&
424-
!skipTypeChecking(f, options) &&
425-
removeSemanticDiagnosticsOf(state, f.path)
426-
)) {
427-
return;
439+
const sourceFile = program.getSourceFileByPath(path);
440+
if (sourceFile) {
441+
// Even though the js emit doesnt change and we are already handling dts emit and semantic diagnostics
442+
// we need to update the signature to reflect correctness of the signature(which is output d.ts emit) of this file
443+
// This ensures that we dont later during incremental builds considering wrong signature.
444+
// Eg where this also is needed to ensure that .tsbuildinfo generated by incremental build should be same as if it was first fresh build
445+
BuilderState.updateShapeSignature(
446+
state,
447+
program,
448+
sourceFile,
449+
Debug.assertDefined(state.currentAffectedFilesSignatures),
450+
cancellationToken,
451+
computeHash,
452+
state.currentAffectedFilesExportedModulesMap
453+
);
454+
// If not dts emit, nothing more to do
455+
if (getEmitDeclarations(state.compilerOptions)) {
456+
addToAffectedFilesPendingEmit(state, [path]);
457+
}
428458
}
429459
}
430460

431-
// If there was change in signature for the changed file,
432-
// then delete the semantic diagnostics for files that are affected by using exports of this module
461+
return false;
462+
}
433463

464+
/**
465+
* Removes semantic diagnostics for path and
466+
* returns true if there are no more semantic diagnostics from the old state
467+
*/
468+
function removeSemanticDiagnosticsOf(state: BuilderProgramState, path: Path) {
469+
if (!state.semanticDiagnosticsFromOldState) {
470+
return true;
471+
}
472+
state.semanticDiagnosticsFromOldState.delete(path);
473+
state.semanticDiagnosticsPerFile!.delete(path);
474+
return !state.semanticDiagnosticsFromOldState.size;
475+
}
476+
477+
/**
478+
* Iterate on referencing modules that export entities from affected file
479+
*/
480+
function forEachReferencingModulesOfExportOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile, fn: (state: BuilderProgramState, filePath: Path) => boolean) {
481+
// If there was change in signature (dts output) for the changed file,
482+
// then only we need to handle pending file emit
434483
if (!state.exportedModulesMap || state.affectedFiles!.length === 1 || !state.changedFilesSet.has(affectedFile.path)) {
435484
return;
436485
}
@@ -442,7 +491,7 @@ namespace ts {
442491
if (forEachEntry(state.currentAffectedFilesExportedModulesMap!, (exportedModules, exportedFromPath) =>
443492
exportedModules &&
444493
exportedModules.has(affectedFile.path) &&
445-
removeSemanticDiagnosticsOfFilesReferencingPath(state, exportedFromPath as Path, seenFileAndExportsOfFile)
494+
forEachFilesReferencingPath(state, exportedFromPath as Path, seenFileAndExportsOfFile, fn)
446495
)) {
447496
return;
448497
}
@@ -451,29 +500,28 @@ namespace ts {
451500
forEachEntry(state.exportedModulesMap, (exportedModules, exportedFromPath) =>
452501
!state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it
453502
exportedModules.has(affectedFile.path) &&
454-
removeSemanticDiagnosticsOfFilesReferencingPath(state, exportedFromPath as Path, seenFileAndExportsOfFile)
503+
forEachFilesReferencingPath(state, exportedFromPath as Path, seenFileAndExportsOfFile, fn)
455504
);
456505
}
457506

458507
/**
459-
* removes the semantic diagnostics of files referencing referencedPath and
460-
* returns true if there are no more semantic diagnostics from old state
508+
* Iterate on files referencing referencedPath
461509
*/
462-
function removeSemanticDiagnosticsOfFilesReferencingPath(state: BuilderProgramState, referencedPath: Path, seenFileAndExportsOfFile: Map<true>) {
510+
function forEachFilesReferencingPath(state: BuilderProgramState, referencedPath: Path, seenFileAndExportsOfFile: Map<true>, fn: (state: BuilderProgramState, filePath: Path) => boolean) {
463511
return forEachEntry(state.referencedMap!, (referencesInFile, filePath) =>
464-
referencesInFile.has(referencedPath) && removeSemanticDiagnosticsOfFileAndExportsOfFile(state, filePath as Path, seenFileAndExportsOfFile)
512+
referencesInFile.has(referencedPath) && forEachFileAndExportsOfFile(state, filePath as Path, seenFileAndExportsOfFile, fn)
465513
);
466514
}
467515

468516
/**
469-
* Removes semantic diagnostics of file and anything that exports this file
517+
* fn on file and iterate on anything that exports this file
470518
*/
471-
function removeSemanticDiagnosticsOfFileAndExportsOfFile(state: BuilderProgramState, filePath: Path, seenFileAndExportsOfFile: Map<true>): boolean {
519+
function forEachFileAndExportsOfFile(state: BuilderProgramState, filePath: Path, seenFileAndExportsOfFile: Map<true>, fn: (state: BuilderProgramState, filePath: Path) => boolean): boolean {
472520
if (!addToSeen(seenFileAndExportsOfFile, filePath)) {
473521
return false;
474522
}
475523

476-
if (removeSemanticDiagnosticsOf(state, filePath)) {
524+
if (fn(state, filePath)) {
477525
// If there are no more diagnostics from old cache, done
478526
return true;
479527
}
@@ -484,7 +532,7 @@ namespace ts {
484532
if (forEachEntry(state.currentAffectedFilesExportedModulesMap!, (exportedModules, exportedFromPath) =>
485533
exportedModules &&
486534
exportedModules.has(filePath) &&
487-
removeSemanticDiagnosticsOfFileAndExportsOfFile(state, exportedFromPath as Path, seenFileAndExportsOfFile)
535+
forEachFileAndExportsOfFile(state, exportedFromPath as Path, seenFileAndExportsOfFile, fn)
488536
)) {
489537
return true;
490538
}
@@ -493,7 +541,7 @@ namespace ts {
493541
if (forEachEntry(state.exportedModulesMap!, (exportedModules, exportedFromPath) =>
494542
!state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it
495543
exportedModules.has(filePath) &&
496-
removeSemanticDiagnosticsOfFileAndExportsOfFile(state, exportedFromPath as Path, seenFileAndExportsOfFile)
544+
forEachFileAndExportsOfFile(state, exportedFromPath as Path, seenFileAndExportsOfFile, fn)
497545
)) {
498546
return true;
499547
}
@@ -502,22 +550,10 @@ namespace ts {
502550
return !!forEachEntry(state.referencedMap!, (referencesInFile, referencingFilePath) =>
503551
referencesInFile.has(filePath) &&
504552
!seenFileAndExportsOfFile.has(referencingFilePath) && // Not already removed diagnostic file
505-
removeSemanticDiagnosticsOf(state, referencingFilePath as Path) // Dont add to seen since this is not yet done with the export removal
553+
fn(state, referencingFilePath as Path) // Dont add to seen since this is not yet done with the export removal
506554
);
507555
}
508556

509-
/**
510-
* Removes semantic diagnostics for path and
511-
* returns true if there are no more semantic diagnostics from the old state
512-
*/
513-
function removeSemanticDiagnosticsOf(state: BuilderProgramState, path: Path) {
514-
if (!state.semanticDiagnosticsFromOldState) {
515-
return true;
516-
}
517-
state.semanticDiagnosticsFromOldState.delete(path);
518-
state.semanticDiagnosticsPerFile!.delete(path);
519-
return !state.semanticDiagnosticsFromOldState.size;
520-
}
521557

522558
/**
523559
* This is called after completing operation on the next affected file.
@@ -808,11 +844,6 @@ namespace ts {
808844
}
809845
}
810846

811-
// Mark seen emitted files if there are pending files to be emitted
812-
if (state.affectedFilesPendingEmit && state.program !== affected) {
813-
(state.seenEmittedFiles || (state.seenEmittedFiles = createMap())).set((affected as SourceFile).path, true);
814-
}
815-
816847
return toAffectedFileResult(
817848
state,
818849
// When whole program is affected, do emit only once (eg when --out or --outFile is specified)
@@ -931,14 +962,7 @@ namespace ts {
931962

932963
// In case of emit builder, cache the files to be emitted
933964
if (affectedFilesPendingEmit) {
934-
state.affectedFilesPendingEmit = concatenate(state.affectedFilesPendingEmit, affectedFilesPendingEmit);
935-
// affectedFilesPendingEmitIndex === undefined
936-
// - means the emit state.affectedFilesPendingEmit was undefined before adding current affected files
937-
// so start from 0 as array would be affectedFilesPendingEmit
938-
// else, continue to iterate from existing index, the current set is appended to existing files
939-
if (state.affectedFilesPendingEmitIndex === undefined) {
940-
state.affectedFilesPendingEmitIndex = 0;
941-
}
965+
addToAffectedFilesPendingEmit(state, affectedFilesPendingEmit);
942966
}
943967

944968
let diagnostics: Diagnostic[] | undefined;
@@ -949,6 +973,17 @@ namespace ts {
949973
}
950974
}
951975

976+
function addToAffectedFilesPendingEmit(state: BuilderProgramState, affectedFilesPendingEmit: readonly Path[]) {
977+
state.affectedFilesPendingEmit = concatenate(state.affectedFilesPendingEmit, affectedFilesPendingEmit);
978+
// affectedFilesPendingEmitIndex === undefined
979+
// - means the emit state.affectedFilesPendingEmit was undefined before adding current affected files
980+
// so start from 0 as array would be affectedFilesPendingEmit
981+
// else, continue to iterate from existing index, the current set is appended to existing files
982+
if (state.affectedFilesPendingEmitIndex === undefined) {
983+
state.affectedFilesPendingEmitIndex = 0;
984+
}
985+
}
986+
952987
function getMapOfReferencedSet(mapLike: MapLike<ReadonlyArray<string>> | undefined): ReadonlyMap<BuilderState.ReferencedSet> | undefined {
953988
if (!mapLike) return undefined;
954989
const map = createMap<BuilderState.ReferencedSet>();

src/compiler/builderState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ namespace ts.BuilderState {
321321
/**
322322
* Returns if the shape of the signature has changed since last emit
323323
*/
324-
function updateShapeSignature(state: Readonly<BuilderState>, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache?: ComputingExportedModulesMap) {
324+
export function updateShapeSignature(state: Readonly<BuilderState>, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache?: ComputingExportedModulesMap) {
325325
Debug.assert(!!sourceFile);
326326
Debug.assert(!exportedModulesMapCache || !!state.exportedModulesMap, "Compute visible to outside map only if visibleToOutsideReferencedMap present in the state");
327327

src/testRunner/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"unittests/tsbuild/amdModulesWithOut.ts",
9393
"unittests/tsbuild/emptyFiles.ts",
9494
"unittests/tsbuild/graphOrdering.ts",
95+
"unittests/tsbuild/inferredTypeFromTransitiveModule.ts",
9596
"unittests/tsbuild/lateBoundSymbol.ts",
9697
"unittests/tsbuild/missingExtendedFile.ts",
9798
"unittests/tsbuild/outFile.ts",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
namespace ts {
2+
describe("unittests:: tsbuild:: inferredTypeFromTransitiveModule::", () => {
3+
let projFs: vfs.FileSystem;
4+
const { time, tick } = getTime();
5+
before(() => {
6+
projFs = loadProjectFromDisk("tests/projects/inferredTypeFromTransitiveModule", time);
7+
});
8+
after(() => {
9+
projFs = undefined!;
10+
});
11+
12+
verifyTsbuildOutput({
13+
scenario: "inferred type from transitive module",
14+
projFs: () => projFs,
15+
time,
16+
tick,
17+
proj: "inferredTypeFromTransitiveModule",
18+
rootNames: ["/src"],
19+
expectedMapFileNames: emptyArray,
20+
lastProjectOutputJs: `/src/obj/index.js`,
21+
outputFiles: [
22+
"/src/obj/bar.js", "/src/obj/bar.d.ts",
23+
"/src/obj/bundling.js", "/src/obj/bundling.d.ts",
24+
"/src/obj/lazyIndex.js", "/src/obj/lazyIndex.d.ts",
25+
"/src/obj/index.js", "/src/obj/index.d.ts",
26+
"/src/obj/tsconfig.tsbuildinfo"
27+
],
28+
initialBuild: {
29+
modifyFs: noop,
30+
expectedDiagnostics: [
31+
getExpectedDiagnosticForProjectsInBuild("src/tsconfig.json"),
32+
[Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, "src/tsconfig.json", "src/obj/bar.js"],
33+
[Diagnostics.Building_project_0, "/src/tsconfig.json"]
34+
]
35+
},
36+
incrementalDtsChangedBuild: {
37+
modifyFs: fs => replaceText(fs, "/src/bar.ts", "param: string", ""),
38+
expectedDiagnostics: [
39+
getExpectedDiagnosticForProjectsInBuild("src/tsconfig.json"),
40+
[Diagnostics.Project_0_is_out_of_date_because_oldest_output_1_is_older_than_newest_input_2, "src/tsconfig.json", "src/obj/bar.js", "src/bar.ts"],
41+
[Diagnostics.Building_project_0, "/src/tsconfig.json"],
42+
[Diagnostics.Updating_unchanged_output_timestamps_of_project_0, "/src/tsconfig.json"]
43+
]
44+
},
45+
baselineOnly: true,
46+
verifyDiagnostics: true
47+
});
48+
});
49+
}

0 commit comments

Comments
 (0)