From 2586bb303c976eb8f55a47e95a9b450690f8b119 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 7 Dec 2017 12:39:26 -0800 Subject: [PATCH] From builder use the builderState containing references and file infos --- src/compiler/builder.ts | 480 ++---------------- src/compiler/builderState.ts | 214 +++++--- .../reference/api/tsserverlibrary.d.ts | 92 +--- tests/baselines/reference/api/typescript.d.ts | 62 +-- 4 files changed, 214 insertions(+), 634 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 9c7707bd1d8..ff372479fad 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -1,101 +1,26 @@ -/// +/// /*@internal*/ namespace ts { - export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, - cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { - const outputFiles: OutputFile[] = []; - const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); - return { outputFiles, emitSkipped: emitResult.emitSkipped }; - - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { - outputFiles.push({ name: fileName, writeByteOrderMark, text }); - } - } - - /** - * Internal Builder to get files affected by another file - */ - export interface InternalBuilder extends BaseBuilder { - /** - * Gets the files affected by the file path - * This api is only for internal use - */ - /*@internal*/ - getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken): ReadonlyArray; - } - - /** - * Create the internal builder to get files affected by sourceFile - */ - export function createInternalBuilder(host: BuilderHost): InternalBuilder { - return createBuilder(host, BuilderKind.BuilderKindInternal); - } - export enum BuilderKind { - BuilderKindInternal, BuilderKindSemanticDiagnostics, BuilderKindEmitAndSemanticDiagnostics } - /** - * Information about the source file: Its version and optional signature from last emit - */ - interface FileInfo { - version: string; - signature?: string; - } - - /** - * Referenced files with values for the keys as referenced file's path to be true - */ - type ReferencedSet = ReadonlyMap; - - function hasSameKeys(map1: ReadonlyMap | undefined, map2: ReadonlyMap | undefined) { - if (map1 === undefined) { - return map2 === undefined; - } - if (map2 === undefined) { - return map1 === undefined; - } - // Has same size and every key is present in both maps - return map1.size === map2.size && !forEachEntry(map1, (_value, key) => !map2.has(key)); - } - - export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindInternal): InternalBuilder; export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindSemanticDiagnostics): SemanticDiagnosticsBuilder; export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindEmitAndSemanticDiagnostics): EmitAndSemanticDiagnosticsBuilder; export function createBuilder(host: BuilderHost, builderKind: BuilderKind) { /** - * Create the canonical file name for identity + * State corresponding to all the file references and shapes of the module etc */ - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); - /** - * Computing hash to for signature verification - */ - const computeHash = host.createHash || identity; - - /** - * Information of the file eg. its version, signature etc - */ - const fileInfos = createMap(); - - /** - * true if module emit is enabled - */ - let isModuleEmit: boolean; - - /** - * Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled - * Otherwise undefined - */ - let referencedMap: Map | undefined; - - /** - * Get the files affected by the source file. - * This is dependent on whether its a module emit or not and hence function expression - */ - let getEmitDependentFilesAffectedBy: (programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined) => ReadonlyArray; + const state = createBuilderState({ + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(), + createHash: host.createHash, + onUpdateProgramInitialized, + onSourceFileAdd: addToChangedFilesSet, + onSourceFileChanged: path => { addToChangedFilesSet(path); deleteSemanticDiagnostics(path); }, + onSourceFileRemoved: deleteSemanticDiagnostics + }); /** * Cache of semantic diagnostics for files with their Path being the key @@ -107,18 +32,6 @@ namespace ts { */ const changedFilesSet = createMap(); - /** - * Map of files that have already called update signature. - * That means hence forth these files are assumed to have - * no change in their signature for this version of the program - */ - const hasCalledUpdateShapeSignature = createMap(); - - /** - * Cache of all files excluding default library file for the current program - */ - let allFilesExcludingDefaultLibraryFile: ReadonlyArray | undefined; - /** * Set of affected files being iterated */ @@ -142,8 +55,6 @@ namespace ts { const seenAffectedFiles = createMap(); switch (builderKind) { - case BuilderKind.BuilderKindInternal: - return getInternalBuilder(); case BuilderKind.BuilderKindSemanticDiagnostics: return getSemanticDiagnosticsBuilder(); case BuilderKind.BuilderKindEmitAndSemanticDiagnostics: @@ -152,14 +63,6 @@ namespace ts { notImplemented(); } - function getInternalBuilder(): InternalBuilder { - return { - updateProgram, - getFilesAffectedBy, - getAllDependencies - }; - } - function getSemanticDiagnosticsBuilder(): SemanticDiagnosticsBuilder { return { updateProgram, @@ -179,17 +82,13 @@ namespace ts { } /** - * Update current state to reflect new program - * Updates changed files, references, file infos etc + * Initialize changedFiles, affected files set, cached diagnostics, signatures */ - function updateProgram(newProgram: Program) { - const newProgramHasModuleEmit = newProgram.getCompilerOptions().module !== ModuleKind.None; - const oldReferencedMap = referencedMap; - if (isModuleEmit !== newProgramHasModuleEmit) { + function onUpdateProgramInitialized(isModuleEmitChanged: boolean) { + if (isModuleEmitChanged) { // Changes in the module emit, clear out everything and initialize as if first time // Clear file information and semantic diagnostics - fileInfos.clear(); semanticDiagnosticsPerFile.clear(); // Clear changed files and affected files information @@ -197,21 +96,12 @@ namespace ts { affectedFiles = undefined; currentChangedFilePath = undefined; currentAffectedFilesSignatures.clear(); - - // Update the reference map creation - referencedMap = newProgramHasModuleEmit ? createMap() : undefined; - - // Update the module emit - isModuleEmit = newProgramHasModuleEmit; - getEmitDependentFilesAffectedBy = isModuleEmit ? - getFilesAffectedByUpdatedShapeWhenModuleEmit : - getFilesAffectedByUpdatedShapeWhenNonModuleEmit; } else { if (currentChangedFilePath) { // Remove the diagnostics for all the affected files since we should resume the state such that // the whole iteration on currentChangedFile never happened - affectedFiles.map(sourceFile => semanticDiagnosticsPerFile.delete(sourceFile.path)); + affectedFiles.forEach(sourceFile => deleteSemanticDiagnostics(sourceFile.path)); affectedFiles = undefined; currentAffectedFilesSignatures.clear(); } @@ -219,100 +109,27 @@ namespace ts { // Verify the sanity of old state Debug.assert(!affectedFiles && !currentAffectedFilesSignatures.size, "Cannot reuse if only few affected files of currentChangedFile were iterated"); } - Debug.assert(!forEachEntry(changedFilesSet, (_value, path) => semanticDiagnosticsPerFile.has(path)), "Semantic diagnostics shouldnt be available for changed files"); - } - - // Clear datas that cant be retained beyond previous state - seenAffectedFiles.clear(); - hasCalledUpdateShapeSignature.clear(); - allFilesExcludingDefaultLibraryFile = undefined; - - // Create the reference map and update changed files - for (const sourceFile of newProgram.getSourceFiles()) { - const version = sourceFile.version; - const newReferences = referencedMap && getReferencedFiles(newProgram, sourceFile); - const oldInfo = fileInfos.get(sourceFile.path); - let oldReferences: ReferencedSet; - - // Register changed file if its new file or we arent reusing old state - if (!oldInfo) { - // New file: Set the file info - fileInfos.set(sourceFile.path, { version }); - changedFilesSet.set(sourceFile.path, true); - } - // versions dont match - else if (oldInfo.version !== version || - // Referenced files changed - !hasSameKeys(newReferences, (oldReferences = oldReferencedMap && oldReferencedMap.get(sourceFile.path))) || - // Referenced file was deleted in the new program - newReferences && forEachEntry(newReferences, (_value, path) => !newProgram.getSourceFileByPath(path as Path) && fileInfos.has(path))) { - - // Changed file: Update the version, set as changed file - oldInfo.version = version; - changedFilesSet.set(sourceFile.path, true); - - // All changed files need to re-evaluate its semantic diagnostics - semanticDiagnosticsPerFile.delete(sourceFile.path); - } - - // Set the references - if (newReferences) { - referencedMap.set(sourceFile.path, newReferences); - } - else if (referencedMap) { - referencedMap.delete(sourceFile.path); - } - } - - // For removed files, remove the semantic diagnostics and file info - if (fileInfos.size > newProgram.getSourceFiles().length) { - fileInfos.forEach((_value, path) => { - if (!newProgram.getSourceFileByPath(path as Path)) { - fileInfos.delete(path); - semanticDiagnosticsPerFile.delete(path); - if (referencedMap) { - referencedMap.delete(path); - } - } - }); + Debug.assert(!forEachKey(changedFilesSet, path => semanticDiagnosticsPerFile.has(path)), "Semantic diagnostics shouldnt be available for changed files"); } } /** - * Gets the files affected by the path from the program + * Add file to the changed files set */ - function getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken | undefined, cacheToUpdateSignature?: Map): 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 - // In the other cases the affected files signatures are commited only after the iteration through the result is complete - const signatureCache = cacheToUpdateSignature || createMap(); - const sourceFile = programOfThisState.getSourceFileByPath(path); - if (!sourceFile) { - return emptyArray; - } + function addToChangedFilesSet(path: Path) { + changedFilesSet.set(path, true); + } - if (!updateShapeSignature(programOfThisState, sourceFile, signatureCache, cancellationToken)) { - return [sourceFile]; - } - - const result = getEmitDependentFilesAffectedBy(programOfThisState, sourceFile, signatureCache, cancellationToken); - if (!cacheToUpdateSignature) { - // Commit all the signatures in the signature cache - updateSignaturesFromCache(signatureCache); - } - return result; + function deleteSemanticDiagnostics(path: Path) { + semanticDiagnosticsPerFile.delete(path); } /** - * Updates the signatures from the cache - * This should be called whenever it is safe to commit the state of the builder + * Update current state to reflect new program + * Updates changed files, references, file infos etc which happens through the state callbacks */ - function updateSignaturesFromCache(signatureCache: Map) { - signatureCache.forEach((signature, path) => { - fileInfos.get(path).signature = signature; - hasCalledUpdateShapeSignature.set(path, true); - }); + function updateProgram(newProgram: Program) { + state.updateProgram(newProgram); } /** @@ -339,7 +156,7 @@ namespace ts { changedFilesSet.delete(currentChangedFilePath); currentChangedFilePath = undefined; // Commit the changes in file signature - updateSignaturesFromCache(currentAffectedFilesSignatures); + state.updateSignaturesFromCache(currentAffectedFilesSignatures); currentAffectedFilesSignatures.clear(); affectedFiles = undefined; } @@ -361,7 +178,7 @@ namespace ts { // Get next batch of affected files currentAffectedFilesSignatures.clear(); - affectedFiles = getFilesAffectedBy(programOfThisState, nextKey.value as Path, cancellationToken, currentAffectedFilesSignatures); + affectedFiles = state.getFilesAffectedBy(programOfThisState, nextKey.value as Path, cancellationToken, currentAffectedFilesSignatures); currentChangedFilePath = nextKey.value as Path; semanticDiagnosticsPerFile.delete(currentChangedFilePath); affectedFilesIndex = 0; @@ -500,250 +317,13 @@ namespace ts { /** * Get all the dependencies of the sourceFile */ - function getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): string[] { - const compilerOptions = programOfThisState.getCompilerOptions(); - // With --out or --outFile all outputs go into single file, all files depend on each other - if (compilerOptions.outFile || compilerOptions.out) { - return programOfThisState.getSourceFiles().map(getFileName); - } - - // If this is non module emit, or its a global file, it depends on all the source files - if (!isModuleEmit || (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile))) { - return programOfThisState.getSourceFiles().map(getFileName); - } - - // Get the references, traversing deep from the referenceMap - Debug.assert(!!referencedMap); - const seenMap = createMap(); - const queue = [sourceFile.path]; - while (queue.length) { - const path = queue.pop(); - if (!seenMap.has(path)) { - seenMap.set(path, true); - const references = referencedMap.get(path); - if (references) { - const iterator = references.keys(); - for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) { - queue.push(value as Path); - } - } - } - } - - return flatMapIter(seenMap.keys(), path => { - const file = programOfThisState.getSourceFileByPath(path as Path); - if (file) { - return file.fileName; - } - return path; - }); - } - - function getFileName(sourceFile: SourceFile) { - return sourceFile.fileName; - } - - /** - * For script files that contains only ambient external modules, although they are not actually external module files, - * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, - * there are no point to rebuild all script files if these special files have changed. However, if any statement - * in the file is not ambient external module, we treat it as a regular script file. - */ - function containsOnlyAmbientModules(sourceFile: SourceFile) { - for (const statement of sourceFile.statements) { - if (!isModuleWithStringLiteralName(statement)) { - return false; - } - } - return true; - } - - /** - * Returns if the shape of the signature has changed since last emit - * Note that it also updates the current signature as the latest signature for the file - */ - function updateShapeSignature(program: Program, sourceFile: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined) { - Debug.assert(!!sourceFile); - - // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate - if (hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) { - return false; - } - - const info = fileInfos.get(sourceFile.path); - Debug.assert(!!info); - - const prevSignature = info.signature; - let latestSignature: string; - if (sourceFile.isDeclarationFile) { - latestSignature = sourceFile.version; - } - else { - const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken); - if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { - latestSignature = computeHash(emitOutput.outputFiles[0].text); - } - else { - latestSignature = prevSignature; - } - } - cacheToUpdateSignature.set(sourceFile.path, latestSignature); - - return !prevSignature || latestSignature !== prevSignature; - } - - /** - * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true - */ - function getReferencedFiles(program: Program, sourceFile: SourceFile): Map | undefined { - let referencedFiles: Map | undefined; - - // We need to use a set here since the code can contain the same import twice, - // but that will only be one dependency. - // To avoid invernal conversion, the key of the referencedFiles map must be of type Path - 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 sourceFileDirectory = getDirectoryPath(sourceFile.path); - // Handle triple slash references - if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { - for (const referencedFile of sourceFile.referencedFiles) { - const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); - addReferencedFile(referencedPath); - } - } - - // Handle type reference directives - if (sourceFile.resolvedTypeReferenceDirectiveNames) { - sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { - if (!resolvedTypeReferenceDirective) { - return; - } - - const fileName = resolvedTypeReferenceDirective.resolvedFileName; - const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName); - addReferencedFile(typeFilePath); - }); - } - - return referencedFiles; - - function addReferencedFile(referencedPath: Path) { - if (!referencedFiles) { - referencedFiles = createMap(); - } - referencedFiles.set(referencedPath, true); - } - } - - /** - * Gets the files referenced by the the file path - */ - function getReferencedByPaths(referencedFilePath: Path) { - return mapDefinedIter(referencedMap.entries(), ([filePath, referencesInFile]) => - referencesInFile.has(referencedFilePath) ? filePath as Path : undefined - ); - } - - /** - * Gets all files of the program excluding the default library file - */ - function getAllFilesExcludingDefaultLibraryFile(program: Program, firstSourceFile: SourceFile): ReadonlyArray { - // Use cached result - if (allFilesExcludingDefaultLibraryFile) { - return allFilesExcludingDefaultLibraryFile; - } - - let result: SourceFile[]; - addSourceFile(firstSourceFile); - for (const sourceFile of program.getSourceFiles()) { - if (sourceFile !== firstSourceFile) { - addSourceFile(sourceFile); - } - } - allFilesExcludingDefaultLibraryFile = result || emptyArray; - return allFilesExcludingDefaultLibraryFile; - - function addSourceFile(sourceFile: SourceFile) { - if (!program.isSourceFileDefaultLibrary(sourceFile)) { - (result || (result = [])).push(sourceFile); - } - } - } - - /** - * When program emits non modular code, gets the files affected by the sourceFile whose shape has changed - */ - function getFilesAffectedByUpdatedShapeWhenNonModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile) { - const compilerOptions = programOfThisState.getCompilerOptions(); - // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, - // so returning the file itself is good enough. - if (compilerOptions && (compilerOptions.out || compilerOptions.outFile)) { - return [sourceFileWithUpdatedShape]; - } - return getAllFilesExcludingDefaultLibraryFile(programOfThisState, sourceFileWithUpdatedShape); - } - - /** - * When program emits modular code, gets the files affected by the sourceFile whose shape has changed - */ - function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map, cancellationToken: CancellationToken | undefined) { - if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) { - return getAllFilesExcludingDefaultLibraryFile(programOfThisState, sourceFileWithUpdatedShape); - } - - const compilerOptions = programOfThisState.getCompilerOptions(); - if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { - return [sourceFileWithUpdatedShape]; - } - - // Now we need to if each file in the referencedBy list has a shape change as well. - // Because if so, its own referencedBy files need to be saved as well to make the - // emitting result consistent with files on disk. - const seenFileNamesMap = createMap(); - - // Start with the paths this file was referenced by - seenFileNamesMap.set(sourceFileWithUpdatedShape.path, sourceFileWithUpdatedShape); - const queue = getReferencedByPaths(sourceFileWithUpdatedShape.path); - while (queue.length > 0) { - const currentPath = queue.pop(); - if (!seenFileNamesMap.has(currentPath)) { - const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath); - seenFileNamesMap.set(currentPath, currentSourceFile); - if (currentSourceFile && updateShapeSignature(programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken)) { - queue.push(...getReferencedByPaths(currentPath)); - } - } - } - - // Return array of values that needs emit - return flatMapIter(seenFileNamesMap.values(), value => value); + function getAllDependencies(programOfThisState: Program, sourceFile: SourceFile) { + return state.getAllDependencies(programOfThisState, sourceFile); } } } namespace ts { - export interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - - export interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } - export type AffectedFileResult = { result: T; affected: SourceFile | Program; } | undefined; export interface BuilderHost { @@ -769,7 +349,7 @@ namespace ts { /** * Get all the dependencies of the file */ - getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): string[]; + getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray; } /** diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts index f6706195a75..eb3887eed15 100644 --- a/src/compiler/builderState.ts +++ b/src/compiler/builderState.ts @@ -46,7 +46,76 @@ namespace ts { return map1 === undefined; } // Has same size and every key is present in both maps - return map1.size === map2.size && !forEachEntry(map1, (_value, key) => !map2.has(key)); + return map1.size === map2.size && !forEachKey(map1, key => !map2.has(key)); + } + + /** + * For script files that contains only ambient external modules, although they are not actually external module files, + * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, + * there are no point to rebuild all script files if these special files have changed. However, if any statement + * in the file is not ambient external module, we treat it as a regular script file. + */ + function containsOnlyAmbientModules(sourceFile: SourceFile) { + for (const statement of sourceFile.statements) { + if (!isModuleWithStringLiteralName(statement)) { + return false; + } + } + return true; + } + + /** + * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true + */ + function getReferencedFiles(program: Program, sourceFile: SourceFile, getCanonicalFileName: GetCanonicalFileName): Map | undefined { + let referencedFiles: Map | undefined; + + // We need to use a set here since the code can contain the same import twice, + // but that will only be one dependency. + // To avoid invernal conversion, the key of the referencedFiles map must be of type Path + 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 sourceFileDirectory = getDirectoryPath(sourceFile.path); + // Handle triple slash references + if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { + for (const referencedFile of sourceFile.referencedFiles) { + const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); + addReferencedFile(referencedPath); + } + } + + // Handle type reference directives + if (sourceFile.resolvedTypeReferenceDirectiveNames) { + sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { + if (!resolvedTypeReferenceDirective) { + return; + } + + const fileName = resolvedTypeReferenceDirective.resolvedFileName; + const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName); + addReferencedFile(typeFilePath); + }); + } + + return referencedFiles; + + function addReferencedFile(referencedPath: Path) { + if (!referencedFiles) { + referencedFiles = createMap(); + } + referencedFiles.set(referencedPath, true); + } } export interface BuilderStateHost { @@ -76,6 +145,15 @@ namespace ts { * Gets the files affected by the file path */ getFilesAffectedBy(programOfThisState: Program, path: Path, cancellationToken: CancellationToken, cacheToUpdateSignature?: Map): ReadonlyArray; + /** + * Updates the signatures from the cache + * This should be called whenever it is safe to commit the state of the builder + */ + updateSignaturesFromCache(signatureCache: Map): void; + /** + * Get all the dependencies of the sourceFile + */ + getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray; } export function createBuilderState(host: BuilderStateHost): BuilderState { @@ -121,11 +199,17 @@ namespace ts { * Cache of all files excluding default library file for the current program */ let allFilesExcludingDefaultLibraryFile: ReadonlyArray | undefined; + /** + * Cache of all the file names + */ + let allFileNames: ReadonlyArray | undefined; return { updateProgram, getFilesAffectedBy, - }; + getAllDependencies, + updateSignaturesFromCache + }; /** * Update current state to reflect new program @@ -155,11 +239,12 @@ namespace ts { // Clear datas that cant be retained beyond previous state hasCalledUpdateShapeSignature.clear(); allFilesExcludingDefaultLibraryFile = undefined; + allFileNames = undefined; // Create the reference map and update changed files for (const sourceFile of newProgram.getSourceFiles()) { const version = sourceFile.version; - const newReferences = referencedMap && getReferencedFiles(newProgram, sourceFile); + const newReferences = referencedMap && getReferencedFiles(newProgram, sourceFile, getCanonicalFileName); const oldInfo = fileInfos.get(sourceFile.path); let oldReferences: ReferencedSet; @@ -174,7 +259,7 @@ namespace ts { // Referenced files changed !hasSameKeys(newReferences, (oldReferences = oldReferencedMap && oldReferencedMap.get(sourceFile.path))) || // Referenced file was deleted in the new program - newReferences && forEachEntry(newReferences, (_value, path) => !newProgram.getSourceFileByPath(path as Path) && fileInfos.has(path))) { + newReferences && forEachKey(newReferences, path => !newProgram.getSourceFileByPath(path as Path) && fileInfos.has(path))) { // Changed file: Update the version, set as changed file oldInfo.version = version; @@ -230,6 +315,58 @@ namespace ts { return result; } + /** + * Get all the dependencies of the sourceFile + */ + function getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray { + const compilerOptions = programOfThisState.getCompilerOptions(); + // With --out or --outFile all outputs go into single file, all files depend on each other + if (compilerOptions.outFile || compilerOptions.out) { + return getAllFileNames(programOfThisState); + } + + // If this is non module emit, or its a global file, it depends on all the source files + if (!isModuleEmit || (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile))) { + return getAllFileNames(programOfThisState); + } + + // Get the references, traversing deep from the referenceMap + Debug.assert(!!referencedMap); + const seenMap = createMap(); + const queue = [sourceFile.path]; + while (queue.length) { + const path = queue.pop(); + if (!seenMap.has(path)) { + seenMap.set(path, true); + const references = referencedMap.get(path); + if (references) { + const iterator = references.keys(); + for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) { + queue.push(value as Path); + } + } + } + } + + return flatMapIter(seenMap.keys(), path => { + const file = programOfThisState.getSourceFileByPath(path as Path); + if (file) { + return file.fileName; + } + return path; + }); + } + + /** + * Gets the names of all files from the program + */ + function getAllFileNames(programOfThisState: Program): ReadonlyArray { + if (!allFileNames) { + allFileNames = programOfThisState.getSourceFiles().map(file => file.fileName); + } + return allFileNames; + } + /** * Updates the signatures from the cache * This should be called whenever it is safe to commit the state of the builder @@ -241,21 +378,6 @@ namespace ts { }); } - /** - * For script files that contains only ambient external modules, although they are not actually external module files, - * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, - * there are no point to rebuild all script files if these special files have changed. However, if any statement - * in the file is not ambient external module, we treat it as a regular script file. - */ - function containsOnlyAmbientModules(sourceFile: SourceFile) { - for (const statement of sourceFile.statements) { - if (!isModuleWithStringLiteralName(statement)) { - return false; - } - } - return true; - } - /** * Returns if the shape of the signature has changed since last emit * Note that it also updates the current signature as the latest signature for the file @@ -290,60 +412,6 @@ namespace ts { return !prevSignature || latestSignature !== prevSignature; } - /** - * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true - */ - function getReferencedFiles(program: Program, sourceFile: SourceFile): Map | undefined { - let referencedFiles: Map | undefined; - - // We need to use a set here since the code can contain the same import twice, - // but that will only be one dependency. - // To avoid invernal conversion, the key of the referencedFiles map must be of type Path - 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 sourceFileDirectory = getDirectoryPath(sourceFile.path); - // Handle triple slash references - if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { - for (const referencedFile of sourceFile.referencedFiles) { - const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); - addReferencedFile(referencedPath); - } - } - - // Handle type reference directives - if (sourceFile.resolvedTypeReferenceDirectiveNames) { - sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { - if (!resolvedTypeReferenceDirective) { - return; - } - - const fileName = resolvedTypeReferenceDirective.resolvedFileName; - const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName); - addReferencedFile(typeFilePath); - }); - } - - return referencedFiles; - - function addReferencedFile(referencedPath: Path) { - if (!referencedFiles) { - referencedFiles = createMap(); - } - referencedFiles.set(referencedPath, true); - } - } - /** * Gets the files referenced by the the file path */ diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index d7f3821a25d..32f4b78a8c5 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3757,87 +3757,6 @@ declare namespace ts { declare namespace ts { function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer; } -declare namespace ts { - interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } - type AffectedFileResult = { - result: T; - affected: SourceFile | Program; - } | undefined; - interface BuilderHost { - /** - * return true if file names are treated with case sensitivity - */ - useCaseSensitiveFileNames(): boolean; - /** - * If provided this would be used this hash instead of actual file shape text for detecting changes - */ - createHash?: (data: string) => string; - } - /** - * Builder to manage the program state changes - */ - interface BaseBuilder { - /** - * Updates the program in the builder to represent new state - */ - updateProgram(newProgram: Program): void; - /** - * Get all the dependencies of the file - */ - getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): string[]; - } - /** - * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files - */ - interface SemanticDiagnosticsBuilder extends BaseBuilder { - /** - * Gets the semantic diagnostics from the program for the next affected file and caches it - * Returns undefined if the iteration is complete - */ - getSemanticDiagnosticsOfNextAffectedFile(programOfThisState: Program, cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; - /** - * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program - * The semantic diagnostics are cached and managed here - * Note that it is assumed that the when asked about semantic diagnostics through this API, - * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics - */ - getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; - } - /** - * The builder that can handle the changes in program and iterate through changed file to emit the files - * The semantic diagnostics are cached per file and managed by clearing for the changed/affected files - */ - interface EmitAndSemanticDiagnosticsBuilder extends BaseBuilder { - /** - * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete - */ - emitNextAffectedFile(programOfThisState: Program, writeFileCallback: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult; - /** - * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program - * The semantic diagnostics are cached and managed here - * Note that it is assumed that the when asked about semantic diagnostics through this API, - * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics - */ - getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; - } - /** - * Create the builder to manage semantic diagnostics and cache them - */ - function createSemanticDiagnosticsBuilder(host: BuilderHost): SemanticDiagnosticsBuilder; - /** - * Create the builder that can handle the changes in program and iterate through changed files - * to emit the those files and manage semantic diagnostics cache as well - */ - function createEmitAndSemanticDiagnosticsBuilder(host: BuilderHost): EmitAndSemanticDiagnosticsBuilder; -} declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; function resolveTripleslashReference(moduleName: string, containingFile: string): string; @@ -7233,6 +7152,17 @@ declare namespace ts.server { onProjectClosed(project: Project): void; } } +declare namespace ts { + interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } +} declare namespace ts.server { enum ProjectKind { Inferred = 0, diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 06c75e843c0..514c140f05e 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3704,6 +3704,35 @@ declare namespace ts { declare namespace ts { function createPrinter(printerOptions?: PrinterOptions, handlers?: PrintHandlers): Printer; } +declare namespace ts { + function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; + function resolveTripleslashReference(moduleName: string, containingFile: string): string; + function createCompilerHost(options: CompilerOptions, setParentNodes?: boolean): CompilerHost; + function getPreEmitDiagnostics(program: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[]; + interface FormatDiagnosticsHost { + getCurrentDirectory(): string; + getCanonicalFileName(fileName: string): string; + getNewLine(): string; + } + function formatDiagnostics(diagnostics: ReadonlyArray, host: FormatDiagnosticsHost): string; + function formatDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string; + function formatDiagnosticsWithColorAndContext(diagnostics: ReadonlyArray, host: FormatDiagnosticsHost): string; + function flattenDiagnosticMessageText(messageText: string | DiagnosticMessageChain, newLine: string): string; + /** + * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' + * that represent a compilation unit. + * + * Creating a program proceeds from a set of root files, expanding the set of inputs by following imports and + * triple-slash-reference-path directives transitively. '@types' and triple-slash-reference-types are also pulled in. + * + * @param rootNames - A set of root files. + * @param options - The compiler options which should be used. + * @param host - The host interacts with the underlying file system. + * @param oldProgram - Reuses an old program structure. + * @returns A 'Program' object. + */ + function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program; +} declare namespace ts { interface EmitOutput { outputFiles: OutputFile[]; @@ -3714,6 +3743,8 @@ declare namespace ts { writeByteOrderMark: boolean; text: string; } +} +declare namespace ts { type AffectedFileResult = { result: T; affected: SourceFile | Program; @@ -3739,7 +3770,7 @@ declare namespace ts { /** * Get all the dependencies of the file */ - getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): string[]; + getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray; } /** * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files @@ -3785,35 +3816,6 @@ declare namespace ts { */ function createEmitAndSemanticDiagnosticsBuilder(host: BuilderHost): EmitAndSemanticDiagnosticsBuilder; } -declare namespace ts { - function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; - function resolveTripleslashReference(moduleName: string, containingFile: string): string; - function createCompilerHost(options: CompilerOptions, setParentNodes?: boolean): CompilerHost; - function getPreEmitDiagnostics(program: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): Diagnostic[]; - interface FormatDiagnosticsHost { - getCurrentDirectory(): string; - getCanonicalFileName(fileName: string): string; - getNewLine(): string; - } - function formatDiagnostics(diagnostics: ReadonlyArray, host: FormatDiagnosticsHost): string; - function formatDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string; - function formatDiagnosticsWithColorAndContext(diagnostics: ReadonlyArray, host: FormatDiagnosticsHost): string; - function flattenDiagnosticMessageText(messageText: string | DiagnosticMessageChain, newLine: string): string; - /** - * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' - * that represent a compilation unit. - * - * Creating a program proceeds from a set of root files, expanding the set of inputs by following imports and - * triple-slash-reference-path directives transitively. '@types' and triple-slash-reference-types are also pulled in. - * - * @param rootNames - A set of root files. - * @param options - The compiler options which should be used. - * @param host - The host interacts with the underlying file system. - * @param oldProgram - Reuses an old program structure. - * @returns A 'Program' object. - */ - function createProgram(rootNames: ReadonlyArray, options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program; -} declare namespace ts { type DiagnosticReporter = (diagnostic: Diagnostic) => void; /**