Skip to content

Commit 585acb1

Browse files
committed
Use exported modules through declaration emit to invalidate the semantic diagnostics
Fixes #24986
1 parent 0774fdc commit 585acb1

File tree

3 files changed

+153
-14
lines changed

3 files changed

+153
-14
lines changed

src/compiler/builder.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ namespace ts {
3030
* These will be commited whenever the iteration through affected files of current changed file is complete
3131
*/
3232
currentAffectedFilesSignatures: Map<string> | undefined;
33+
/**
34+
* Newly computed visible to outside referencedSet
35+
*/
36+
currentAffectedFilesExportedModulesMap: BuilderState.ComputingExportedModulesMap | undefined;
3337
/**
3438
* Already seen affected files
3539
*/
@@ -128,6 +132,8 @@ namespace ts {
128132
// Set the next affected file as seen and remove the cached semantic diagnostics
129133
state.affectedFilesIndex = affectedFilesIndex;
130134
semanticDiagnosticsPerFile!.delete(affectedFile.path);
135+
// Remove semantic diagnostics for files that are affected by using exports of this module
136+
BuilderState.getFilesAffectedByExportedModule(state, affectedFile.path, state.currentAffectedFilesExportedModulesMap).forEach(path => semanticDiagnosticsPerFile!.delete(path));
131137
return affectedFile;
132138
}
133139
seenAffectedFiles!.set(affectedFile.path, true);
@@ -140,6 +146,7 @@ namespace ts {
140146
// Commit the changes in file signature
141147
BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures!);
142148
state.currentAffectedFilesSignatures!.clear();
149+
BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap);
143150
state.affectedFiles = undefined;
144151
}
145152

@@ -160,7 +167,10 @@ namespace ts {
160167

161168
// Get next batch of affected files
162169
state.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures || createMap();
163-
state.affectedFiles = BuilderState.getFilesAffectedBy(state, state.program, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures);
170+
if (state.exportedModulesMap) {
171+
state.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap || createMap<BuilderState.ReferencedSet | false>();
172+
}
173+
state.affectedFiles = BuilderState.getFilesAffectedBy(state, state.program, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures, state.currentAffectedFilesExportedModulesMap);
164174
state.currentChangedFilePath = nextKey.value as Path;
165175
state.semanticDiagnosticsPerFile!.delete(nextKey.value as Path);
166176
state.affectedFilesIndex = 0;

src/compiler/builderState.ts

Lines changed: 139 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace ts {
22
export interface EmitOutput {
33
outputFiles: OutputFile[];
44
emitSkipped: boolean;
5+
/* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit;
56
}
67

78
export interface OutputFile {
@@ -17,7 +18,7 @@ namespace ts {
1718
cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput {
1819
const outputFiles: OutputFile[] = [];
1920
const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
20-
return { outputFiles, emitSkipped: emitResult.emitSkipped };
21+
return { outputFiles, emitSkipped: emitResult.emitSkipped, exportedModulesFromDeclarationEmit: emitResult.exportedModulesFromDeclarationEmit };
2122

2223
function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) {
2324
outputFiles.push({ name: fileName, writeByteOrderMark, text });
@@ -35,6 +36,11 @@ namespace ts {
3536
* Thus non undefined value indicates, module emit
3637
*/
3738
readonly referencedMap: ReadonlyMap<BuilderState.ReferencedSet> | undefined;
39+
/**
40+
* Contains the map of exported modules ReferencedSet=exorted module files from the file if module emit is enabled
41+
* Otherwise undefined
42+
*/
43+
readonly exportedModulesMap: Map<BuilderState.ReferencedSet> | undefined;
3844
/**
3945
* Map of files that have already called update signature.
4046
* That means hence forth these files are assumed to have
@@ -70,6 +76,30 @@ namespace ts.BuilderState {
7076
*/
7177
export type ComputeHash = (data: string) => string;
7278

79+
/**
80+
* Exported modules to from declaration emit being computed.
81+
* This can contain false in the affected file path to specify that there are no exported module(types from other modules) for this file
82+
*/
83+
export type ComputingExportedModulesMap = Map<ReferencedSet | false>;
84+
85+
/**
86+
* Get the referencedFile from the imported module symbol
87+
*/
88+
function getReferencedFileFromImportedModuleSymbol(symbol: Symbol) {
89+
if (symbol.declarations && symbol.declarations[0]) {
90+
const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]);
91+
return declarationSourceFile && declarationSourceFile.path;
92+
}
93+
}
94+
95+
/**
96+
* Get the referencedFile from the import name node from file
97+
*/
98+
function getReferencedFileFromImportLiteral(checker: TypeChecker, importName: StringLiteralLike) {
99+
const symbol = checker.getSymbolAtLocation(importName);
100+
return symbol && getReferencedFileFromImportedModuleSymbol(symbol);
101+
}
102+
73103
/**
74104
* Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
75105
*/
@@ -82,12 +112,9 @@ namespace ts.BuilderState {
82112
if (sourceFile.imports && sourceFile.imports.length > 0) {
83113
const checker: TypeChecker = program.getTypeChecker();
84114
for (const importName of sourceFile.imports) {
85-
const symbol = checker.getSymbolAtLocation(importName);
86-
if (symbol && symbol.declarations && symbol.declarations[0]) {
87-
const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]);
88-
if (declarationSourceFile) {
89-
addReferencedFile(declarationSourceFile.path);
90-
}
115+
const declarationSourceFilePath = getReferencedFileFromImportLiteral(checker, importName);
116+
if (declarationSourceFilePath) {
117+
addReferencedFile(declarationSourceFilePath);
91118
}
92119
}
93120
}
@@ -137,6 +164,7 @@ namespace ts.BuilderState {
137164
export function create(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly<BuilderState>): BuilderState {
138165
const fileInfos = createMap<FileInfo>();
139166
const referencedMap = newProgram.getCompilerOptions().module !== ModuleKind.None ? createMap<ReferencedSet>() : undefined;
167+
const exportedModulesMap = referencedMap ? createMap<ReferencedSet>() : undefined;
140168
const hasCalledUpdateShapeSignature = createMap<true>();
141169
const useOldState = canReuseOldState(referencedMap, oldState);
142170

@@ -149,13 +177,21 @@ namespace ts.BuilderState {
149177
if (newReferences) {
150178
referencedMap.set(sourceFile.path, newReferences);
151179
}
180+
// Copy old visible to outside files map
181+
if (useOldState) {
182+
const exportedModules = oldState!.exportedModulesMap!.get(sourceFile.path);
183+
if (exportedModules) {
184+
exportedModulesMap!.set(sourceFile.path, exportedModules);
185+
}
186+
}
152187
}
153188
fileInfos.set(sourceFile.path, { version, signature: oldInfo && oldInfo.signature });
154189
}
155190

156191
return {
157192
fileInfos,
158193
referencedMap,
194+
exportedModulesMap,
159195
hasCalledUpdateShapeSignature,
160196
allFilesExcludingDefaultLibraryFile: undefined,
161197
allFileNames: undefined
@@ -165,7 +201,7 @@ namespace ts.BuilderState {
165201
/**
166202
* Gets the files affected by the path from the program
167203
*/
168-
export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: Map<string>): ReadonlyArray<SourceFile> {
204+
export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: Map<string>, exportedModulesMapCache?: ComputingExportedModulesMap): ReadonlyArray<SourceFile> {
169205
// Since the operation could be cancelled, the signatures are always stored in the cache
170206
// They will be commited once it is safe to use them
171207
// eg when calling this api from tsserver, if there is no cancellation of the operation
@@ -176,11 +212,11 @@ namespace ts.BuilderState {
176212
return emptyArray;
177213
}
178214

179-
if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash)) {
215+
if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache)) {
180216
return [sourceFile];
181217
}
182218

183-
const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash);
219+
const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache);
184220
if (!cacheToUpdateSignature) {
185221
// Commit all the signatures in the signature cache
186222
updateSignaturesFromCache(state, signatureCache);
@@ -202,8 +238,9 @@ namespace ts.BuilderState {
202238
/**
203239
* Returns if the shape of the signature has changed since last emit
204240
*/
205-
function updateShapeSignature(state: Readonly<BuilderState>, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash) {
241+
function updateShapeSignature(state: Readonly<BuilderState>, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache?: ComputingExportedModulesMap) {
206242
Debug.assert(!!sourceFile);
243+
Debug.assert(!exportedModulesMapCache || !!state.exportedModulesMap, "Compute visible to outside map only if visibleToOutsideReferencedMap present in the state");
207244

208245
// If we have cached the result for this file, that means hence forth we should assume file shape is uptodate
209246
if (state.hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) {
@@ -222,16 +259,105 @@ namespace ts.BuilderState {
222259
const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken);
223260
if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
224261
latestSignature = computeHash(emitOutput.outputFiles[0].text);
262+
if (exportedModulesMapCache && latestSignature !== prevSignature) {
263+
updateExportedModules(programOfThisState, sourceFile, emitOutput.exportedModulesFromDeclarationEmit, exportedModulesMapCache);
264+
}
225265
}
226266
else {
227267
latestSignature = prevSignature!; // TODO: GH#18217
228268
}
269+
229270
}
230271
cacheToUpdateSignature.set(sourceFile.path, latestSignature);
231272

232273
return !prevSignature || latestSignature !== prevSignature;
233274
}
234275

276+
/**
277+
* Coverts the declaration emit result into exported modules map
278+
*/
279+
function updateExportedModules(programOfThisState: Program, sourceFile: SourceFile, exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined, exportedModulesMapCache: ComputingExportedModulesMap) {
280+
if (!exportedModulesFromDeclarationEmit) {
281+
exportedModulesMapCache.set(sourceFile.path, false);
282+
return;
283+
}
284+
285+
const checker = programOfThisState.getTypeChecker();
286+
let exportedModules: Map<true> | undefined;
287+
288+
exportedModulesFromDeclarationEmit.exportedModuleSpecifiers.forEach(importName =>
289+
addExportedModule(getReferencedFileFromImportLiteral(checker, importName)));
290+
exportedModulesFromDeclarationEmit.exportedModuleSymbolsUsingImportTypeNodes.forEach(symbol =>
291+
addExportedModule(getReferencedFileFromImportedModuleSymbol(symbol)));
292+
293+
exportedModulesMapCache.set(sourceFile.path, exportedModules || false);
294+
295+
function addExportedModule(exportedModulePath: Path | undefined) {
296+
if (exportedModulePath) {
297+
if (!exportedModules) {
298+
exportedModules = createMap<true>();
299+
}
300+
exportedModules.set(exportedModulePath, true);
301+
}
302+
}
303+
}
304+
305+
/**
306+
* Updates the exported modules from cache into state's exported modules map
307+
* This should be called whenever it is safe to commit the state of the builder
308+
*/
309+
export function updateExportedFilesMapFromCache(state: BuilderState, exportedModulesMapCache: ComputingExportedModulesMap | undefined) {
310+
if (exportedModulesMapCache) {
311+
Debug.assert(!!state.exportedModulesMap);
312+
exportedModulesMapCache.forEach((exportedModules, path) => {
313+
if (exportedModules) {
314+
state.exportedModulesMap!.set(path, exportedModules);
315+
}
316+
else {
317+
state.exportedModulesMap!.delete(path);
318+
}
319+
});
320+
}
321+
}
322+
323+
/**
324+
* Gets the files affected by exported module
325+
*/
326+
export function getFilesAffectedByExportedModule(state: BuilderState, path: Path, exportedModulesMapCache?: ComputingExportedModulesMap): ReadonlyArray<Path> {
327+
if (!state.exportedModulesMap) {
328+
return emptyArray;
329+
}
330+
331+
Debug.assert(!!exportedModulesMapCache);
332+
let affectedFiles: Map<true> | undefined;
333+
// Go through exported modules from cache first
334+
exportedModulesMapCache!.forEach((exportedModules, exportedFromPath) => {
335+
// If exported modules has path, all files referencing file exported from are affected
336+
if (exportedModules && exportedModules.has(path)) {
337+
addFilesReferencing(exportedFromPath as Path);
338+
}
339+
});
340+
state.exportedModulesMap.forEach((exportedModules, exportedFromPath) => {
341+
// If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected
342+
if (!exportedModulesMapCache!.has(exportedFromPath) && exportedModules.has(path)) {
343+
addFilesReferencing(exportedFromPath as Path);
344+
}
345+
});
346+
347+
return affectedFiles ? arrayFrom(affectedFiles.keys()) as Path[] : emptyArray;
348+
349+
function addFilesReferencing(referencingFilePath: Path) {
350+
state.referencedMap!.forEach((referencesInFile, filePath) => {
351+
if (referencesInFile.has(referencingFilePath)) {
352+
if (!affectedFiles) {
353+
affectedFiles = createMap<true>();
354+
}
355+
affectedFiles.set(filePath, true);
356+
}
357+
});
358+
}
359+
}
360+
235361
/**
236362
* Get all the dependencies of the sourceFile
237363
*/
@@ -347,7 +473,7 @@ namespace ts.BuilderState {
347473
/**
348474
* When program emits modular code, gets the files affected by the sourceFile whose shape has changed
349475
*/
350-
function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined) {
476+
function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map<string>, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined, exportedModulesMapCache: ComputingExportedModulesMap | undefined) {
351477
if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) {
352478
return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape);
353479
}
@@ -370,7 +496,7 @@ namespace ts.BuilderState {
370496
if (!seenFileNamesMap.has(currentPath)) {
371497
const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath)!;
372498
seenFileNamesMap.set(currentPath, currentSourceFile);
373-
if (currentSourceFile && updateShapeSignature(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash!)) { // TODO: GH#18217
499+
if (currentSourceFile && updateShapeSignature(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash!, exportedModulesMapCache)) { // TODO: GH#18217
374500
queue.push(...getReferencedByPaths(state, currentPath));
375501
}
376502
}

src/testRunner/unittests/tscWatchMode.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,11 +1304,14 @@ export class B
13041304
const watch = createWatchOfConfigFile("tsconfig.json", host);
13051305
checkProgramActualFiles(watch(), [aFile.path, bFile.path, cFile.path, libFile.path]);
13061306
checkOutputErrorsInitial(host, emptyArray);
1307+
const modifiedTimeOfAJs = host.getModifiedTime(`${currentDirectory}/a.js`);
13071308
host.writeFile(cFile.path, cFile.content.replace("d", "d2"));
13081309
host.runQueuedTimeoutCallbacks();
13091310
checkOutputErrorsIncremental(host, [
13101311
getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.lastIndexOf("d"), 1, Diagnostics.Property_0_does_not_exist_on_type_1, "d", "C")
13111312
]);
1313+
// File a need not be rewritten
1314+
assert.equal(host.getModifiedTime(`${currentDirectory}/a.js`), modifiedTimeOfAJs);
13121315
});
13131316
});
13141317

0 commit comments

Comments
 (0)