From 85ce1d0398e252192b1826b9c2818a091b783f3e Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 14 Nov 2017 14:47:58 -0800 Subject: [PATCH] Make the builder state as internal and expose builder instead of builder state --- src/compiler/builder.ts | 627 ++++++++++++------ src/compiler/watch.ts | 15 +- src/harness/unittests/builder.ts | 9 +- src/server/project.ts | 19 +- .../reference/api/tsserverlibrary.d.ts | 85 ++- tests/baselines/reference/api/typescript.d.ts | 85 ++- 6 files changed, 521 insertions(+), 319 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index efa0f0a0372..8703d83aa1d 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -1,29 +1,56 @@ /// +/*@internal*/ namespace ts { - export interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - - export interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } - - /* @internal */ 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 }; + 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): ReadonlyArray; + } + + /** + * Create the internal builder to get files affected by sourceFile + */ + export function createInternalBuilder(options: BuilderOptions): InternalBuilder { + return createBuilder(options, BuilderType.InternalBuilder); + } + + export enum BuilderType { + InternalBuilder, + SemanticDiagnosticsBuilder, + EmitAndSemanticDiagnosticsBuilder + } + + /** + * 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; @@ -35,191 +62,273 @@ namespace ts { return map1.size === map2.size && !forEachEntry(map1, (_value, key) => !map2.has(key)); } - /** - * 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 { + export function createBuilder(options: BuilderOptions, builderType: BuilderType.InternalBuilder): InternalBuilder; + export function createBuilder(options: BuilderOptions, builderType: BuilderType.SemanticDiagnosticsBuilder): SemanticDiagnosticsBuilder; + export function createBuilder(options: BuilderOptions, builderType: BuilderType.EmitAndSemanticDiagnosticsBuilder): EmitAndSemanticDiagnosticsBuilder; + export function createBuilder(options: BuilderOptions, builderType: BuilderType) { /** - * 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 + * Information of the file eg. its version, signature etc */ - 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; - - /** - * 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; - - /** - * Get all the dependencies of the file - */ - getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): string[]; - } - - /** - * 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 createBuilderState(newProgram: Program, options: BuilderOptions, oldState?: Readonly): BuilderState { const fileInfos = createMap(); - const isModuleEmit = newProgram.getCompilerOptions().module !== ModuleKind.None; - const referencedMap = isModuleEmit ? createMap() : undefined; + /** + * 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 | undefined) => ReadonlyArray; + + /** + * Cache of semantic diagnostics for files with their Path being the key + */ const semanticDiagnosticsPerFile = createMap>(); - /** The map has key by source file's path that has been changed */ + + /** + * The map has key by source file's path that has been changed + */ const changedFilesSet = createMap(); - const hasShapeChanged = 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; - // Iterator datas + /** + * Set of affected files being iterated + */ let affectedFiles: ReadonlyArray | undefined; + /** + * Current index to retrieve affected file from + */ let affectedFilesIndex = 0; + /** + * Current changed file for iterating over affected files + */ + let currentChangedFilePath: Path | undefined; + /** + * Map of file signatures, with key being file path, calculated while getting current changed file's affected files + * These will be commited whenever the iteration through affected files of current changed file is complete + */ + const currentAffectedFilesSignatures = createMap(); + /** + * Already seen affected files + */ 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); + switch (builderType) { + case BuilderType.InternalBuilder: + return getInternalBuilder(); + case BuilderType.SemanticDiagnosticsBuilder: + return getSemanticDiagnosticsBuilder(); + case BuilderType.EmitAndSemanticDiagnosticsBuilder: + return getEmitAndSemanticDiagnosticsBuilder(); + default: + notImplemented(); } - 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); - } - - if (newReferences) { - referencedMap.set(sourceFile.path, newReferences); - } - fileInfos.set(sourceFile.path, { version, signature: oldInfo && oldInfo.signature }); + function getInternalBuilder(): InternalBuilder { + return { + updateProgram, + getFilesAffectedBy, + getAllDependencies + }; } - // For removed files, remove the semantic diagnostics removed files as changed - if (useOldState) { - oldState.fileInfos.forEach((_value, path) => !fileInfos.has(path) && semanticDiagnosticsPerFile.delete(path)); + function getSemanticDiagnosticsBuilder(): SemanticDiagnosticsBuilder { + return { + updateProgram, + getAllDependencies, + getSemanticDiagnosticsOfNextAffectedFile, + getSemanticDiagnostics + }; } - // Set the old state and program to undefined to ensure we arent keeping them alive hence forward - oldState = undefined; - newProgram = undefined; - - return { - fileInfos, - isModuleEmit, - referencedMap, - changedFilesSet, - semanticDiagnosticsPerFile, - canCreateNewStateFrom, - getFilesAffectedBy, - emitNextAffectedFile, - getSemanticDiagnostics, - getAllDependencies - }; + function getEmitAndSemanticDiagnosticsBuilder(): EmitAndSemanticDiagnosticsBuilder { + return { + updateProgram, + getAllDependencies, + emitNextAffectedFile, + getSemanticDiagnostics + }; + } /** - * Can use this state as old State if we have iterated through all affected files present + * Update current state to reflect new program + * Updates changed files, references, file infos etc */ - function canCreateNewStateFrom() { - return !affectedFiles || affectedFiles.length <= affectedFilesIndex; + function updateProgram(newProgram: Program) { + const newProgramHasModuleEmit = newProgram.getCompilerOptions().module !== ModuleKind.None; + const oldReferencedMap = referencedMap; + if (isModuleEmit !== newProgramHasModuleEmit) { + // 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 + changedFilesSet.clear(); + 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 = undefined; + currentAffectedFilesSignatures.clear(); + } + else { + // 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); + } + } + }); + } } /** * Gets the files affected by the path from the program */ - function getFilesAffectedBy(programOfThisState: Program, path: Path): ReadonlyArray { + function getFilesAffectedBy(programOfThisState: Program, path: Path, cacheToUpdateSignature?: Map): ReadonlyArray { const sourceFile = programOfThisState.getSourceFileByPath(path); if (!sourceFile) { return emptyArray; } - if (!updateShapeSignature(programOfThisState, sourceFile)) { + if (!updateShapeSignature(programOfThisState, sourceFile, cacheToUpdateSignature)) { return [sourceFile]; } - return getEmitDependentFilesAffectedBy(programOfThisState, sourceFile); + return getEmitDependentFilesAffectedBy(programOfThisState, sourceFile, cacheToUpdateSignature); + } + + function getNextAffectedFile(programOfThisState: Program): SourceFile | Program | undefined { + while (true) { + if (affectedFiles) { + while (affectedFilesIndex < affectedFiles.length) { + const affectedFile = affectedFiles[affectedFilesIndex]; + affectedFilesIndex++; + if (!seenAffectedFiles.has(affectedFile.path)) { + // Set the next affected file as seen and remove the cached semantic diagnostics + seenAffectedFiles.set(affectedFile.path, true); + semanticDiagnosticsPerFile.delete(affectedFile.path); + return affectedFile; + } + } + + // Remove the changed file from the change set + changedFilesSet.delete(currentChangedFilePath); + currentChangedFilePath = undefined; + // Commit the changes in file signature + currentAffectedFilesSignatures.forEach((signature, path) => fileInfos.get(path).signature = signature); + currentAffectedFilesSignatures.clear(); + 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 + // so operations are performed directly on program, return program + if (compilerOptions.outFile || compilerOptions.out) { + Debug.assert(semanticDiagnosticsPerFile.size === 0); + changedFilesSet.clear(); + return programOfThisState; + } + + // Get next batch of affected files + currentChangedFilePath = nextKey.value as Path; + affectedFilesIndex = 0; + affectedFiles = getFilesAffectedBy(programOfThisState, nextKey.value as Path, currentAffectedFilesSignatures); + } } /** @@ -227,47 +336,48 @@ namespace ts { * 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); - - // 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) { + const affectedFile = getNextAffectedFile(programOfThisState); + if (!affectedFile) { // 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(); + else if (affectedFile === programOfThisState) { + // When whole program is affected, do emit only once (eg when --out or --outFile is specified) return programOfThisState.emit(/*targetSourceFile*/ undefined, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers); } - // Get next batch of affected files - changedFilesSet.delete(nextKey.value); - affectedFilesIndex = 0; - affectedFiles = getFilesAffectedBy(programOfThisState, nextKey.value as Path); + // Emit the affected file + const targetSourceFile = affectedFile as SourceFile; + const result = programOfThisState.emit(targetSourceFile, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers) as AffectedFileEmitResult; + result.affectedFile = targetSourceFile; + return result; + } - // Clear the semantic diagnostic of affected files - affectedFiles.forEach(affectedFile => semanticDiagnosticsPerFile.delete(affectedFile.path)); + /** + * Return the semantic diagnostics for the next affected file or undefined if iteration is complete + * If provided ignoreSourceFile would be called before getting the diagnostics and would ignore the sourceFile if the returned value was true + */ + function getSemanticDiagnosticsOfNextAffectedFile(programOfThisState: Program, cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): ReadonlyArray { + while (true) { + const affectedFile = getNextAffectedFile(programOfThisState); + if (!affectedFile) { + // Done + return undefined; + } + else if (affectedFile === programOfThisState) { + // When whole program is affected, get all semantic diagnostics (eg when --out or --outFile is specified) + return programOfThisState.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken); + } - return emitNextAffectedFile(programOfThisState, writeFileCallback, cancellationToken, customTransformers); + // Get diagnostics for the affected file if its not ignored + const targetSourceFile = affectedFile as SourceFile; + if (ignoreSourceFile && ignoreSourceFile(targetSourceFile)) { + // Get next affected file + continue; + } + + return getSemanticDiagnosticsOfFile(programOfThisState, targetSourceFile, cancellationToken); + } } /** @@ -276,6 +386,7 @@ namespace ts { * 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 { + Debug.assert(!affectedFiles || affectedFiles[affectedFilesIndex - 1] !== sourceFile || !semanticDiagnosticsPerFile.has(sourceFile.path)); const compilerOptions = programOfThisState.getCompilerOptions(); if (compilerOptions.outFile || compilerOptions.out) { Debug.assert(semanticDiagnosticsPerFile.size === 0); @@ -373,19 +484,20 @@ namespace ts { 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) { + /** + * 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 | 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 (hasShapeChanged.has(sourceFile.path)) { + if (hasCalledUpdateShapeSignature.has(sourceFile.path)) { return false; } - hasShapeChanged.set(sourceFile.path, true); + Debug.assert(!cacheToUpdateSignature || !cacheToUpdateSignature.has(sourceFile.path)); + hasCalledUpdateShapeSignature.set(sourceFile.path, true); const info = fileInfos.get(sourceFile.path); Debug.assert(!!info); @@ -393,13 +505,13 @@ namespace ts { let latestSignature: string; if (sourceFile.isDeclarationFile) { latestSignature = sourceFile.version; - info.signature = latestSignature; + setLatestSigature(); } else { const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { latestSignature = options.computeHash(emitOutput.outputFiles[0].text); - info.signature = latestSignature; + setLatestSigature(); } else { latestSignature = prevSignature; @@ -407,6 +519,15 @@ namespace ts { } return !prevSignature || latestSignature !== prevSignature; + + function setLatestSigature() { + if (cacheToUpdateSignature) { + cacheToUpdateSignature.set(sourceFile.path, latestSignature); + } + else { + info.signature = latestSignature; + } + } } /** @@ -514,7 +635,7 @@ namespace ts { /** * When program emits modular code, gets the files affected by the sourceFile whose shape has changed */ - function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile) { + function getFilesAffectedByUpdatedShapeWhenModuleEmit(programOfThisState: Program, sourceFileWithUpdatedShape: SourceFile, cacheToUpdateSignature: Map | undefined) { if (!isExternalModule(sourceFileWithUpdatedShape) && !containsOnlyAmbientModules(sourceFileWithUpdatedShape)) { return getAllFilesExcludingDefaultLibraryFile(programOfThisState, sourceFileWithUpdatedShape); } @@ -537,7 +658,7 @@ namespace ts { if (!seenFileNamesMap.has(currentPath)) { const currentSourceFile = programOfThisState.getSourceFileByPath(currentPath); seenFileNamesMap.set(currentPath, currentSourceFile); - if (currentSourceFile && updateShapeSignature(programOfThisState, currentSourceFile)) { + if (currentSourceFile && updateShapeSignature(programOfThisState, currentSourceFile, cacheToUpdateSignature)) { queue.push(...getReferencedByPaths(currentPath)); } } @@ -548,3 +669,93 @@ namespace ts { } } } + +namespace ts { + export interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + + export interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } + + export interface AffectedFileEmitResult extends EmitResult { + affectedFile?: SourceFile; + } + + export interface BuilderOptions { + getCanonicalFileName: (fileName: string) => string; + computeHash: (data: string) => string; + } + + /** + * Builder to manage the program state changes + */ + export 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 + */ + export 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): ReadonlyArray; + + /** + * 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 + */ + export 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): 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 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 + */ + export function createSemanticDiagnosticsBuilder(options: BuilderOptions): SemanticDiagnosticsBuilder { + return createBuilder(options, BuilderType.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 + */ + export function createEmitAndSemanticDiagnosticsBuilder(options: BuilderOptions): EmitAndSemanticDiagnosticsBuilder { + return createBuilder(options, BuilderType.EmitAndSemanticDiagnosticsBuilder); + } +} diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 07fb9203b6a..fcb4d98ace0 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -84,18 +84,17 @@ namespace ts { } /** - * Creates the function that compiles the program by maintaining the builder state and also return diagnostic reporter + * Creates the function that compiles the program by maintaining the builder for the program and reports the errors and emits files */ export function createProgramCompilerWithBuilderState(system = sys, reportDiagnostic?: DiagnosticReporter) { reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system); - let builderState: Readonly | undefined; - const options: BuilderOptions = { + const builder = createEmitAndSemanticDiagnosticsBuilder({ getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), - computeHash: data => system.createHash ? system.createHash(data) : data - }; + computeHash: system.createHash ? system.createHash.bind(system) : identity + }); return (host: DirectoryStructureHost, program: Program) => { - builderState = createBuilderState(program, options, builderState); + builder.updateProgram(program); // First get and report any syntactic errors. const diagnostics = program.getSyntacticDiagnostics().slice(); @@ -118,14 +117,14 @@ namespace ts { let emitSkipped: boolean; let affectedEmitResult: AffectedFileEmitResult; - while (affectedEmitResult = builderState.emitNextAffectedFile(program, writeFile)) { + while (affectedEmitResult = builder.emitNextAffectedFile(program, writeFile)) { emitSkipped = emitSkipped || affectedEmitResult.emitSkipped; addRange(diagnostics, affectedEmitResult.diagnostics); sourceMaps = addRange(sourceMaps, affectedEmitResult.sourceMaps); } if (reportSemanticDiagnostics) { - addRange(diagnostics, builderState.getSemanticDiagnostics(program)); + addRange(diagnostics, builder.getSemanticDiagnostics(program)); } sortAndDeduplicateDiagnostics(diagnostics).forEach(reportDiagnostic); diff --git a/src/harness/unittests/builder.ts b/src/harness/unittests/builder.ts index 89b0852bd32..a9c16c59fbd 100644 --- a/src/harness/unittests/builder.ts +++ b/src/harness/unittests/builder.ts @@ -44,17 +44,16 @@ namespace ts { }); function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { - let builderState: BuilderState; - const builderOptions: BuilderOptions = { + const builder = createEmitAndSemanticDiagnosticsBuilder({ getCanonicalFileName: identity, computeHash: identity - }; + }); return fileNames => { const program = getProgram(); - builderState = createBuilderState(program, builderOptions, builderState); + builder.updateProgram(program); const outputFileNames: string[] = []; // tslint:disable-next-line no-empty - while (builderState.emitNextAffectedFile(program, fileName => outputFileNames.push(fileName))) { + while (builder.emitNextAffectedFile(program, fileName => outputFileNames.push(fileName))) { } assert.deepEqual(outputFileNames, fileNames); }; diff --git a/src/server/project.ts b/src/server/project.ts index 056fedf19af..3a5785163d2 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -139,7 +139,7 @@ namespace ts.server { /*@internal*/ resolutionCache: ResolutionCache; - private builderState: BuilderState; + private builder: InternalBuilder | undefined; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ @@ -451,11 +451,14 @@ namespace ts.server { return []; } this.updateGraph(); - 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), + if (!this.builder) { + this.builder = createInternalBuilder({ + getCanonicalFileName: this.projectService.toCanonicalFileName, + computeHash: data => this.projectService.host.createHash(data) + }); + } + this.builder.updateProgram(this.program); + return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path), sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); } @@ -491,7 +494,7 @@ namespace ts.server { } this.languageService.cleanupSemanticCache(); this.languageServiceEnabled = false; - this.builderState = undefined; + this.builder = undefined; this.resolutionCache.closeTypeRootsWatch(); this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false); } @@ -532,7 +535,7 @@ namespace ts.server { this.rootFilesMap = undefined; this.externalFiles = undefined; this.program = undefined; - this.builderState = undefined; + this.builder = undefined; this.resolutionCache.clear(); this.resolutionCache = undefined; this.cachedUnresolvedImportsPerFile = undefined; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 5c306474533..e258d179310 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3771,40 +3771,48 @@ declare namespace ts { writeByteOrderMark: boolean; text: string; } + interface AffectedFileEmitResult extends EmitResult { + affectedFile?: SourceFile; + } + interface BuilderOptions { + getCanonicalFileName: (fileName: string) => string; + computeHash: (data: string) => 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 + * Builder to manage the program state changes */ - interface BuilderState { + interface BaseBuilder { /** - * 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 + * Updates the program in the builder to represent new state */ - fileInfos: ReadonlyMap>; + updateProgram(newProgram: Program): void; /** - * Returns true if module gerneration is not ModuleKind.None + * Get all the dependencies of the file */ - isModuleEmit: boolean; + 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 { /** - * 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) + * Gets the semantic diagnostics from the program for the next affected file and caches it + * Returns undefined if the iteration is complete */ - referencedMap: ReadonlyMap | undefined; + getSemanticDiagnosticsOfNextAffectedFile(programOfThisState: Program, cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): ReadonlyArray; /** - * Set of source file's paths that have been changed, either in resolution or versions + * 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 */ - changedFilesSet: ReadonlyMap; - /** - * Set of cached semantic diagnostics per file - */ - semanticDiagnosticsPerFile: ReadonlyMap>; - /** - * Returns true if this state is safe to use as oldState - */ - canCreateNewStateFrom(): boolean; + 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 */ @@ -3812,33 +3820,20 @@ declare namespace ts { /** * 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 + * 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; - /** - * Get all the dependencies of the file - */ - getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): string[]; } /** - * Information about the source file: Its version and optional signature from last emit + * Create the builder to manage semantic diagnostics and cache them */ - interface FileInfo { - version: string; - signature: string; - } - interface AffectedFileEmitResult extends EmitResult { - affectedFile?: SourceFile; - } + function createSemanticDiagnosticsBuilder(options: BuilderOptions): SemanticDiagnosticsBuilder; /** - * Referenced files with values for the keys as referenced file's path to be true + * 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 */ - type ReferencedSet = ReadonlyMap; - interface BuilderOptions { - getCanonicalFileName: (fileName: string) => string; - computeHash: (data: string) => string; - } - function createBuilderState(newProgram: Program, options: BuilderOptions, oldState?: Readonly): BuilderState; + function createEmitAndSemanticDiagnosticsBuilder(options: BuilderOptions): EmitAndSemanticDiagnosticsBuilder; } declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; @@ -7278,7 +7273,7 @@ declare namespace ts.server { languageServiceEnabled: boolean; readonly trace?: (s: string) => void; readonly realpath?: (path: string) => string; - private builderState; + private builder; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 2212a859c0a..bac53072302 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3718,40 +3718,48 @@ declare namespace ts { writeByteOrderMark: boolean; text: string; } + interface AffectedFileEmitResult extends EmitResult { + affectedFile?: SourceFile; + } + interface BuilderOptions { + getCanonicalFileName: (fileName: string) => string; + computeHash: (data: string) => 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 + * Builder to manage the program state changes */ - interface BuilderState { + interface BaseBuilder { /** - * 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 + * Updates the program in the builder to represent new state */ - fileInfos: ReadonlyMap>; + updateProgram(newProgram: Program): void; /** - * Returns true if module gerneration is not ModuleKind.None + * Get all the dependencies of the file */ - isModuleEmit: boolean; + 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 { /** - * 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) + * Gets the semantic diagnostics from the program for the next affected file and caches it + * Returns undefined if the iteration is complete */ - referencedMap: ReadonlyMap | undefined; + getSemanticDiagnosticsOfNextAffectedFile(programOfThisState: Program, cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): ReadonlyArray; /** - * Set of source file's paths that have been changed, either in resolution or versions + * 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 */ - changedFilesSet: ReadonlyMap; - /** - * Set of cached semantic diagnostics per file - */ - semanticDiagnosticsPerFile: ReadonlyMap>; - /** - * Returns true if this state is safe to use as oldState - */ - canCreateNewStateFrom(): boolean; + 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 */ @@ -3759,33 +3767,20 @@ declare namespace ts { /** * 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 + * 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; - /** - * Get all the dependencies of the file - */ - getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): string[]; } /** - * Information about the source file: Its version and optional signature from last emit + * Create the builder to manage semantic diagnostics and cache them */ - interface FileInfo { - version: string; - signature: string; - } - interface AffectedFileEmitResult extends EmitResult { - affectedFile?: SourceFile; - } + function createSemanticDiagnosticsBuilder(options: BuilderOptions): SemanticDiagnosticsBuilder; /** - * Referenced files with values for the keys as referenced file's path to be true + * 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 */ - type ReferencedSet = ReadonlyMap; - interface BuilderOptions { - getCanonicalFileName: (fileName: string) => string; - computeHash: (data: string) => string; - } - function createBuilderState(newProgram: Program, options: BuilderOptions, oldState?: Readonly): BuilderState; + function createEmitAndSemanticDiagnosticsBuilder(options: BuilderOptions): EmitAndSemanticDiagnosticsBuilder; } declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; @@ -3819,7 +3814,7 @@ declare namespace ts { declare namespace ts { type DiagnosticReporter = (diagnostic: Diagnostic) => void; /** - * Creates the function that compiles the program by maintaining the builder state and also return diagnostic reporter + * Creates the function that compiles the program by maintaining the builder for the program and reports the errors and emits files */ function createProgramCompilerWithBuilderState(system?: System, reportDiagnostic?: DiagnosticReporter): (host: DirectoryStructureHost, program: Program) => void; interface WatchHost {