Skip to content

Commit 45faac7

Browse files
authored
If we are updating dts of any of the file and it affects global scope, everything needs update in signature and dts emit (#48600)
* Add test * Renames * More refactoring * If we are updating dts of any of the file and it affects global scope, everything needs update in signature and dts emit Fixes #42769
1 parent 273a567 commit 45faac7

File tree

4 files changed

+681
-68
lines changed

4 files changed

+681
-68
lines changed

src/compiler/builder.ts

Lines changed: 130 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -434,25 +434,35 @@ namespace ts {
434434
return undefined;
435435
}
436436

437+
function removeDiagnosticsOfLibraryFiles(state: BuilderProgramState) {
438+
if (!state.cleanedDiagnosticsOfLibFiles) {
439+
state.cleanedDiagnosticsOfLibFiles = true;
440+
const program = Debug.checkDefined(state.program);
441+
const options = program.getCompilerOptions();
442+
forEach(program.getSourceFiles(), f =>
443+
program.isSourceFileDefaultLibrary(f) &&
444+
!skipTypeChecking(f, options, program) &&
445+
removeSemanticDiagnosticsOf(state, f.resolvedPath)
446+
);
447+
}
448+
}
449+
437450
/**
438451
* Handles semantic diagnostics and dts emit for affectedFile and files, that are referencing modules that export entities from affected file
439452
* This is because even though js emit doesnt change, dts emit / type used can change resulting in need for dts emit and js change
440453
*/
441-
function handleDtsMayChangeOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash, host: BuilderProgramHost) {
454+
function handleDtsMayChangeOfAffectedFile(
455+
state: BuilderProgramState,
456+
affectedFile: SourceFile,
457+
cancellationToken: CancellationToken | undefined,
458+
computeHash: BuilderState.ComputeHash,
459+
host: BuilderProgramHost,
460+
) {
442461
removeSemanticDiagnosticsOf(state, affectedFile.resolvedPath);
443462

444463
// If affected files is everything except default library, then nothing more to do
445464
if (state.allFilesExcludingDefaultLibraryFile === state.affectedFiles) {
446-
if (!state.cleanedDiagnosticsOfLibFiles) {
447-
state.cleanedDiagnosticsOfLibFiles = true;
448-
const program = Debug.checkDefined(state.program);
449-
const options = program.getCompilerOptions();
450-
forEach(program.getSourceFiles(), f =>
451-
program.isSourceFileDefaultLibrary(f) &&
452-
!skipTypeChecking(f, options, program) &&
453-
removeSemanticDiagnosticsOf(state, f.resolvedPath)
454-
);
455-
}
465+
removeDiagnosticsOfLibraryFiles(state);
456466
// When a change affects the global scope, all files are considered to be affected without updating their signature
457467
// That means when affected file is handled, its signature can be out of date
458468
// To avoid this, ensure that we update the signature for any affected file in this scenario.
@@ -467,20 +477,22 @@ namespace ts {
467477
);
468478
return;
469479
}
470-
else {
471-
Debug.assert(state.hasCalledUpdateShapeSignature.has(affectedFile.resolvedPath) || state.currentAffectedFilesSignatures?.has(affectedFile.resolvedPath), `Signature not updated for affected file: ${affectedFile.fileName}`);
472-
}
473-
474-
if (!state.compilerOptions.assumeChangesOnlyAffectDirectDependencies) {
475-
forEachReferencingModulesOfExportOfAffectedFile(state, affectedFile, (state, path) => handleDtsMayChangeOf(state, path, cancellationToken, computeHash, host));
476-
}
480+
Debug.assert(state.hasCalledUpdateShapeSignature.has(affectedFile.resolvedPath) || state.currentAffectedFilesSignatures?.has(affectedFile.resolvedPath), `Signature not updated for affected file: ${affectedFile.fileName}`);
481+
if (state.compilerOptions.assumeChangesOnlyAffectDirectDependencies) return;
482+
handleDtsMayChangeOfReferencingExportOfAffectedFile(state, affectedFile, cancellationToken, computeHash, host);
477483
}
478484

479485
/**
480486
* Handle the dts may change, so they need to be added to pending emit if dts emit is enabled,
481487
* Also we need to make sure signature is updated for these files
482488
*/
483-
function handleDtsMayChangeOf(state: BuilderProgramState, path: Path, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash, host: BuilderProgramHost): void {
489+
function handleDtsMayChangeOf(
490+
state: BuilderProgramState,
491+
path: Path,
492+
cancellationToken: CancellationToken | undefined,
493+
computeHash: BuilderState.ComputeHash,
494+
host: BuilderProgramHost
495+
): void {
484496
removeSemanticDiagnosticsOf(state, path);
485497

486498
if (!state.changedFilesSet.has(path)) {
@@ -529,16 +541,61 @@ namespace ts {
529541
return newSignature !== oldSignature;
530542
}
531543

544+
function forEachKeyOfExportedModulesMap<T>(
545+
state: BuilderProgramState,
546+
filePath: Path,
547+
fn: (exportedFromPath: Path) => T | undefined,
548+
): T | undefined {
549+
// Go through exported modules from cache first
550+
let keys = state.currentAffectedFilesExportedModulesMap!.getKeys(filePath);
551+
const result = keys && forEachKey(keys, fn);
552+
if (result) return result;
553+
554+
// If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected
555+
keys = state.exportedModulesMap!.getKeys(filePath);
556+
return keys && forEachKey(keys, exportedFromPath =>
557+
// If the cache had an updated value, skip
558+
!state.currentAffectedFilesExportedModulesMap!.hasKey(exportedFromPath) &&
559+
!state.currentAffectedFilesExportedModulesMap!.deletedKeys()?.has(exportedFromPath) ?
560+
fn(exportedFromPath) :
561+
undefined
562+
);
563+
}
564+
565+
function handleDtsMayChangeOfGlobalScope(
566+
state: BuilderProgramState,
567+
filePath: Path,
568+
cancellationToken: CancellationToken | undefined,
569+
computeHash: BuilderState.ComputeHash,
570+
host: BuilderProgramHost,
571+
): boolean {
572+
if (!state.fileInfos.get(filePath)?.affectsGlobalScope) return false;
573+
// Every file needs to be handled
574+
BuilderState.getAllFilesExcludingDefaultLibraryFile(state, state.program!, /*firstSourceFile*/ undefined)
575+
.forEach(file => handleDtsMayChangeOf(
576+
state,
577+
file.resolvedPath,
578+
cancellationToken,
579+
computeHash,
580+
host,
581+
));
582+
removeDiagnosticsOfLibraryFiles(state);
583+
return true;
584+
}
585+
532586
/**
533-
* Iterate on referencing modules that export entities from affected file
587+
* Iterate on referencing modules that export entities from affected file and delete diagnostics and add pending emit
534588
*/
535-
function forEachReferencingModulesOfExportOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile, fn: (state: BuilderProgramState, filePath: Path) => void) {
589+
function handleDtsMayChangeOfReferencingExportOfAffectedFile(
590+
state: BuilderProgramState,
591+
affectedFile: SourceFile,
592+
cancellationToken: CancellationToken | undefined,
593+
computeHash: BuilderState.ComputeHash,
594+
host: BuilderProgramHost
595+
) {
536596
// If there was change in signature (dts output) for the changed file,
537597
// then only we need to handle pending file emit
538-
if (!state.exportedModulesMap || !state.changedFilesSet.has(affectedFile.resolvedPath)) {
539-
return;
540-
}
541-
598+
if (!state.exportedModulesMap || !state.changedFilesSet.has(affectedFile.resolvedPath)) return;
542599
if (!isChangedSignature(state, affectedFile.resolvedPath)) return;
543600

544601
// Since isolated modules dont change js files, files affected by change in signature is itself
@@ -551,7 +608,8 @@ namespace ts {
551608
const currentPath = queue.pop()!;
552609
if (!seenFileNamesMap.has(currentPath)) {
553610
seenFileNamesMap.set(currentPath, true);
554-
fn(state, currentPath);
611+
if (handleDtsMayChangeOfGlobalScope(state, currentPath, cancellationToken, computeHash, host)) return;
612+
handleDtsMayChangeOf(state, currentPath, cancellationToken, computeHash, host);
555613
if (isChangedSignature(state, currentPath)) {
556614
const currentSourceFile = Debug.checkDefined(state.program).getSourceFileByPath(currentPath)!;
557615
queue.push(...BuilderState.getReferencedByPaths(state, currentSourceFile.resolvedPath));
@@ -561,65 +619,69 @@ namespace ts {
561619
}
562620

563621
Debug.assert(!!state.currentAffectedFilesExportedModulesMap);
564-
565622
const seenFileAndExportsOfFile = new Set<string>();
566623
// Go through exported modules from cache first
567624
// If exported modules has path, all files referencing file exported from are affected
568-
state.currentAffectedFilesExportedModulesMap.getKeys(affectedFile.resolvedPath)?.forEach(exportedFromPath =>
569-
forEachFilesReferencingPath(state, exportedFromPath, seenFileAndExportsOfFile, fn)
570-
);
571-
572-
// If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected
573-
state.exportedModulesMap.getKeys(affectedFile.resolvedPath)?.forEach(exportedFromPath =>
574-
// If the cache had an updated value, skip
575-
!state.currentAffectedFilesExportedModulesMap!.hasKey(exportedFromPath) &&
576-
!state.currentAffectedFilesExportedModulesMap!.deletedKeys()?.has(exportedFromPath) &&
577-
forEachFilesReferencingPath(state, exportedFromPath, seenFileAndExportsOfFile, fn)
578-
);
579-
}
580-
581-
/**
582-
* Iterate on files referencing referencedPath
583-
*/
584-
function forEachFilesReferencingPath(state: BuilderProgramState, referencedPath: Path, seenFileAndExportsOfFile: Set<string>, fn: (state: BuilderProgramState, filePath: Path) => void): void {
585-
state.referencedMap!.getKeys(referencedPath)?.forEach(filePath =>
586-
forEachFileAndExportsOfFile(state, filePath, seenFileAndExportsOfFile, fn)
587-
);
625+
forEachKeyOfExportedModulesMap(state, affectedFile.resolvedPath, exportedFromPath => {
626+
if (handleDtsMayChangeOfGlobalScope(state, exportedFromPath, cancellationToken, computeHash, host)) return true;
627+
const references = state.referencedMap!.getKeys(exportedFromPath);
628+
return references && forEachKey(references, filePath =>
629+
handleDtsMayChangeOfFileAndExportsOfFile(
630+
state,
631+
filePath,
632+
seenFileAndExportsOfFile,
633+
cancellationToken,
634+
computeHash,
635+
host,
636+
)
637+
);
638+
});
588639
}
589640

590641
/**
591-
* fn on file and iterate on anything that exports this file
642+
* handle dts and semantic diagnostics on file and iterate on anything that exports this file
643+
* return true when all work is done and we can exit handling dts emit and semantic diagnostics
592644
*/
593-
function forEachFileAndExportsOfFile(state: BuilderProgramState, filePath: Path, seenFileAndExportsOfFile: Set<string>, fn: (state: BuilderProgramState, filePath: Path) => void): void {
594-
if (!tryAddToSet(seenFileAndExportsOfFile, filePath)) {
595-
return;
596-
}
597-
598-
fn(state, filePath);
599-
645+
function handleDtsMayChangeOfFileAndExportsOfFile(
646+
state: BuilderProgramState,
647+
filePath: Path,
648+
seenFileAndExportsOfFile: Set<string>,
649+
cancellationToken: CancellationToken | undefined,
650+
computeHash: BuilderState.ComputeHash,
651+
host: BuilderProgramHost,
652+
): boolean | undefined {
653+
if (!tryAddToSet(seenFileAndExportsOfFile, filePath)) return undefined;
654+
655+
if (handleDtsMayChangeOfGlobalScope(state, filePath, cancellationToken, computeHash, host)) return true;
656+
handleDtsMayChangeOf(state, filePath, cancellationToken, computeHash, host);
600657
Debug.assert(!!state.currentAffectedFilesExportedModulesMap);
601-
// Go through exported modules from cache first
602-
// If exported modules has path, all files referencing file exported from are affected
603-
state.currentAffectedFilesExportedModulesMap.getKeys(filePath)?.forEach(exportedFromPath =>
604-
forEachFileAndExportsOfFile(state, exportedFromPath, seenFileAndExportsOfFile, fn)
605-
);
606658

607-
// If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected
608-
state.exportedModulesMap!.getKeys(filePath)?.forEach(exportedFromPath =>
609-
// If the cache had an updated value, skip
610-
!state.currentAffectedFilesExportedModulesMap!.hasKey(exportedFromPath) &&
611-
!state.currentAffectedFilesExportedModulesMap!.deletedKeys()?.has(exportedFromPath) &&
612-
forEachFileAndExportsOfFile(state, exportedFromPath, seenFileAndExportsOfFile, fn)
659+
// If exported modules has path, all files referencing file exported from are affected
660+
forEachKeyOfExportedModulesMap(state, filePath, exportedFromPath =>
661+
handleDtsMayChangeOfFileAndExportsOfFile(
662+
state,
663+
exportedFromPath,
664+
seenFileAndExportsOfFile,
665+
cancellationToken,
666+
computeHash,
667+
host,
668+
)
613669
);
614670

615671
// Remove diagnostics of files that import this file (without going to exports of referencing files)
616672
state.referencedMap!.getKeys(filePath)?.forEach(referencingFilePath =>
617673
!seenFileAndExportsOfFile.has(referencingFilePath) && // Not already removed diagnostic file
618-
fn(state, referencingFilePath) // Dont add to seen since this is not yet done with the export removal
674+
handleDtsMayChangeOf( // Dont add to seen since this is not yet done with the export removal
675+
state,
676+
referencingFilePath,
677+
cancellationToken,
678+
computeHash,
679+
host,
680+
)
619681
);
682+
return undefined;
620683
}
621684

622-
623685
/**
624686
* This is called after completing operation on the next affected file.
625687
* The operations here are postponed to ensure that cancellation during the iteration is handled correctly

src/testRunner/unittests/tsc/incremental.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,5 +464,40 @@ declare global {
464464
fs.writeFileSync("/lib/lib.esnext.d.ts", libContent);
465465
}
466466
});
467+
468+
verifyTscWithEdits({
469+
scenario: "incremental",
470+
subScenario: "change to type that gets used as global through export in another file",
471+
commandLineArgs: ["-p", `src/project`],
472+
fs: () => loadProjectFromFiles({
473+
"/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, }),
474+
"/src/project/class1.ts": `const a: MagicNumber = 1;
475+
console.log(a);`,
476+
"/src/project/constants.ts": "export default 1;",
477+
"/src/project/types.d.ts": `type MagicNumber = typeof import('./constants').default`,
478+
}),
479+
edits: [{
480+
subScenario: "Modify imports used in global file",
481+
modifyFs: fs => fs.writeFileSync("/src/project/constants.ts", "export default 2;"),
482+
}],
483+
});
484+
485+
verifyTscWithEdits({
486+
scenario: "incremental",
487+
subScenario: "change to type that gets used as global through export in another file through indirect import",
488+
commandLineArgs: ["-p", `src/project`],
489+
fs: () => loadProjectFromFiles({
490+
"/src/project/tsconfig.json": JSON.stringify({ compilerOptions: { composite: true }, }),
491+
"/src/project/class1.ts": `const a: MagicNumber = 1;
492+
console.log(a);`,
493+
"/src/project/constants.ts": "export default 1;",
494+
"/src/project/reexport.ts": `export { default as ConstantNumber } from "./constants"`,
495+
"/src/project/types.d.ts": `type MagicNumber = typeof import('./reexport').ConstantNumber`,
496+
}),
497+
edits: [{
498+
subScenario: "Modify imports used in global file",
499+
modifyFs: fs => fs.writeFileSync("/src/project/constants.ts", "export default 2;"),
500+
}],
501+
});
467502
});
468503
}

0 commit comments

Comments
 (0)