diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index fc52559a318da..e2d59975d042a 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -30,10 +30,18 @@ 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 */ seenAffectedFiles: Map | undefined; + /** + * True if the semantic diagnostics were copied from the old state + */ + semanticDiagnosticsFromOldState?: Map; /** * program corresponding to this state */ @@ -96,6 +104,10 @@ namespace ts { const diagnostics = oldState!.semanticDiagnosticsPerFile!.get(sourceFilePath); if (diagnostics) { state.semanticDiagnosticsPerFile!.set(sourceFilePath, diagnostics); + if (!state.semanticDiagnosticsFromOldState) { + state.semanticDiagnosticsFromOldState = createMap(); + } + state.semanticDiagnosticsFromOldState.set(sourceFilePath, true); } } }); @@ -120,17 +132,17 @@ namespace ts { while (true) { const { affectedFiles } = state; if (affectedFiles) { - const { seenAffectedFiles, semanticDiagnosticsPerFile } = state; + const seenAffectedFiles = state.seenAffectedFiles!; let affectedFilesIndex = state.affectedFilesIndex!; // TODO: GH#18217 while (affectedFilesIndex < affectedFiles.length) { const affectedFile = affectedFiles[affectedFilesIndex]; - if (!seenAffectedFiles!.has(affectedFile.path)) { + if (!seenAffectedFiles.has(affectedFile.path)) { // Set the next affected file as seen and remove the cached semantic diagnostics state.affectedFilesIndex = affectedFilesIndex; - semanticDiagnosticsPerFile!.delete(affectedFile.path); + cleanSemanticDiagnosticsOfAffectedFile(state, affectedFile); return affectedFile; } - seenAffectedFiles!.set(affectedFile.path, true); + seenAffectedFiles.set(affectedFile.path, true); affectedFilesIndex++; } @@ -140,6 +152,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,14 +173,74 @@ 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; state.seenAffectedFiles = state.seenAffectedFiles || createMap(); } } + /** + * Remove the semantic diagnostics cached from old state for affected File and the files that are referencing modules that export entities from affected file + */ + function cleanSemanticDiagnosticsOfAffectedFile(state: BuilderProgramState, affectedFile: SourceFile) { + if (removeSemanticDiagnosticsOf(state, affectedFile.path)) { + // If there are no more diagnostics from old cache, done + return; + } + + // If there was change in signature for the changed file, + // then delete the semantic diagnostics for files that are affected by using exports of this module + + if (!state.exportedModulesMap || state.affectedFiles!.length === 1 || !state.changedFilesSet.has(affectedFile.path)) { + return; + } + + Debug.assert(!!state.currentAffectedFilesExportedModulesMap); + // Go through exported modules from cache first + // If exported modules has path, all files referencing file exported from are affected + if (forEachEntry(state.currentAffectedFilesExportedModulesMap!, (exportedModules, exportedFromPath) => + exportedModules && + exportedModules.has(affectedFile.path) && + removeSemanticDiagnosticsOfFilesReferencingPath(state, exportedFromPath as Path) + )) { + return; + } + + // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected + forEachEntry(state.exportedModulesMap, (exportedModules, exportedFromPath) => + !state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it + exportedModules.has(affectedFile.path) && + removeSemanticDiagnosticsOfFilesReferencingPath(state, exportedFromPath as Path) + ); + } + + /** + * removes the semantic diagnostics of files referencing referencedPath and + * returns true if there are no more semantic diagnostics from old state + */ + function removeSemanticDiagnosticsOfFilesReferencingPath(state: BuilderProgramState, referencedPath: Path) { + return forEachEntry(state.referencedMap!, (referencesInFile, filePath) => + referencesInFile.has(referencedPath) && removeSemanticDiagnosticsOf(state, filePath as Path) + ); + } + + /** + * Removes semantic diagnostics for path and + * returns true if there are no more semantic diagnostics from the old state + */ + function removeSemanticDiagnosticsOf(state: BuilderProgramState, path: Path) { + if (!state.semanticDiagnosticsFromOldState) { + return false; + } + state.semanticDiagnosticsFromOldState.delete(path); + state.semanticDiagnosticsPerFile!.delete(path); + return !state.semanticDiagnosticsFromOldState.size; + } + /** * This is called after completing operation on the next affected file. * The operations here are postponed to ensure that cancellation during the iteration is handled correctly diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts index ed41ef4a8d3fa..12936e5b2e860 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,61 @@ 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(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(sourceFile: SourceFile, exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined, exportedModulesMapCache: ComputingExportedModulesMap) { + if (!exportedModulesFromDeclarationEmit) { + exportedModulesMapCache.set(sourceFile.path, false); + return; + } + + let exportedModules: Map | undefined; + exportedModulesFromDeclarationEmit.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); + } + }); + } + } + /** * Get all the dependencies of the sourceFile */ @@ -347,7 +429,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 +452,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/compiler/checker.ts b/src/compiler/checker.ts index f4da8833c2070..3c307585a9d5f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3914,6 +3914,7 @@ namespace ts { const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context); const specifier = getSpecifierForModuleSymbol(chain[0], context); const lit = createLiteralTypeNode(createLiteral(specifier)); + if (context.tracker.trackExternalModuleSymbolOfImportTypeNode) context.tracker.trackExternalModuleSymbolOfImportTypeNode(chain[0]); context.approximateLength += specifier.length + 10; // specifier + import("") if (!nonRootParts || isEntityName(nonRootParts)) { if (nonRootParts) { @@ -28088,7 +28089,8 @@ namespace ts { setAccessor, getAccessor }; - } + }, + getSymbolOfExternalModuleSpecifier: moduleName => resolveExternalModuleNameWorker(moduleName, moduleName, /*moduleNotFoundError*/ undefined) }; function isInHeritageClause(node: PropertyAccessEntityNameExpression) { diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 3b3fac5c7a53c..badbfc4fa3fa6 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -113,6 +113,7 @@ namespace ts { let bundleInfo: BundleInfo = createDefaultBundleInfo(); let emitSkipped = false; + let exportedModulesFromDeclarationEmit: ExportedModulesFromDeclarationEmit | undefined; // Emit each output file performance.mark("beforePrint"); @@ -125,6 +126,7 @@ namespace ts { diagnostics: emitterDiagnostics.getDiagnostics(), emittedFiles: emittedFilesList, sourceMaps: sourceMapDataList, + exportedModulesFromDeclarationEmit }; function emitSourceFileOrBundle({ jsFilePath, sourceMapFilePath, declarationFilePath, declarationMapPath, bundleInfoPath }: EmitFileNames, sourceFileOrBundle: SourceFile | Bundle) { @@ -222,6 +224,10 @@ namespace ts { if (!declBlocked || emitOnlyDtsFiles) { Debug.assert(declarationTransform.transformed.length === 1, "Should only see one output from the decl transform"); printSourceFileOrBundle(declarationFilePath, declarationMapPath, declarationTransform.transformed[0], /* bundleInfopath*/ undefined, declarationPrinter, declarationSourceMap); + if (emitOnlyDtsFiles && declarationTransform.transformed[0].kind === SyntaxKind.SourceFile) { + const sourceFile = declarationTransform.transformed[0] as SourceFile; + exportedModulesFromDeclarationEmit = sourceFile.exportedModulesFromDeclarationEmit; + } } declarationTransform.dispose(); } diff --git a/src/compiler/transformers/declarations.ts b/src/compiler/transformers/declarations.ts index fdf730489ce76..2fa093128bc30 100644 --- a/src/compiler/transformers/declarations.ts +++ b/src/compiler/transformers/declarations.ts @@ -37,6 +37,7 @@ namespace ts { let lateMarkedStatements: LateVisibilityPaintedStatement[] | undefined; let lateStatementReplacementMap: Map>; let suppressNewDiagnosticContexts: boolean; + let exportedModulesFromDeclarationEmit: Symbol[] | undefined; const host = context.getEmitHost(); const symbolTracker: SymbolTracker = { @@ -46,6 +47,7 @@ namespace ts { reportPrivateInBaseOfClassExpression, moduleResolverHost: host, trackReferencedAmbientModule, + trackExternalModuleSymbolOfImportTypeNode }; let errorNameNode: DeclarationName | undefined; @@ -115,6 +117,12 @@ namespace ts { } } + function trackExternalModuleSymbolOfImportTypeNode(symbol: Symbol) { + if (!isBundledEmit) { + (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); + } + } + function trackSymbol(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags) { if (symbol.flags & SymbolFlags.TypeParameter) return; handleSymbolAccessibilityError(resolver.isSymbolAccessible(symbol, enclosingDeclaration, meaning, /*shouldComputeAliasesToMakeVisible*/ true)); @@ -224,6 +232,7 @@ namespace ts { combinedStatements = setTextRange(createNodeArray([...combinedStatements, createExportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createNamedExports([]), /*moduleSpecifier*/ undefined)]), combinedStatements); } const updated = updateSourceFileNode(node, combinedStatements, /*isDeclarationFile*/ true, references, getFileReferencesForUsedTypeReferences(), node.hasNoDefaultLib); + updated.exportedModulesFromDeclarationEmit = exportedModulesFromDeclarationEmit; return updated; function getFileReferencesForUsedTypeReferences() { @@ -483,10 +492,18 @@ namespace ts { function rewriteModuleSpecifier(parent: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode, input: T | undefined): T | StringLiteral { if (!input) return undefined!; // TODO: GH#18217 resultHasExternalModuleIndicator = resultHasExternalModuleIndicator || (parent.kind !== SyntaxKind.ModuleDeclaration && parent.kind !== SyntaxKind.ImportType); - if (input.kind === SyntaxKind.StringLiteral && isBundledEmit) { - const newName = getExternalModuleNameFromDeclaration(context.getEmitHost(), resolver, parent); - if (newName) { - return createLiteral(newName); + if (isStringLiteralLike(input)) { + if (isBundledEmit) { + const newName = getExternalModuleNameFromDeclaration(context.getEmitHost(), resolver, parent); + if (newName) { + return createLiteral(newName); + } + } + else { + const symbol = resolver.getSymbolOfExternalModuleSpecifier(input); + if (symbol) { + (exportedModulesFromDeclarationEmit || (exportedModulesFromDeclarationEmit = [])).push(symbol); + } } } return input; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 2fdf8d4ac7525..bc5456ffe7f1c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2633,8 +2633,13 @@ namespace ts { /* @internal */ pragmas: PragmaMap; /* @internal */ localJsxNamespace?: __String; /* @internal */ localJsxFactory?: EntityName; + + /*@internal*/ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit; } + /*@internal*/ + export type ExportedModulesFromDeclarationEmit = ReadonlyArray; + export interface Bundle extends Node { kind: SyntaxKind.Bundle; prepends: ReadonlyArray; @@ -2878,6 +2883,7 @@ namespace ts { diagnostics: ReadonlyArray; emittedFiles?: string[]; // Array of files the compiler wrote to disk /* @internal */ sourceMaps?: SourceMapData[]; // Array of sourceMapData if compiler emitted sourcemaps + /* @internal */ exportedModulesFromDeclarationEmit?: ExportedModulesFromDeclarationEmit; } /* @internal */ @@ -3381,6 +3387,7 @@ namespace ts { isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean; getJsxFactoryEntity(location?: Node): EntityName | undefined; getAllAccessorDeclarations(declaration: AccessorDeclaration): AllAccessorDeclarations; + getSymbolOfExternalModuleSpecifier(node: StringLiteralLike): Symbol | undefined; } export const enum SymbolFlags { @@ -5338,6 +5345,7 @@ namespace ts { reportInaccessibleUniqueSymbolError?(): void; moduleResolverHost?: ModuleSpecifierResolutionHost; trackReferencedAmbientModule?(decl: ModuleDeclaration, symbol: Symbol): void; + trackExternalModuleSymbolOfImportTypeNode?(symbol: Symbol): void; } export interface TextSpan { diff --git a/src/testRunner/unittests/tscWatchMode.ts b/src/testRunner/unittests/tscWatchMode.ts index 880c494645af1..8a3b732ad979f 100644 --- a/src/testRunner/unittests/tscWatchMode.ts +++ b/src/testRunner/unittests/tscWatchMode.ts @@ -1270,6 +1270,49 @@ export default test;`; checkOutputErrorsIncremental(host, expectedErrors); } }); + + it("updates errors when deep import file changes", () => { + const currentDirectory = "/user/username/projects/myproject"; + const aFile: File = { + path: `${currentDirectory}/a.ts`, + content: `import {B} from './b'; +declare var console: any; +let b = new B(); +console.log(b.c.d);` + }; + const bFile: File = { + path: `${currentDirectory}/b.ts`, + content: `import {C} from './c'; +export class B +{ + c = new C(); +}` + }; + const cFile: File = { + path: `${currentDirectory}/c.ts`, + content: `export class C +{ + d = 1; +}` + }; + const config: File = { + path: `${currentDirectory}/tsconfig.json`, + content: `{}` + }; + const files = [aFile, bFile, cFile, config, libFile]; + const host = createWatchedSystem(files, { currentDirectory }); + 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); + }); }); describe("tsc-watch emit with outFile or out setting", () => {