From a06f0c3d9f97a99fb84cd7ee15433c2fe37655b8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 18 Oct 2017 17:15:02 -0700 Subject: [PATCH] Use builder state to emit instead --- src/compiler/builder.ts | 513 +++++++++--------- src/compiler/tsc.ts | 4 +- src/compiler/watch.ts | 40 +- src/harness/unittests/builder.ts | 9 +- src/server/project.ts | 27 +- .../reference/api/tsserverlibrary.d.ts | 67 ++- tests/baselines/reference/api/typescript.d.ts | 64 +++ 7 files changed, 408 insertions(+), 316 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 34ca1bdf0e9..9a60557ae36 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -11,10 +11,8 @@ namespace ts { writeByteOrderMark: boolean; text: string; } -} -/* @internal */ -namespace ts { + /* @internal */ export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { const outputFiles: OutputFile[] = []; @@ -26,213 +24,270 @@ namespace ts { } } - export interface Builder { - /** Called to inform builder about new program */ - updateProgram(newProgram: Program): void; - - /** Gets the files affected by the file path */ - getFilesAffectedBy(program: Program, path: Path): ReadonlyArray; - - /** Emit the changed files and clear the cache of the changed files */ - emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray; - - /** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */ - getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): ReadonlyArray; - - /** Called to reset the status of the builder */ - clear(): void; + 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)); } - interface EmitHandler { + /** + * State on which you can query affected files (files to save) and get semantic diagnostics(with their cache managed in the object) + * Note that it is only safe to pass BuilderState as old state when creating new state, when + * - If iterator's next method to get next affected file is never called + * - Iteration of single changed file and its dependencies (iteration through all of its affected files) is complete + */ + export interface BuilderState { /** - * Called when sourceFile is added to the program + * The map of file infos, where there is entry for each file in the program + * The entry is signature of the file (from last emit) or empty string */ - onAddSourceFile(program: Program, sourceFile: SourceFile): void; + fileInfos: ReadonlyMap>; + /** - * Called when sourceFile is removed from the program + * Returns true if module gerneration is not ModuleKind.None */ - onRemoveSourceFile(path: Path): void; + isModuleEmit: boolean; + /** - * For all source files, either "onUpdateSourceFile" or "onUpdateSourceFileWithSameVersion" will be called. - * If the builder is sure that the source file needs an update, "onUpdateSourceFile" will be called; - * otherwise "onUpdateSourceFileWithSameVersion" will be called. + * Map of file referenced or undefined if it wasnt module emit + * The entry is present only if file references other files + * The key is path of file and value is referenced map for that file (for every file referenced, there is entry in the set) */ - onUpdateSourceFile(program: Program, sourceFile: SourceFile): void; + referencedMap: ReadonlyMap | undefined; + /** - * For all source files, either "onUpdateSourceFile" or "onUpdateSourceFileWithSameVersion" will be called. - * If the builder is sure that the source file needs an update, "onUpdateSourceFile" will be called; - * otherwise "onUpdateSourceFileWithSameVersion" will be called. - * This function should return whether the source file should be marked as changed (meaning that something associated with file has changed, e.g. module resolution) + * Set of source file's paths that have been changed, either in resolution or versions */ - onUpdateSourceFileWithSameVersion(program: Program, sourceFile: SourceFile): boolean; + changedFilesSet: ReadonlyMap; + /** - * Gets the files affected by the script info which has updated shape from the known one + * Set of cached semantic diagnostics per file */ - getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray; + semanticDiagnosticsPerFile: ReadonlyMap>; + + /** + * Returns true if this state is safe to use as oldState + */ + canCreateNewStateFrom(): boolean; + + /** + * Gets the files affected by the file path + * This api is only for internal use + */ + /* @internal */ + getFilesAffectedBy(programOfThisState: Program, path: Path): ReadonlyArray; + + /** + * 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): AffectedFileEmitResult | undefined; + + /** + * 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, the file has been taken out of affected files + */ + getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; } - interface FileInfo { + /** + * Information about the source file: Its version and optional signature from last emit + */ + export interface FileInfo { version: string; signature: string; } + export interface AffectedFileEmitResult extends EmitResult { + affectedFile?: SourceFile; + } + + /** + * Referenced files with values for the keys as referenced file's path to be true + */ + export type ReferencedSet = ReadonlyMap; + export interface BuilderOptions { getCanonicalFileName: GetCanonicalFileName; computeHash: (data: string) => string; } - export function createBuilder(options: BuilderOptions): Builder { - let isModuleEmit: boolean | undefined; + export function createBuilderState(newProgram: Program, options: BuilderOptions, oldState?: Readonly): BuilderState { const fileInfos = createMap(); + const isModuleEmit = newProgram.getCompilerOptions().module !== ModuleKind.None; + const referencedMap = isModuleEmit ? createMap() : undefined; + const semanticDiagnosticsPerFile = createMap>(); /** The map has key by source file's path that has been changed */ const changedFilesSet = createMap(); const hasShapeChanged = createMap(); let allFilesExcludingDefaultLibraryFile: ReadonlyArray | undefined; - let emitHandler: EmitHandler; + + // Iterator datas + let affectedFiles: ReadonlyArray | undefined; + let affectedFilesIndex = 0; + const seenAffectedFiles = createMap(); + const getEmitDependentFilesAffectedBy = isModuleEmit ? + getFilesAffectedByUpdatedShapeWhenModuleEmit : getFilesAffectedByUpdatedShapeWhenNonModuleEmit; + + const useOldState = oldState && oldState.isModuleEmit === isModuleEmit; + if (useOldState) { + Debug.assert(oldState.canCreateNewStateFrom(), "Cannot use this state as old state"); + Debug.assert(!forEachEntry(oldState.changedFilesSet, (_value, path) => oldState.semanticDiagnosticsPerFile.has(path)), "Semantic diagnostics shouldnt be available for changed files"); + + copyEntries(oldState.changedFilesSet, changedFilesSet); + copyEntries(oldState.semanticDiagnosticsPerFile, semanticDiagnosticsPerFile); + } + + for (const sourceFile of newProgram.getSourceFiles()) { + const version = sourceFile.version; + let oldInfo: Readonly; + let oldReferences: ReferencedSet; + const newReferences = referencedMap && getReferencedFiles(newProgram, sourceFile); + + // Register changed file + // if not using old state so every file is changed + if (!useOldState || + // File wasnt present earlier + !(oldInfo = oldState.fileInfos.get(sourceFile.path)) || + // versions dont match + oldInfo.version !== version || + // Referenced files changed + !hasSameKeys(newReferences, (oldReferences = oldState.referencedMap && oldState.referencedMap.get(sourceFile.path))) || + // Referenced file was deleted + newReferences && forEachEntry(newReferences, (_value, path) => oldState.fileInfos.has(path) && !newProgram.getSourceFileByPath(path as Path))) { + changedFilesSet.set(sourceFile.path, true); + // All changed files need to re-evaluate its semantic diagnostics + semanticDiagnosticsPerFile.delete(sourceFile.path); + } + + newReferences && referencedMap.set(sourceFile.path, newReferences); + fileInfos.set(sourceFile.path, { version, signature: oldInfo && oldInfo.signature }); + } + + // For removed files, remove the semantic diagnostics removed files as changed + useOldState && oldState.fileInfos.forEach((_value, path) => !fileInfos.has(path) && semanticDiagnosticsPerFile.delete(path)); + + // Set the old state and program to undefined to ensure we arent keeping them alive hence forward + oldState = undefined; + newProgram = undefined; + return { - updateProgram, + fileInfos, + isModuleEmit, + referencedMap, + changedFilesSet, + semanticDiagnosticsPerFile, + canCreateNewStateFrom, getFilesAffectedBy, - emitChangedFiles, - getSemanticDiagnostics, - clear + emitNextAffectedFile, + getSemanticDiagnostics }; - function createProgramGraph(program: Program) { - const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None; - if (isModuleEmit !== currentIsModuleEmit) { - isModuleEmit = currentIsModuleEmit; - emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler(); - fileInfos.clear(); - semanticDiagnosticsPerFile.clear(); - } - hasShapeChanged.clear(); - allFilesExcludingDefaultLibraryFile = undefined; - mutateMap( - fileInfos, - arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path), - { - // Add new file info - createNewValue: (_path, sourceFile) => addNewFileInfo(program, sourceFile), - // Remove existing file info - onDeleteValue: removeExistingFileInfo, - // We will update in place instead of deleting existing value and adding new one - onExistingValue: (existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile) - } - ); + /** + * Can use this state as old State if we have iterated through all affected files present + */ + function canCreateNewStateFrom() { + return !affectedFiles || affectedFiles.length <= affectedFilesIndex; } - function registerChangedFile(path: Path) { - changedFilesSet.set(path, true); - // All changed files need to re-evaluate its semantic diagnostics - semanticDiagnosticsPerFile.delete(path); - } - - function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo { - registerChangedFile(sourceFile.path); - emitHandler.onAddSourceFile(program, sourceFile); - return { version: sourceFile.version, signature: undefined }; - } - - function removeExistingFileInfo(_existingFileInfo: FileInfo, path: Path) { - // Since we dont need to track removed file as changed file - // We can just remove its diagnostics - changedFilesSet.delete(path); - semanticDiagnosticsPerFile.delete(path); - emitHandler.onRemoveSourceFile(path); - } - - function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) { - if (existingInfo.version !== sourceFile.version) { - registerChangedFile(sourceFile.path); - existingInfo.version = sourceFile.version; - emitHandler.onUpdateSourceFile(program, sourceFile); - } - else if (emitHandler.onUpdateSourceFileWithSameVersion(program, sourceFile)) { - registerChangedFile(sourceFile.path); - } - } - - function ensureProgramGraph(program: Program) { - if (!emitHandler) { - createProgramGraph(program); - } - } - - function updateProgram(newProgram: Program) { - if (emitHandler) { - createProgramGraph(newProgram); - } - } - - function getFilesAffectedBy(program: Program, path: Path): ReadonlyArray { - ensureProgramGraph(program); - - const sourceFile = program.getSourceFileByPath(path); + /** + * Gets the files affected by the path from the program + */ + function getFilesAffectedBy(programOfThisState: Program, path: Path): ReadonlyArray { + const sourceFile = programOfThisState.getSourceFileByPath(path); if (!sourceFile) { return emptyArray; } - if (!updateShapeSignature(program, sourceFile)) { + if (!updateShapeSignature(programOfThisState, sourceFile)) { return [sourceFile]; } - return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile); + + return getEmitDependentFilesAffectedBy(programOfThisState, sourceFile); } - function emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray { - ensureProgramGraph(program); - const compilerOptions = program.getCompilerOptions(); + /** + * Emits the next affected file, and returns the EmitResult along with source files emitted + * Returns undefined when iteration is complete + */ + function emitNextAffectedFile(programOfThisState: Program, writeFileCallback: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileEmitResult | undefined { + if (affectedFiles) { + while (affectedFilesIndex < affectedFiles.length) { + const affectedFile = affectedFiles[affectedFilesIndex]; + affectedFilesIndex++; + if (!seenAffectedFiles.has(affectedFile.path)) { + seenAffectedFiles.set(affectedFile.path, true); - if (!changedFilesSet.size) { - return emptyArray; + // Emit the affected file + const result = programOfThisState.emit(affectedFile, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers) as AffectedFileEmitResult; + result.affectedFile = affectedFile; + return result; + } + } + + affectedFiles = undefined; } + // Get next changed file + const nextKey = changedFilesSet.keys().next(); + if (nextKey.done) { + // Done + return undefined; + } + + const compilerOptions = programOfThisState.getCompilerOptions(); // With --out or --outFile all outputs go into single file, do it only once if (compilerOptions.outFile || compilerOptions.out) { Debug.assert(semanticDiagnosticsPerFile.size === 0); changedFilesSet.clear(); - return [program.emit(/*targetSourceFile*/ undefined, writeFileCallback)]; + return programOfThisState.emit(/*targetSourceFile*/ undefined, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers); } - const seenFiles = createMap(); - let result: EmitResult[] | undefined; - changedFilesSet.forEach((_true, path) => { - // Get the affected Files by this program - const affectedFiles = getFilesAffectedBy(program, path as Path); - affectedFiles.forEach(affectedFile => { - // Affected files shouldnt have cached diagnostics - semanticDiagnosticsPerFile.delete(affectedFile.path); + // Get next batch of affected files + changedFilesSet.delete(nextKey.value); + affectedFilesIndex = 0; + affectedFiles = getFilesAffectedBy(programOfThisState, nextKey.value as Path); - if (!seenFiles.has(affectedFile.path)) { - seenFiles.set(affectedFile.path, true); + // Clear the semantic diagnostic of affected files + affectedFiles.forEach(affectedFile => semanticDiagnosticsPerFile.delete(affectedFile.path)); - // Emit the affected file - (result || (result = [])).push(program.emit(affectedFile, writeFileCallback)); - } - }); - }); - changedFilesSet.clear(); - return result || emptyArray; + return emitNextAffectedFile(programOfThisState, writeFileCallback, cancellationToken, customTransformers); } - function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): ReadonlyArray { - ensureProgramGraph(program); - Debug.assert(changedFilesSet.size === 0); - - const compilerOptions = program.getCompilerOptions(); + /** + * 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, the file has been taken out of affected files + */ + function getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { + const compilerOptions = programOfThisState.getCompilerOptions(); if (compilerOptions.outFile || compilerOptions.out) { Debug.assert(semanticDiagnosticsPerFile.size === 0); // We dont need to cache the diagnostics just return them from program - return program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); + return programOfThisState.getSemanticDiagnostics(sourceFile, cancellationToken); + } + + if (sourceFile) { + return getSemanticDiagnosticsOfFile(programOfThisState, sourceFile, cancellationToken); } let diagnostics: Diagnostic[]; - for (const sourceFile of program.getSourceFiles()) { - diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(program, sourceFile, cancellationToken)); + for (const sourceFile of programOfThisState.getSourceFiles()) { + diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(programOfThisState, sourceFile, cancellationToken)); } return diagnostics || emptyArray; } + /** + * Gets the semantic diagnostics either from cache if present, or otherwise from program and caches it + * Note that it is assumed that the when asked about semantic diagnostics, the file has been taken out of affected files/changed file set + */ function getSemanticDiagnosticsOfFile(program: Program, sourceFile: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { const path = sourceFile.path; const cachedDiagnostics = semanticDiagnosticsPerFile.get(path); @@ -247,15 +302,6 @@ namespace ts { return diagnostics; } - function clear() { - isModuleEmit = undefined; - emitHandler = undefined; - fileInfos.clear(); - semanticDiagnosticsPerFile.clear(); - changedFilesSet.clear(); - hasShapeChanged.clear(); - } - /** * 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, @@ -271,9 +317,10 @@ namespace ts { return true; } - /** - * @return {boolean} indicates if the shape signature has changed since last update. - */ + /** + * 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) { Debug.assert(!!sourceFile); @@ -360,6 +407,15 @@ namespace ts { } } + /** + * 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 */ @@ -386,126 +442,53 @@ namespace ts { } } - function getNonModuleEmitHandler(): EmitHandler { - return { - onAddSourceFile: noop, - onRemoveSourceFile: noop, - onUpdateSourceFile: noop, - onUpdateSourceFileWithSameVersion: returnFalse, - getFilesAffectedByUpdatedShape - }; - - function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray { - const options = program.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 (options && (options.out || options.outFile)) { - return [sourceFile]; - } - return getAllFilesExcludingDefaultLibraryFile(program, 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); } - function getModuleEmitHandler(): EmitHandler { - const references = createMap>(); - return { - onAddSourceFile: setReferences, - onRemoveSourceFile, - onUpdateSourceFile: updateReferences, - onUpdateSourceFileWithSameVersion: updateReferencesTrackingChangedReferences, - getFilesAffectedByUpdatedShape - }; - - function setReferences(program: Program, sourceFile: SourceFile) { - const newReferences = getReferencedFiles(program, sourceFile); - if (newReferences) { - references.set(sourceFile.path, newReferences); - } + /** + * When program emits modular code, gets the files affected by the sourceFile whose shape has changed + */ + function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile) { + if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) { + return getAllFilesExcludingDefaultLibraryFile(programOfThisState, sourceFileWithUpdatedShape); } - function updateReferences(program: Program, sourceFile: SourceFile) { - const newReferences = getReferencedFiles(program, sourceFile); - if (newReferences) { - references.set(sourceFile.path, newReferences); - } - else { - references.delete(sourceFile.path); - } + const compilerOptions = programOfThisState.getCompilerOptions(); + if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { + return [sourceFileWithUpdatedShape]; } - function updateReferencesTrackingChangedReferences(program: Program, sourceFile: SourceFile) { - const newReferences = getReferencedFiles(program, sourceFile); - if (!newReferences) { - // Changed if we had references - return references.delete(sourceFile.path); - } + // 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(); - const oldReferences = references.get(sourceFile.path); - references.set(sourceFile.path, newReferences); - if (!oldReferences || oldReferences.size !== newReferences.size) { - return true; - } - - // If there are any new references that werent present previously there is change - return forEachEntry(newReferences, (_true, referencedPath) => !oldReferences.delete(referencedPath)) || - // Otherwise its changed if there are more references previously than now - !!oldReferences.size; - } - - function onRemoveSourceFile(removedFilePath: Path) { - // Remove existing references - references.forEach((referencesInFile, filePath) => { - if (referencesInFile.has(removedFilePath)) { - // add files referencing the removedFilePath, as changed files too - const referencedByInfo = fileInfos.get(filePath); - if (referencedByInfo) { - registerChangedFile(filePath as Path); - } - } - }); - // Delete the entry for the removed file path - references.delete(removedFilePath); - } - - function getReferencedByPaths(referencedFilePath: Path) { - return mapDefinedIter(references.entries(), ([filePath, referencesInFile]) => - referencesInFile.has(referencedFilePath) ? filePath as Path : undefined - ); - } - - function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray { - if (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile)) { - return getAllFilesExcludingDefaultLibraryFile(program, sourceFile); - } - - const compilerOptions = program.getCompilerOptions(); - if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { - return [sourceFile]; - } - - // 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 - const path = sourceFile.path; - seenFileNamesMap.set(path, sourceFile); - const queue = getReferencedByPaths(path); - while (queue.length > 0) { - const currentPath = queue.pop(); - if (!seenFileNamesMap.has(currentPath)) { - const currentSourceFile = program.getSourceFileByPath(currentPath); - seenFileNamesMap.set(currentPath, currentSourceFile); - if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) { - queue.push(...getReferencedByPaths(currentPath)); - } + // 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)) { + queue.push(...getReferencedByPaths(currentPath)); } } - - // Return array of values that needs emit - return flatMapIter(seenFileNamesMap.values(), value => value); } + + // Return array of values that needs emit + return flatMapIter(seenFileNamesMap.values(), value => value); } } } diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 01fb45e4f7d..ab1e5cc566e 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -155,8 +155,8 @@ namespace ts { const watchingHost = ts.createWatchingSystemHost(/*pretty*/ undefined, sys, parseConfigFile, reportDiagnostic, reportWatchDiagnostic); watchingHost.beforeCompile = enableStatistics; const afterCompile = watchingHost.afterCompile; - watchingHost.afterCompile = (host, program, builder) => { - afterCompile(host, program, builder); + watchingHost.afterCompile = (host, program) => { + afterCompile(host, program); reportStatistics(program); }; return watchingHost; diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 8e49bb71a15..96605f7b6b9 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -2,7 +2,6 @@ /// /// -/* @internal */ namespace ts { export type DiagnosticReporter = (diagnostic: Diagnostic) => void; export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: DirectoryStructureHost, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; @@ -19,7 +18,7 @@ namespace ts { // Callbacks to do custom action before creating program and after creating program beforeCompile(compilerOptions: CompilerOptions): void; - afterCompile(host: DirectoryStructureHost, program: Program, builder: Builder): void; + afterCompile(host: DirectoryStructureHost, program: Program): void; } const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? { @@ -133,6 +132,11 @@ namespace ts { reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system, pretty ? reportDiagnosticWithColorAndContext : reportDiagnosticSimply); reportWatchDiagnostic = reportWatchDiagnostic || createWatchDiagnosticReporter(system); parseConfigFile = parseConfigFile || ts.parseConfigFile; + let builderState: Readonly | undefined; + const options: BuilderOptions = { + getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), + computeHash: data => system.createHash ? system.createHash(data) : data + }; return { system, parseConfigFile, @@ -142,7 +146,9 @@ namespace ts { afterCompile: compileWatchedProgram, }; - function compileWatchedProgram(host: DirectoryStructureHost, program: Program, builder: Builder) { + function compileWatchedProgram(host: DirectoryStructureHost, program: Program) { + builderState = createBuilderState(program, options, builderState); + // First get and report any syntactic errors. const diagnostics = program.getSyntacticDiagnostics().slice(); let reportSemanticDiagnostics = false; @@ -163,22 +169,15 @@ namespace ts { let sourceMaps: SourceMapData[]; let emitSkipped: boolean; - const result = builder.emitChangedFiles(program, writeFile); - if (result.length === 0) { - emitSkipped = true; - } - else { - for (const emitOutput of result) { - if (emitOutput.emitSkipped) { - emitSkipped = true; - } - addRange(diagnostics, emitOutput.diagnostics); - sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); - } + let affectedEmitResult: AffectedFileEmitResult; + while (affectedEmitResult = builderState.emitNextAffectedFile(program, writeFile)) { + emitSkipped = emitSkipped || affectedEmitResult.emitSkipped; + addRange(diagnostics, affectedEmitResult.diagnostics); + sourceMaps = addRange(sourceMaps, affectedEmitResult.sourceMaps); } if (reportSemanticDiagnostics) { - addRange(diagnostics, builder.getSemanticDiagnostics(program)); + addRange(diagnostics, builderState.getSemanticDiagnostics(program)); } return handleEmitOutputAndReportErrors(host, program, emittedFiles, emitSkipped, diagnostics, reportDiagnostic); @@ -299,8 +298,6 @@ namespace ts { getDirectoryPath(getNormalizedAbsolutePath(configFileName, getCurrentDirectory())) : getCurrentDirectory() ); - // There is no extra check needed since we can just rely on the program to decide emit - const builder = createBuilder({ getCanonicalFileName, computeHash }); synchronizeProgram(); @@ -334,7 +331,6 @@ namespace ts { compilerHost.hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; program = createProgram(rootFileNames, compilerOptions, compilerHost, program); resolutionCache.finishCachingPerDirectoryResolution(); - builder.updateProgram(program); // Update watches updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath); @@ -356,7 +352,7 @@ namespace ts { missingFilePathsRequestedForRelease = undefined; } - afterCompile(directoryStructureHost, program, builder); + afterCompile(directoryStructureHost, program); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); } @@ -640,9 +636,5 @@ namespace ts { flags ); } - - function computeHash(data: string) { - return system.createHash ? system.createHash(data) : data; - } } } diff --git a/src/harness/unittests/builder.ts b/src/harness/unittests/builder.ts index bfdeb1a4067..60297d2b7fc 100644 --- a/src/harness/unittests/builder.ts +++ b/src/harness/unittests/builder.ts @@ -44,15 +44,16 @@ namespace ts { }); function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { - const builder = createBuilder({ + let builderState: BuilderState; + const builderOptions: BuilderOptions = { getCanonicalFileName: identity, computeHash: identity - }); + }; return fileNames => { const program = getProgram(); - builder.updateProgram(program); + builderState = createBuilderState(program, builderOptions, builderState); const outputFileNames: string[] = []; - builder.emitChangedFiles(program, fileName => outputFileNames.push(fileName)); + while (builderState.emitNextAffectedFile(program, fileName => outputFileNames.push(fileName))) { } assert.deepEqual(outputFileNames, fileNames); }; } diff --git a/src/server/project.ts b/src/server/project.ts index ce750c78e23..056fedf19af 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -139,7 +139,7 @@ namespace ts.server { /*@internal*/ resolutionCache: ResolutionCache; - private builder: Builder; + private builderState: BuilderState; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ @@ -442,15 +442,6 @@ namespace ts.server { return this.languageService; } - private ensureBuilder() { - if (!this.builder) { - this.builder = createBuilder({ - getCanonicalFileName: this.projectService.toCanonicalFileName, - computeHash: data => this.projectService.host.createHash(data) - }); - } - } - private shouldEmitFile(scriptInfo: ScriptInfo) { return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent(); } @@ -460,8 +451,11 @@ namespace ts.server { return []; } this.updateGraph(); - this.ensureBuilder(); - return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path), + this.builderState = createBuilderState(this.program, { + getCanonicalFileName: this.projectService.toCanonicalFileName, + computeHash: data => this.projectService.host.createHash(data) + }, this.builderState); + return mapDefined(this.builderState.getFilesAffectedBy(this.program, scriptInfo.path), sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); } @@ -497,6 +491,7 @@ namespace ts.server { } this.languageService.cleanupSemanticCache(); this.languageServiceEnabled = false; + this.builderState = undefined; this.resolutionCache.closeTypeRootsWatch(); this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false); } @@ -537,7 +532,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; @@ -787,15 +782,9 @@ namespace ts.server { if (this.setTypings(cachedTypings)) { hasChanges = this.updateGraphWorker() || hasChanges; } - if (this.builder) { - this.builder.updateProgram(this.program); - } } else { this.lastCachedUnresolvedImportsList = undefined; - if (this.builder) { - this.builder.clear(); - } } if (hasChanges) { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 231d7bae3f2..3ec16254780 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3771,6 +3771,70 @@ declare namespace ts { writeByteOrderMark: boolean; text: string; } + /** + * State on which you can query affected files (files to save) and get semantic diagnostics(with their cache managed in the object) + * Note that it is only safe to pass BuilderState as old state when creating new state, when + * - If iterator's next method to get next affected file is never called + * - Iteration of single changed file and its dependencies (iteration through all of its affected files) is complete + */ + interface BuilderState { + /** + * The map of file infos, where there is entry for each file in the program + * The entry is signature of the file (from last emit) or empty string + */ + fileInfos: ReadonlyMap>; + /** + * Returns true if module gerneration is not ModuleKind.None + */ + isModuleEmit: boolean; + /** + * Map of file referenced or undefined if it wasnt module emit + * The entry is present only if file references other files + * The key is path of file and value is referenced map for that file (for every file referenced, there is entry in the set) + */ + referencedMap: ReadonlyMap | undefined; + /** + * Set of source file's paths that have been changed, either in resolution or versions + */ + changedFilesSet: ReadonlyMap; + /** + * Set of cached semantic diagnostics per file + */ + semanticDiagnosticsPerFile: ReadonlyMap>; + /** + * Returns true if this state is safe to use as oldState + */ + canCreateNewStateFrom(): boolean; + /** + * 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): AffectedFileEmitResult | undefined; + /** + * 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, the file has been taken out of affected files + */ + getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + } + /** + * Information about the source file: Its version and optional signature from last emit + */ + interface FileInfo { + version: string; + signature: string; + } + interface AffectedFileEmitResult extends EmitResult { + affectedFile?: SourceFile; + } + /** + * Referenced files with values for the keys as referenced file's path to be true + */ + type ReferencedSet = ReadonlyMap; + interface BuilderOptions { + getCanonicalFileName: (fileName: string) => string; + computeHash: (data: string) => string; + } + function createBuilderState(newProgram: Program, options: BuilderOptions, oldState?: Readonly): BuilderState; } declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; @@ -7210,7 +7274,7 @@ declare namespace ts.server { languageServiceEnabled: boolean; readonly trace?: (s: string) => void; readonly realpath?: (path: string) => string; - private builder; + private builderState; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ @@ -7271,7 +7335,6 @@ declare namespace ts.server { getGlobalProjectErrors(): ReadonlyArray; getAllProjectErrors(): ReadonlyArray; getLanguageService(ensureSynchronized?: boolean): LanguageService; - private ensureBuilder(); private shouldEmitFile(scriptInfo); getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[]; /** diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 6b2db71b140..5dec286a8b7 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3718,6 +3718,70 @@ declare namespace ts { writeByteOrderMark: boolean; text: string; } + /** + * State on which you can query affected files (files to save) and get semantic diagnostics(with their cache managed in the object) + * Note that it is only safe to pass BuilderState as old state when creating new state, when + * - If iterator's next method to get next affected file is never called + * - Iteration of single changed file and its dependencies (iteration through all of its affected files) is complete + */ + interface BuilderState { + /** + * The map of file infos, where there is entry for each file in the program + * The entry is signature of the file (from last emit) or empty string + */ + fileInfos: ReadonlyMap>; + /** + * Returns true if module gerneration is not ModuleKind.None + */ + isModuleEmit: boolean; + /** + * Map of file referenced or undefined if it wasnt module emit + * The entry is present only if file references other files + * The key is path of file and value is referenced map for that file (for every file referenced, there is entry in the set) + */ + referencedMap: ReadonlyMap | undefined; + /** + * Set of source file's paths that have been changed, either in resolution or versions + */ + changedFilesSet: ReadonlyMap; + /** + * Set of cached semantic diagnostics per file + */ + semanticDiagnosticsPerFile: ReadonlyMap>; + /** + * Returns true if this state is safe to use as oldState + */ + canCreateNewStateFrom(): boolean; + /** + * 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): AffectedFileEmitResult | undefined; + /** + * 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, the file has been taken out of affected files + */ + getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + } + /** + * Information about the source file: Its version and optional signature from last emit + */ + interface FileInfo { + version: string; + signature: string; + } + interface AffectedFileEmitResult extends EmitResult { + affectedFile?: SourceFile; + } + /** + * Referenced files with values for the keys as referenced file's path to be true + */ + type ReferencedSet = ReadonlyMap; + interface BuilderOptions { + getCanonicalFileName: (fileName: string) => string; + computeHash: (data: string) => string; + } + function createBuilderState(newProgram: Program, options: BuilderOptions, oldState?: Readonly): BuilderState; } declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string;