diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index ff372479fa..198bf0556f 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -13,7 +13,7 @@ namespace ts { /** * State corresponding to all the file references and shapes of the module etc */ - const state = createBuilderState({ + const state = createBuilderStateOld({ useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(), createHash: host.createHash, onUpdateProgramInitialized, diff --git a/src/compiler/builderState.ts b/src/compiler/builderState.ts index eb3887eed1..0cd55ebd61 100644 --- a/src/compiler/builderState.ts +++ b/src/compiler/builderState.ts @@ -136,7 +136,7 @@ namespace ts { onSourceFileRemoved(path: Path): void; } - export interface BuilderState { + export interface BuilderStateOld { /** * Updates the program in the builder to represent new state */ @@ -156,7 +156,65 @@ namespace ts { getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray; } - export function createBuilderState(host: BuilderStateHost): BuilderState { + export interface BuilderState { + /** + * Information of the file eg. its version, signature etc + */ + fileInfos: Map; + /** + * Contains the map of ReferencedSet=Referenced files of the file if module emit is enabled + * Otherwise undefined + * Thus non undefined value indicates, module emit + */ + readonly referencedMap: ReadonlyMap | undefined; + /** + * 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 + */ + hasCalledUpdateShapeSignature: Map; + /** + * Cache of all files excluding default library file for the current program + */ + allFilesExcludingDefaultLibraryFile: ReadonlyArray | undefined; + /** + * Cache of all the file names + */ + allFileNames: ReadonlyArray | undefined; + } + + export function createBuilderState(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: BuilderState): BuilderState { + const fileInfos = createMap(); + const referencedMap = newProgram.getCompilerOptions().module !== ModuleKind.None ? createMap() : undefined; + const hasCalledUpdateShapeSignature = createMap(); + const useOldState = oldState && !!oldState.referencedMap !== !!referencedMap; + + // Create the reference map, and set the file infos + for (const sourceFile of newProgram.getSourceFiles()) { + const version = sourceFile.version; + const oldInfo = useOldState && oldState.fileInfos.get(sourceFile.path); + if (referencedMap) { + const newReferences = getReferencedFiles(newProgram, sourceFile, getCanonicalFileName); + if (newReferences) { + referencedMap.set(sourceFile.path, newReferences); + } + } + fileInfos.set(sourceFile.path, { version, signature: oldInfo && oldInfo.signature }); + } + + oldState = undefined; + newProgram = undefined; + + return { + fileInfos, + referencedMap, + hasCalledUpdateShapeSignature, + allFilesExcludingDefaultLibraryFile: undefined, + allFileNames: undefined + }; + } + + export function createBuilderStateOld(host: BuilderStateHost): BuilderStateOld { /** * Create the canonical file name for identity */ @@ -171,11 +229,6 @@ namespace ts { */ 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 @@ -218,7 +271,7 @@ namespace ts { function updateProgram(newProgram: Program) { const newProgramHasModuleEmit = newProgram.getCompilerOptions().module !== ModuleKind.None; const oldReferencedMap = referencedMap; - const isModuleEmitChanged = isModuleEmit !== newProgramHasModuleEmit; + const isModuleEmitChanged = !!referencedMap !== newProgramHasModuleEmit; if (isModuleEmitChanged) { // Changes in the module emit, clear out everything and initialize as if first time @@ -229,8 +282,7 @@ namespace ts { referencedMap = newProgramHasModuleEmit ? createMap() : undefined; // Update the module emit - isModuleEmit = newProgramHasModuleEmit; - getEmitDependentFilesAffectedBy = isModuleEmit ? + getEmitDependentFilesAffectedBy = newProgramHasModuleEmit ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit; } @@ -326,12 +378,11 @@ namespace ts { } // If this is non module emit, or its a global file, it depends on all the source files - if (!isModuleEmit || (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile))) { + if (!referencedMap || (!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) { @@ -497,3 +548,177 @@ namespace ts { } } } + +/*@internal*/ +namespace ts.BuilderState { + type ComputeHash = (data: string) => string; + + /** + * 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 { + // 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; + } + + if (!updateShapeSignature(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash)) { + return [sourceFile]; + } + + const result = (state.referencedMap ? getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit)(state, programOfThisState, sourceFile, signatureCache, cancellationToken, computeHash); + if (!cacheToUpdateSignature) { + // Commit all the signatures in the signature cache + updateSignaturesFromCache(state, signatureCache); + } + return result; + } + + /** + * Updates the signatures from the cache into state's fileinfo signatures + * This should be called whenever it is safe to commit the state of the builder + */ + export function updateSignaturesFromCache(state: BuilderState, signatureCache: Map) { + signatureCache.forEach((signature, path) => { + state.fileInfos.get(path).signature = signature; + state.hasCalledUpdateShapeSignature.set(path, true); + }); + } + + /** + * 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 | 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 (state.hasCalledUpdateShapeSignature.has(sourceFile.path) || cacheToUpdateSignature.has(sourceFile.path)) { + return false; + } + + const info = state.fileInfos.get(sourceFile.path); + Debug.assert(!!info); + + const prevSignature = info.signature; + let latestSignature: string; + if (sourceFile.isDeclarationFile) { + latestSignature = sourceFile.version; + } + else { + const emitOutput = getFileEmitOutput(programOfThisState, sourceFile, /*emitOnlyDtsFiles*/ true, cancellationToken); + if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { + latestSignature = (computeHash || identity)(emitOutput.outputFiles[0].text); + } + else { + latestSignature = prevSignature; + } + } + cacheToUpdateSignature.set(sourceFile.path, latestSignature); + + return !prevSignature || latestSignature !== prevSignature; + } + + /** + * Gets the files referenced by the the file path + */ + function getReferencedByPaths(state: Readonly, referencedFilePath: Path) { + return mapDefinedIter(state.referencedMap.entries(), ([filePath, referencesInFile]) => + referencesInFile.has(referencedFilePath) ? filePath as Path : undefined + ); + } + + /** + * 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 all files of the program excluding the default library file + */ + function getAllFilesExcludingDefaultLibraryFile(state: BuilderState, programOfThisState: Program, firstSourceFile: SourceFile): ReadonlyArray { + // Use cached result + if (state.allFilesExcludingDefaultLibraryFile) { + return state.allFilesExcludingDefaultLibraryFile; + } + + let result: SourceFile[]; + addSourceFile(firstSourceFile); + for (const sourceFile of programOfThisState.getSourceFiles()) { + if (sourceFile !== firstSourceFile) { + addSourceFile(sourceFile); + } + } + state.allFilesExcludingDefaultLibraryFile = result || emptyArray; + return state.allFilesExcludingDefaultLibraryFile; + + function addSourceFile(sourceFile: SourceFile) { + if (!programOfThisState.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(state: BuilderState, 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(state, programOfThisState, sourceFileWithUpdatedShape); + } + + /** + * 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) { + if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) { + return getAllFilesExcludingDefaultLibraryFile(state, 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(state, 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(state, programOfThisState, currentSourceFile, cacheToUpdateSignature, cancellationToken, computeHash)) { + queue.push(...getReferencedByPaths(state, currentPath)); + } + } + } + + // Return array of values that needs emit + return flatMapIter(seenFileNamesMap.values(), value => value); + } +} diff --git a/src/server/project.ts b/src/server/project.ts index a5542cb89e..95c41eaaf4 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -139,7 +139,7 @@ namespace ts.server { /*@internal*/ resolutionCache: ResolutionCache; - private builder: BuilderState | undefined; + private builderState: BuilderState | undefined; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ @@ -460,18 +460,8 @@ namespace ts.server { return []; } this.updateGraph(); - if (!this.builder) { - this.builder = createBuilderState({ - useCaseSensitiveFileNames: this.useCaseSensitiveFileNames(), - createHash: data => this.projectService.host.createHash(data), - onUpdateProgramInitialized: noop, - onSourceFileAdd: noop, - onSourceFileChanged: noop, - onSourceFileRemoved: noop - }); - } - this.builder.updateProgram(this.program); - return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path, this.cancellationToken), + this.builderState = createBuilderState(this.program, this.projectService.toCanonicalFileName, this.builderState); + return mapDefined(BuilderState.getFilesAffectedBy(this.builderState, this.program, scriptInfo.path, this.cancellationToken, data => this.projectService.host.createHash(data)), sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); } @@ -507,7 +497,7 @@ namespace ts.server { } this.languageService.cleanupSemanticCache(); this.languageServiceEnabled = false; - this.builder = undefined; + this.builderState = undefined; this.resolutionCache.closeTypeRootsWatch(); this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false); } @@ -548,7 +538,7 @@ namespace ts.server { this.rootFilesMap = undefined; this.externalFiles = undefined; this.program = undefined; - this.builder = undefined; + this.builderState = undefined; this.resolutionCache.clear(); this.resolutionCache = undefined; this.cachedUnresolvedImportsPerFile = undefined;