Skip to content

Commit 7336aad

Browse files
sheetalkamatJack-Works
authored andcommitted
If we are updating dts of any of the file and it affects global scope, everything needs update in signature and dts emit (microsoft#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 microsoft#42769
1 parent ab3b82c commit 7336aad

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)