From 585acb19901dea9bf3aaafb108a905bb236f425f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 11 Jul 2018 15:49:11 -0700 Subject: [PATCH] Use exported modules through declaration emit to invalidate the semantic diagnostics Fixes #24986 --- src/compiler/builder.ts | 12 +- src/compiler/builderState.ts | 152 +++++++++++++++++++++-- src/testRunner/unittests/tscWatchMode.ts | 3 + 3 files changed, 153 insertions(+), 14 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index fc52559a318..bbbdb043616 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -30,6 +30,10 @@ namespace ts { * These will be commited whenever the iteration through affected files of current changed file is complete */ currentAffectedFilesSignatures: Map | undefined; + /** + * Newly computed visible to outside referencedSet + */ + currentAffectedFilesExportedModulesMap: BuilderState.ComputingExportedModulesMap | undefined; /** * Already seen affected files */ @@ -128,6 +132,8 @@ namespace ts { // Set the next affected file as seen and remove the cached semantic diagnostics state.affectedFilesIndex = affectedFilesIndex; semanticDiagnosticsPerFile!.delete(affectedFile.path); + // Remove semantic diagnostics for files that are affected by using exports of this module + BuilderState.getFilesAffectedByExportedModule(state, affectedFile.path, state.currentAffectedFilesExportedModulesMap).forEach(path => semanticDiagnosticsPerFile!.delete(path)); return affectedFile; } seenAffectedFiles!.set(affectedFile.path, true); @@ -140,6 +146,7 @@ namespace ts { // Commit the changes in file signature BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures!); state.currentAffectedFilesSignatures!.clear(); + BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); state.affectedFiles = undefined; } @@ -160,7 +167,10 @@ namespace ts { // Get next batch of affected files state.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures || createMap(); - state.affectedFiles = BuilderState.getFilesAffectedBy(state, state.program, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures); + if (state.exportedModulesMap) { + state.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap || createMap(); + } + state.affectedFiles = BuilderState.getFilesAffectedBy(state, state.program, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures, state.currentAffectedFilesExportedModulesMap); state.currentChangedFilePath = nextKey.value as Path; state.semanticDiagnosticsPerFile!.delete(nextKey.value as Path); state.affectedFilesIndex = 0; diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts index ed41ef4a8d3..9afc9575552 100644 --- a/src/compiler/builderState.ts +++ b/src/compiler/builderState.ts @@ -2,6 +2,7 @@ namespace ts { export interface EmitOutput { outputFiles: OutputFile[]; emitSkipped: boolean; + /* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit; } export interface OutputFile { @@ -17,7 +18,7 @@ namespace ts { cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { const outputFiles: OutputFile[] = []; const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); - return { outputFiles, emitSkipped: emitResult.emitSkipped }; + return { outputFiles, emitSkipped: emitResult.emitSkipped, exportedModulesFromDeclarationEmit: emitResult.exportedModulesFromDeclarationEmit }; function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { outputFiles.push({ name: fileName, writeByteOrderMark, text }); @@ -35,6 +36,11 @@ namespace ts { * Thus non undefined value indicates, module emit */ readonly referencedMap: ReadonlyMap | undefined; + /** + * Contains the map of exported modules ReferencedSet=exorted module files from the file if module emit is enabled + * Otherwise undefined + */ + readonly exportedModulesMap: Map | undefined; /** * Map of files that have already called update signature. * That means hence forth these files are assumed to have @@ -70,6 +76,30 @@ namespace ts.BuilderState { */ export type ComputeHash = (data: string) => string; + /** + * Exported modules to from declaration emit being computed. + * This can contain false in the affected file path to specify that there are no exported module(types from other modules) for this file + */ + export type ComputingExportedModulesMap = Map; + + /** + * Get the referencedFile from the imported module symbol + */ + function getReferencedFileFromImportedModuleSymbol(symbol: Symbol) { + if (symbol.declarations && symbol.declarations[0]) { + const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]); + return declarationSourceFile && declarationSourceFile.path; + } + } + + /** + * Get the referencedFile from the import name node from file + */ + function getReferencedFileFromImportLiteral(checker: TypeChecker, importName: StringLiteralLike) { + const symbol = checker.getSymbolAtLocation(importName); + return symbol && getReferencedFileFromImportedModuleSymbol(symbol); + } + /** * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true */ @@ -82,12 +112,9 @@ namespace ts.BuilderState { if (sourceFile.imports && sourceFile.imports.length > 0) { const checker: TypeChecker = program.getTypeChecker(); for (const importName of sourceFile.imports) { - const symbol = checker.getSymbolAtLocation(importName); - if (symbol && symbol.declarations && symbol.declarations[0]) { - const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]); - if (declarationSourceFile) { - addReferencedFile(declarationSourceFile.path); - } + const declarationSourceFilePath = getReferencedFileFromImportLiteral(checker, importName); + if (declarationSourceFilePath) { + addReferencedFile(declarationSourceFilePath); } } } @@ -137,6 +164,7 @@ namespace ts.BuilderState { export function create(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderState { const fileInfos = createMap(); const referencedMap = newProgram.getCompilerOptions().module !== ModuleKind.None ? createMap() : undefined; + const exportedModulesMap = referencedMap ? createMap() : undefined; const hasCalledUpdateShapeSignature = createMap(); const useOldState = canReuseOldState(referencedMap, oldState); @@ -149,6 +177,13 @@ namespace ts.BuilderState { if (newReferences) { referencedMap.set(sourceFile.path, newReferences); } + // Copy old visible to outside files map + if (useOldState) { + const exportedModules = oldState!.exportedModulesMap!.get(sourceFile.path); + if (exportedModules) { + exportedModulesMap!.set(sourceFile.path, exportedModules); + } + } } fileInfos.set(sourceFile.path, { version, signature: oldInfo && oldInfo.signature }); } @@ -156,6 +191,7 @@ namespace ts.BuilderState { return { fileInfos, referencedMap, + exportedModulesMap, hasCalledUpdateShapeSignature, allFilesExcludingDefaultLibraryFile: undefined, allFileNames: undefined @@ -165,7 +201,7 @@ namespace ts.BuilderState { /** * Gets the files affected by the path from the program */ - export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: Map): ReadonlyArray { + export function getFilesAffectedBy(state: BuilderState, programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, cacheToUpdateSignature?: Map, exportedModulesMapCache?: ComputingExportedModulesMap): ReadonlyArray { // Since the operation could be cancelled, the signatures are always stored in the cache // They will be commited once it is safe to use them // eg when calling this api from tsserver, if there is no cancellation of the operation @@ -176,11 +212,11 @@ namespace ts.BuilderState { return emptyArray; } - if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash)) { + if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache)) { return [sourceFile]; } - const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash); + const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash, exportedModulesMapCache); if (!cacheToUpdateSignature) { // Commit all the signatures in the signature cache updateSignaturesFromCache(state, signatureCache); @@ -202,8 +238,9 @@ namespace ts.BuilderState { /** * Returns if the shape of the signature has changed since last emit */ - function updateShapeSignature(state: Readonly, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash) { + function updateShapeSignature(state: Readonly, programOfThisState: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash, exportedModulesMapCache?: ComputingExportedModulesMap) { Debug.assert(!!sourceFile); + Debug.assert(!exportedModulesMapCache || !!state.exportedModulesMap, "Compute visible to outside map only if visibleToOutsideReferencedMap present in the state"); // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate if (state.hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) { @@ -222,16 +259,105 @@ namespace ts.BuilderState { const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { latestSignature = computeHash(emitOutput.outputFiles[0].text); + if (exportedModulesMapCache && latestSignature !== prevSignature) { + updateExportedModules(programOfThisState, sourceFile, emitOutput.exportedModulesFromDeclarationEmit, exportedModulesMapCache); + } } else { latestSignature = prevSignature!; // TODO: GH#18217 } + } cacheToUpdateSignature.set(sourceFile.path, latestSignature); return !prevSignature || latestSignature !== prevSignature; } + /** + * Coverts the declaration emit result into exported modules map + */ + function updateExportedModules(programOfThisState: Program, sourceFile: SourceFile, exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined, exportedModulesMapCache: ComputingExportedModulesMap) { + if (!exportedModulesFromDeclarationEmit) { + exportedModulesMapCache.set(sourceFile.path, false); + return; + } + + const checker = programOfThisState.getTypeChecker(); + let exportedModules: Map | undefined; + + exportedModulesFromDeclarationEmit.exportedModuleSpecifiers.forEach(importName => + addExportedModule(getReferencedFileFromImportLiteral(checker, importName))); + exportedModulesFromDeclarationEmit.exportedModuleSymbolsUsingImportTypeNodes.forEach(symbol => + addExportedModule(getReferencedFileFromImportedModuleSymbol(symbol))); + + exportedModulesMapCache.set(sourceFile.path, exportedModules || false); + + function addExportedModule(exportedModulePath: Path | undefined) { + if (exportedModulePath) { + if (!exportedModules) { + exportedModules = createMap(); + } + exportedModules.set(exportedModulePath, true); + } + } + } + + /** + * Updates the exported modules from cache into state's exported modules map + * This should be called whenever it is safe to commit the state of the builder + */ + export function updateExportedFilesMapFromCache(state: BuilderState, exportedModulesMapCache: ComputingExportedModulesMap | undefined) { + if (exportedModulesMapCache) { + Debug.assert(!!state.exportedModulesMap); + exportedModulesMapCache.forEach((exportedModules, path) => { + if (exportedModules) { + state.exportedModulesMap!.set(path, exportedModules); + } + else { + state.exportedModulesMap!.delete(path); + } + }); + } + } + + /** + * Gets the files affected by exported module + */ + export function getFilesAffectedByExportedModule(state: BuilderState, path: Path, exportedModulesMapCache?: ComputingExportedModulesMap): ReadonlyArray { + if (!state.exportedModulesMap) { + return emptyArray; + } + + Debug.assert(!!exportedModulesMapCache); + let affectedFiles: Map | undefined; + // Go through exported modules from cache first + exportedModulesMapCache!.forEach((exportedModules, exportedFromPath) => { + // If exported modules has path, all files referencing file exported from are affected + if (exportedModules && exportedModules.has(path)) { + addFilesReferencing(exportedFromPath as Path); + } + }); + state.exportedModulesMap.forEach((exportedModules, exportedFromPath) => { + // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected + if (!exportedModulesMapCache!.has(exportedFromPath) && exportedModules.has(path)) { + addFilesReferencing(exportedFromPath as Path); + } + }); + + return affectedFiles ? arrayFrom(affectedFiles.keys()) as Path[] : emptyArray; + + function addFilesReferencing(referencingFilePath: Path) { + state.referencedMap!.forEach((referencesInFile, filePath) => { + if (referencesInFile.has(referencingFilePath)) { + if (!affectedFiles) { + affectedFiles = createMap(); + } + affectedFiles.set(filePath, true); + } + }); + } + } + /** * Get all the dependencies of the sourceFile */ @@ -347,7 +473,7 @@ namespace ts.BuilderState { /** * When program emits modular code, gets the files affected by the sourceFile whose shape has changed */ - function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined) { + function getFilesAffectedByUpdatedShapeWhenModuleEmit(state: BuilderState, programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined, computeHash: ComputeHash | undefined, exportedModulesMapCache: ComputingExportedModulesMap | undefined) { if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) { return getAllFilesExcludingDefaultLibraryFile(state, programOfThisState, sourceFileWithUpdatedShape); } @@ -370,7 +496,7 @@ namespace ts.BuilderState { if (!seenFileNamesMap.has(currentPath)) { const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath)!; seenFileNamesMap.set(currentPath, currentSourceFile); - if (currentSourceFile && updateShapeSignature(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash!)) { // TODO: GH#18217 + if (currentSourceFile && updateShapeSignature(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash!, exportedModulesMapCache)) { // TODO: GH#18217 queue.push(...getReferencedByPaths(state, currentPath)); } } diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index f962d736465..797fcbc2a79 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -1304,11 +1304,14 @@ export class B const watch = createWatchOfConfigFile("tsconfig.json", host); checkProgramActualFiles(watch(), [aFile.path, bFile.path, cFile.path, libFile.path]); checkOutputErrorsInitial(host, emptyArray); + const modifiedTimeOfAJs = host.getModifiedTime(`${currentDirectory}/a.js`); host.writeFile(cFile.path, cFile.content.replace("d", "d2")); host.runQueuedTimeoutCallbacks(); checkOutputErrorsIncremental(host, [ getDiagnosticOfFileFromProgram(watch(), aFile.path, aFile.content.lastIndexOf("d"), 1, Diagnostics.Property_0_does_not_exist_on_type_1, "d", "C") ]); + // File a need not be rewritten + assert.equal(host.getModifiedTime(`${currentDirectory}/a.js`), modifiedTimeOfAJs); }); });