diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 110bdd5130..21c3b9021e 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -2,12 +2,10 @@ /*@internal*/ namespace ts { - export enum BuilderKind { - BuilderKindSemanticDiagnostics, - BuilderKindEmitAndSemanticDiagnostics - } - - interface BuilderStateWithChangedFiles extends BuilderState { + /** + * State to store the changed files, affected files and cache semantic diagnostics + */ + export interface BuilderProgramState extends BuilderState { /** * Cache of semantic diagnostics for files with their Path being the key */ @@ -37,6 +35,10 @@ namespace ts { * Already seen affected files */ seenAffectedFiles: Map | undefined; + /** + * program corresponding to this state + */ + program: Program; } function hasSameKeys(map1: ReadonlyMap | undefined, map2: ReadonlyMap | undefined) { @@ -53,8 +55,9 @@ namespace ts { /** * Create the state so that we can iterate on changedFiles/affected files */ - function createBuilderStateWithChangedFiles(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderStateWithChangedFiles { - const state = BuilderState.create(newProgram, getCanonicalFileName, oldState) as BuilderStateWithChangedFiles; + function createBuilderProgramState(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState?: Readonly): BuilderProgramState { + const state = BuilderState.create(newProgram, getCanonicalFileName, oldState) as BuilderProgramState; + state.program = newProgram; const compilerOptions = newProgram.getCompilerOptions(); if (!compilerOptions.outFile && !compilerOptions.out) { state.semanticDiagnosticsPerFile = createMap>(); @@ -107,9 +110,119 @@ namespace ts { return state; } - export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindSemanticDiagnostics): SemanticDiagnosticsBuilder; - export function createBuilder(host: BuilderHost, builderKind: BuilderKind.BuilderKindEmitAndSemanticDiagnostics): EmitAndSemanticDiagnosticsBuilder; - export function createBuilder(host: BuilderHost, builderKind: BuilderKind) { + /** + * Verifies that source file is ok to be used in calls that arent handled by next + */ + function assertSourceFileOkWithoutNextAffectedCall(state: BuilderProgramState, sourceFile: SourceFile | undefined) { + Debug.assert(!sourceFile || !state.affectedFiles || state.affectedFiles[state.affectedFilesIndex - 1] !== sourceFile || !state.semanticDiagnosticsPerFile.has(sourceFile.path)); + } + + /** + * This function returns the next affected file to be processed. + * Note that until doneAffected is called it would keep reporting same result + * This is to allow the callers to be able to actually remove affected file only when the operation is complete + * eg. if during diagnostics check cancellation token ends up cancelling the request, the affected file should be retained + */ + function getNextAffectedFile(state: BuilderProgramState, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash): SourceFile | Program | undefined { + while (true) { + const { affectedFiles } = state; + if (affectedFiles) { + const { seenAffectedFiles, semanticDiagnosticsPerFile } = state; + let { affectedFilesIndex } = state; + while (affectedFilesIndex < affectedFiles.length) { + const affectedFile = affectedFiles[affectedFilesIndex]; + if (!seenAffectedFiles.has(affectedFile.path)) { + // Set the next affected file as seen and remove the cached semantic diagnostics + state.affectedFilesIndex = affectedFilesIndex; + semanticDiagnosticsPerFile.delete(affectedFile.path); + return affectedFile; + } + seenAffectedFiles.set(affectedFile.path, true); + affectedFilesIndex++; + } + + // Remove the changed file from the change set + state.changedFilesSet.delete(state.currentChangedFilePath); + state.currentChangedFilePath = undefined; + // Commit the changes in file signature + BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures); + state.currentAffectedFilesSignatures.clear(); + state.affectedFiles = undefined; + } + + // Get next changed file + const nextKey = state.changedFilesSet.keys().next(); + if (nextKey.done) { + // Done + return undefined; + } + + // With --out or --outFile all outputs go into single file + // so operations are performed directly on program, return program + const compilerOptions = state.program.getCompilerOptions(); + if (compilerOptions.outFile || compilerOptions.out) { + Debug.assert(!state.semanticDiagnosticsPerFile); + return state.program; + } + + // Get next batch of affected files + state.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures || createMap(); + state.affectedFiles = BuilderState.getFilesAffectedBy(state, state.program, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures); + state.currentChangedFilePath = nextKey.value as Path; + state.semanticDiagnosticsPerFile.delete(nextKey.value as Path); + state.affectedFilesIndex = 0; + state.seenAffectedFiles = state.seenAffectedFiles || createMap(); + } + } + + /** + * This is called after completing operation on the next affected file. + * The operations here are postponed to ensure that cancellation during the iteration is handled correctly + */ + function doneWithAffectedFile(state: BuilderProgramState, affected: SourceFile | Program) { + if (affected === state.program) { + state.changedFilesSet.clear(); + } + else { + state.seenAffectedFiles.set((affected as SourceFile).path, true); + state.affectedFilesIndex++; + } + } + + /** + * Returns the result with affected file + */ + function toAffectedFileResult(state: BuilderProgramState, result: T, affected: SourceFile | Program): AffectedFileResult { + doneWithAffectedFile(state, affected); + return { result, affected }; + } + + /** + * 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(state: BuilderProgramState, sourceFile: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { + const path = sourceFile.path; + const cachedDiagnostics = state.semanticDiagnosticsPerFile.get(path); + // Report the semantic diagnostics from the cache if we already have those diagnostics present + if (cachedDiagnostics) { + return cachedDiagnostics; + } + + // Diagnostics werent cached, get them from program, and cache the result + const diagnostics = state.program.getSemanticDiagnostics(sourceFile, cancellationToken); + state.semanticDiagnosticsPerFile.set(path, diagnostics); + return diagnostics; + } + + export enum BuilderProgramKind { + SemanticDiagnosticsBuilderProgram, + EmitAndSemanticDiagnosticsBuilderProgram + } + + export function createBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram: BaseBuilderProgram | undefined, kind: BuilderProgramKind.SemanticDiagnosticsBuilderProgram): SemanticDiagnosticsBuilderProgram; + export function createBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram: BaseBuilderProgram | undefined, kind: BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram): EmitAndSemanticDiagnosticsBuilderProgram; + export function createBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram: BaseBuilderProgram | undefined, kind: BuilderProgramKind) { /** * Create the canonical file name for identity */ @@ -118,183 +231,129 @@ namespace ts { * Computing hash to for signature verification */ const computeHash = host.createHash || identity; - let state: BuilderStateWithChangedFiles; + const state = createBuilderProgramState(newProgram, getCanonicalFileName, oldProgram && oldProgram.getState()); - switch (builderKind) { - case BuilderKind.BuilderKindSemanticDiagnostics: - return getSemanticDiagnosticsBuilder(); - case BuilderKind.BuilderKindEmitAndSemanticDiagnostics: - return getEmitAndSemanticDiagnosticsBuilder(); - default: - notImplemented(); + // To ensure that we arent storing any references to old program or new program without state + newProgram = undefined; + oldProgram = undefined; + + const result: BaseBuilderProgram = { + getState: () => state, + getCompilerOptions: () => state.program.getCompilerOptions(), + getSourceFile: fileName => state.program.getSourceFile(fileName), + getSourceFiles: () => state.program.getSourceFiles(), + getOptionsDiagnostics: cancellationToken => state.program.getOptionsDiagnostics(cancellationToken), + getGlobalDiagnostics: cancellationToken => state.program.getGlobalDiagnostics(cancellationToken), + getSyntacticDiagnostics: (sourceFile, cancellationToken) => state.program.getSyntacticDiagnostics(sourceFile, cancellationToken), + getSemanticDiagnostics, + emit, + getAllDependencies: sourceFile => BuilderState.getAllDependencies(state, state.program, sourceFile) + }; + + if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { + (result as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; + } + else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + (result as EmitAndSemanticDiagnosticsBuilderProgram).getCurrentDirectory = () => state.program.getCurrentDirectory(); + (result as EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile; + } + else { + notImplemented(); } - function getSemanticDiagnosticsBuilder(): SemanticDiagnosticsBuilder { - return { - updateProgram, - getAllDependencies, - getSemanticDiagnosticsOfNextAffectedFile, - getSemanticDiagnostics - }; - } - - function getEmitAndSemanticDiagnosticsBuilder(): EmitAndSemanticDiagnosticsBuilder { - return { - updateProgram, - getAllDependencies, - emitNextAffectedFile, - getSemanticDiagnostics - }; - } + return result; /** - * Update current state to reflect new program - * Updates changed files, references, file infos etc which happens through the state callbacks + * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files */ - function updateProgram(newProgram: Program) { - state = createBuilderStateWithChangedFiles(newProgram, getCanonicalFileName, state); - } - - /** - * This function returns the next affected file to be processed. - * Note that until doneAffected is called it would keep reporting same result - * This is to allow the callers to be able to actually remove affected file only when the operation is complete - * eg. if during diagnostics check cancellation token ends up cancelling the request, the affected file should be retained - */ - function getNextAffectedFile(programOfThisState: Program, cancellationToken: CancellationToken | undefined): SourceFile | Program | undefined { - while (true) { - const { affectedFiles } = state; - if (affectedFiles) { - const { seenAffectedFiles, semanticDiagnosticsPerFile } = state; - let { affectedFilesIndex } = state; - while (affectedFilesIndex < affectedFiles.length) { - const affectedFile = affectedFiles[affectedFilesIndex]; - if (!seenAffectedFiles.has(affectedFile.path)) { - // Set the next affected file as seen and remove the cached semantic diagnostics - state.affectedFilesIndex = affectedFilesIndex; - semanticDiagnosticsPerFile.delete(affectedFile.path); - return affectedFile; - } - seenAffectedFiles.set(affectedFile.path, true); - affectedFilesIndex++; - } - - // Remove the changed file from the change set - state.changedFilesSet.delete(state.currentChangedFilePath); - state.currentChangedFilePath = undefined; - // Commit the changes in file signature - BuilderState.updateSignaturesFromCache(state, state.currentAffectedFilesSignatures); - state.currentAffectedFilesSignatures.clear(); - state.affectedFiles = undefined; - } - - // Get next changed file - const nextKey = state.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(!state.semanticDiagnosticsPerFile); - return programOfThisState; - } - - // Get next batch of affected files - state.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures || createMap(); - state.affectedFiles = BuilderState.getFilesAffectedBy(state, programOfThisState, nextKey.value as Path, cancellationToken, computeHash, state.currentAffectedFilesSignatures); - state.currentChangedFilePath = nextKey.value as Path; - state.semanticDiagnosticsPerFile.delete(nextKey.value as Path); - state.affectedFilesIndex = 0; - state.seenAffectedFiles = state.seenAffectedFiles || createMap(); - } - } - - /** - * This is called after completing operation on the next affected file. - * The operations here are postponed to ensure that cancellation during the iteration is handled correctly - */ - function doneWithAffectedFile(programOfThisState: Program, affected: SourceFile | Program) { - if (affected === programOfThisState) { - state.changedFilesSet.clear(); - } - else { - state.seenAffectedFiles.set((affected as SourceFile).path, true); - state.affectedFilesIndex++; - } - } - - /** - * Returns the result with affected file - */ - function toAffectedFileResult(programOfThisState: Program, result: T, affected: SourceFile | Program): AffectedFileResult { - doneWithAffectedFile(programOfThisState, affected); - return { result, affected }; - } - - /** - * 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): AffectedFileResult { - const affectedFile = getNextAffectedFile(programOfThisState, cancellationToken); - if (!affectedFile) { + function emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult { + const affected = getNextAffectedFile(state, cancellationToken, computeHash); + if (!affected) { // Done return undefined; } - else if (affectedFile === programOfThisState) { - // When whole program is affected, do emit only once (eg when --out or --outFile is specified) - return toAffectedFileResult( - programOfThisState, - programOfThisState.emit(/*targetSourceFile*/ undefined, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers), - programOfThisState - ); - } - // Emit the affected file - const targetSourceFile = affectedFile as SourceFile; return toAffectedFileResult( - programOfThisState, - programOfThisState.emit(targetSourceFile, writeFileCallback, cancellationToken, /*emitOnlyDtsFiles*/ false, customTransformers), - targetSourceFile + state, + // When whole program is affected, do emit only once (eg when --out or --outFile is specified) + // Otherwise just affected file + state.program.emit(affected === state.program ? undefined : affected as SourceFile, writeFile || host.writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers), + affected ); } + /** + * Emits the JavaScript and declaration files. + * When targetSource file is specified, emits the files corresponding to that source file, + * otherwise for the whole program. + * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, + * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, + * it will only emit all the affected files instead of whole program + * + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + function emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult { + if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { + assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile); + if (!targetSourceFile) { + // Emit and report any errors we ran into. + let sourceMaps: SourceMapData[] = []; + let emitSkipped: boolean; + let diagnostics: Diagnostic[]; + let emittedFiles: string[] = []; + + let affectedEmitResult: AffectedFileResult; + while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) { + emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; + diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics); + emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles); + sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps); + } + return { + emitSkipped, + diagnostics: diagnostics || emptyArray, + emittedFiles, + sourceMaps + }; + } + } + return state.program.emit(targetSourceFile, writeFile || host.writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + } + /** * 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): AffectedFileResult> { + function getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult> { while (true) { - const affectedFile = getNextAffectedFile(programOfThisState, cancellationToken); - if (!affectedFile) { + const affected = getNextAffectedFile(state, cancellationToken, computeHash); + if (!affected) { // Done return undefined; } - else if (affectedFile === programOfThisState) { + else if (affected === state.program) { // When whole program is affected, get all semantic diagnostics (eg when --out or --outFile is specified) return toAffectedFileResult( - programOfThisState, - programOfThisState.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken), - programOfThisState + state, + state.program.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken), + affected ); } // Get diagnostics for the affected file if its not ignored - const targetSourceFile = affectedFile as SourceFile; - if (ignoreSourceFile && ignoreSourceFile(targetSourceFile)) { + if (ignoreSourceFile && ignoreSourceFile(affected as SourceFile)) { // Get next affected file - doneWithAffectedFile(programOfThisState, targetSourceFile); + doneWithAffectedFile(state, affected); continue; } return toAffectedFileResult( - programOfThisState, - getSemanticDiagnosticsOfFile(programOfThisState, targetSourceFile, cancellationToken), - targetSourceFile + state, + getSemanticDiagnosticsOfFile(state, affected as SourceFile, cancellationToken), + affected ); } } @@ -302,59 +361,46 @@ 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 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 + * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, + * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics */ - function getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { - Debug.assert(!state.affectedFiles || state.affectedFiles[state.affectedFilesIndex - 1] !== sourceFile || !state.semanticDiagnosticsPerFile.has(sourceFile.path)); - const compilerOptions = programOfThisState.getCompilerOptions(); + function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { + assertSourceFileOkWithoutNextAffectedCall(state, sourceFile); + const compilerOptions = state.program.getCompilerOptions(); if (compilerOptions.outFile || compilerOptions.out) { Debug.assert(!state.semanticDiagnosticsPerFile); // We dont need to cache the diagnostics just return them from program - return programOfThisState.getSemanticDiagnostics(sourceFile, cancellationToken); + return state.program.getSemanticDiagnostics(sourceFile, cancellationToken); } if (sourceFile) { - return getSemanticDiagnosticsOfFile(programOfThisState, sourceFile, cancellationToken); + return getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken); + } + + if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { + // When semantic builder asks for diagnostics of the whole program, + // ensure that all the affected files are handled + let affected: SourceFile | Program | undefined; + while (affected = getNextAffectedFile(state, cancellationToken, computeHash)) { + doneWithAffectedFile(state, affected); + } } let diagnostics: Diagnostic[]; - for (const sourceFile of programOfThisState.getSourceFiles()) { - diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(programOfThisState, sourceFile, cancellationToken)); + for (const sourceFile of state.program.getSourceFiles()) { + diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(state, 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 = state.semanticDiagnosticsPerFile.get(path); - // Report the semantic diagnostics from the cache if we already have those diagnostics present - if (cachedDiagnostics) { - return cachedDiagnostics; - } - - // Diagnostics werent cached, get them from program, and cache the result - const diagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken); - state.semanticDiagnosticsPerFile.set(path, diagnostics); - return diagnostics; - } - - /** - * Get all the dependencies of the sourceFile - */ - function getAllDependencies(programOfThisState: Program, sourceFile: SourceFile) { - return BuilderState.getAllDependencies(state, programOfThisState, sourceFile); - } } } namespace ts { export type AffectedFileResult = { result: T; affected: SourceFile | Program; } | undefined; - export interface BuilderHost { + export interface BuilderProgramHost { /** * return true if file names are treated with case sensitivity */ @@ -363,73 +409,110 @@ namespace ts { * If provided this would be used this hash instead of actual file shape text for detecting changes */ createHash?: (data: string) => string; + /** + * When emit or emitNextAffectedFile are called without writeFile, + * this callback if present would be used to write files + */ + writeFile?: WriteFileCallback; } /** * Builder to manage the program state changes */ - export interface BaseBuilder { + export interface BaseBuilderProgram { + /*@internal*/ + getState(): BuilderProgramState; /** - * Updates the program in the builder to represent new state + * Get compiler options of the program */ - updateProgram(newProgram: Program): void; - + getCompilerOptions(): CompilerOptions; + /** + * Get the source file in the program with file name + */ + getSourceFile(fileName: string): SourceFile | undefined; + /** + * Get a list of files in the program + */ + getSourceFiles(): ReadonlyArray; + /** + * Get the diagnostics for compiler options + */ + getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get the diagnostics that dont belong to any file + */ + getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get the syntax diagnostics, for all source files if source file is not supplied + */ + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; /** * Get all the dependencies of the file */ - getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray; + getAllDependencies(sourceFile: SourceFile): 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 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 + * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, + * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics + */ + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Emits the JavaScript and declaration files. + * When targetSource file is specified, emits the files corresponding to that source file, + * otherwise for the whole program. + * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, + * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, + * it will only emit all the affected files instead of whole program + * + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; } /** * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files */ - export interface SemanticDiagnosticsBuilder extends BaseBuilder { + export interface SemanticDiagnosticsBuilderProgram extends BaseBuilderProgram { /** * Gets the semantic diagnostics from the program for the next affected file and caches it * Returns undefined if the iteration is complete */ - getSemanticDiagnosticsOfNextAffectedFile(programOfThisState: Program, cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; - - /** - * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program - * The semantic diagnostics are cached and managed here - * Note that it is assumed that the when asked about semantic diagnostics through this API, - * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics - */ - getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; } /** * 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 { + export interface EmitAndSemanticDiagnosticsBuilderProgram extends BaseBuilderProgram { + /** + * Get the current directory of the program + */ + getCurrentDirectory(): string; /** * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files */ - emitNextAffectedFile(programOfThisState: Program, writeFileCallback: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult; - - /** - * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program - * The semantic diagnostics are cached and managed here - * Note that it is assumed that the when asked about semantic diagnostics through this API, - * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics - */ - getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult; } /** * Create the builder to manage semantic diagnostics and cache them */ - export function createSemanticDiagnosticsBuilder(host: BuilderHost): SemanticDiagnosticsBuilder { - return createBuilder(host, BuilderKind.BuilderKindSemanticDiagnostics); + export function createSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: SemanticDiagnosticsBuilderProgram): SemanticDiagnosticsBuilderProgram { + return createBuilderProgram(newProgram, host, oldProgram, BuilderProgramKind.SemanticDiagnosticsBuilderProgram); } /** * 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(host: BuilderHost): EmitAndSemanticDiagnosticsBuilder { - return createBuilder(host, BuilderKind.BuilderKindEmitAndSemanticDiagnostics); + export function createEmitAndSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram): EmitAndSemanticDiagnosticsBuilderProgram { + return createBuilderProgram(newProgram, host, oldProgram, BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram); } } diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 07f69ddfe2..92d9f09944 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -38,6 +38,7 @@ "emitter.ts", "watchUtilities.ts", "program.ts", + "builderState.ts", "builder.ts", "resolutionCache.ts", "watch.ts", diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 1c5a0ad5d9..c07722cb7d 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -146,62 +146,23 @@ namespace ts { return ExitStatus.Success; } - /** - * Creates the function that emits files and reports errors when called with program - */ - function createEmitFilesAndReportErrorsWithBuilderUsingSystem(system: System, reportDiagnostic: DiagnosticReporter) { - const emitErrorsAndReportErrorsWithBuilder = createEmitFilesAndReportErrorsWithBuilder({ - useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, - createHash: system.createHash && (s => system.createHash(s)), - writeFile, - reportDiagnostic, - writeFileName: s => system.write(s + system.newLine) - }); - let host: CachedDirectoryStructureHost | undefined; - return { - emitFilesAndReportError: (program: Program) => emitErrorsAndReportErrorsWithBuilder(program), - setHost: (cachedDirectoryStructureHost: CachedDirectoryStructureHost) => host = cachedDirectoryStructureHost - }; - - function getHost() { - return host || system; - } - - function ensureDirectoriesExist(directoryPath: string) { - if (directoryPath.length > getRootLength(directoryPath) && !getHost().directoryExists(directoryPath)) { - const parentDirectory = getDirectoryPath(directoryPath); - ensureDirectoriesExist(parentDirectory); - getHost().createDirectory(directoryPath); - } - } - - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, onError: (message: string) => void) { - try { - performance.mark("beforeIOWrite"); - ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - - getHost().writeFile(fileName, text, writeByteOrderMark); - - performance.mark("afterIOWrite"); - performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); - } - catch (e) { - if (onError) { - onError(e.message); - } - } - } - } - const noopFileWatcher: FileWatcher = { close: noop }; /** * Creates the watch compiler host that can be extended with config file or root file names and options host */ function createWatchCompilerHost(system = sys, reportDiagnostic: DiagnosticReporter): WatchCompilerHost { - const { emitFilesAndReportError, setHost } = createEmitFilesAndReportErrorsWithBuilderUsingSystem(system, reportDiagnostic); - const host: WatchCompilerHost = { - useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, + let host: DirectoryStructureHost = system; + const useCaseSensitiveFileNames = () => system.useCaseSensitiveFileNames; + const writeFileName = (s: string) => system.write(s + system.newLine); + const builderProgramHost: BuilderProgramHost = { + useCaseSensitiveFileNames, + createHash: system.createHash && (s => system.createHash(s)), + writeFile + }; + let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; + return { + useCaseSensitiveFileNames, getNewLine: () => system.newLine, getCurrentDirectory: () => system.getCurrentDirectory(), getDefaultLibLocation, @@ -220,10 +181,9 @@ namespace ts { onWatchStatusChange, createDirectory: path => system.createDirectory(path), writeFile: (path, data, writeByteOrderMark) => system.writeFile(path, data, writeByteOrderMark), - onCachedDirectoryStructureHostCreate: host => setHost(host), - afterProgramCreate: emitFilesAndReportError, + onCachedDirectoryStructureHostCreate: cacheHost => host = cacheHost || system, + afterProgramCreate: emitFilesAndReportErrorUsingBuilder, }; - return host; function getDefaultLibLocation() { return getDirectoryPath(normalizePath(system.getExecutingFilePath())); @@ -235,6 +195,36 @@ namespace ts { } system.write(`${new Date().toLocaleTimeString()} - ${flattenDiagnosticMessageText(diagnostic.messageText, newLine)}${newLine + newLine + newLine}`); } + + function emitFilesAndReportErrorUsingBuilder(program: Program) { + builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, builderProgramHost, builderProgram); + emitFilesAndReportErrors(builderProgram, reportDiagnostic, writeFileName); + } + + function ensureDirectoriesExist(directoryPath: string) { + if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { + const parentDirectory = getDirectoryPath(directoryPath); + ensureDirectoriesExist(parentDirectory); + host.createDirectory(directoryPath); + } + } + + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, onError: (message: string) => void) { + try { + performance.mark("beforeIOWrite"); + ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); + + host.writeFile(fileName, text, writeByteOrderMark); + + performance.mark("afterIOWrite"); + performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + if (onError) { + onError(e.message); + } + } + } } /** @@ -272,73 +262,6 @@ namespace ts { namespace ts { export type DiagnosticReporter = (diagnostic: Diagnostic) => void; - interface BuilderProgram extends ProgramToEmitFilesAndReportErrors { - updateProgram(program: Program): void; - } - - function createBuilderProgram(host: BuilderEmitHost): BuilderProgram { - const builder = createEmitAndSemanticDiagnosticsBuilder(host); - let program: Program; - return { - getCurrentDirectory: () => program.getCurrentDirectory(), - getCompilerOptions: () => program.getCompilerOptions(), - getSourceFiles: () => program.getSourceFiles(), - getSyntacticDiagnostics: () => program.getSyntacticDiagnostics(), - getOptionsDiagnostics: () => program.getOptionsDiagnostics(), - getGlobalDiagnostics: () => program.getGlobalDiagnostics(), - getSemanticDiagnostics: () => builder.getSemanticDiagnostics(program), - emit, - updateProgram - }; - - function updateProgram(p: Program) { - program = p; - builder.updateProgram(p); - } - - function emit(): EmitResult { - // Emit and report any errors we ran into. - let sourceMaps: SourceMapData[]; - let emitSkipped: boolean; - let diagnostics: Diagnostic[]; - let emittedFiles: string[]; - - let affectedEmitResult: AffectedFileResult; - while (affectedEmitResult = builder.emitNextAffectedFile(program, host.writeFile)) { - emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped; - diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics); - emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles); - sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps); - } - return { - emitSkipped, - diagnostics, - emittedFiles, - sourceMaps - }; - } - } - - /** - * Host needed to emit files and report errors using builder - */ - export interface BuilderEmitHost extends BuilderHost { - writeFile: WriteFileCallback; - reportDiagnostic: DiagnosticReporter; - writeFileName?: (s: string) => void; - } - - /** - * Creates the function that reports the program errors and emit files every time it is called with argument as program - */ - export function createEmitFilesAndReportErrorsWithBuilder(host: BuilderEmitHost) { - const builderProgram = createBuilderProgram(host); - return (program: Program) => { - builderProgram.updateProgram(program); - emitFilesAndReportErrors(builderProgram, host.reportDiagnostic, host.writeFileName); - }; - } - export interface WatchCompilerHost { /** If provided, callback to invoke before each program creation */ beforeProgramCreate?(compilerOptions: CompilerOptions): void; diff --git a/src/harness/unittests/builder.ts b/src/harness/unittests/builder.ts index a6353d9c0c..8808a151c0 100644 --- a/src/harness/unittests/builder.ts +++ b/src/harness/unittests/builder.ts @@ -81,20 +81,22 @@ namespace ts { }); function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { - const builder = createEmitAndSemanticDiagnosticsBuilder({ useCaseSensitiveFileNames: returnTrue, }); + const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue }; + let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; return fileNames => { const program = getProgram(); - builder.updateProgram(program); + builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); const outputFileNames: string[] = []; // tslint:disable-next-line no-empty - while (builder.emitNextAffectedFile(program, fileName => outputFileNames.push(fileName))) { + while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName))) { } assert.deepEqual(outputFileNames, fileNames); }; } function makeAssertChangesWithCancellationToken(getProgram: () => Program): (fileNames: ReadonlyArray, cancelAfterEmitLength?: number) => void { - const builder = createEmitAndSemanticDiagnosticsBuilder({ useCaseSensitiveFileNames: returnTrue, }); + const host: BuilderProgramHost = { useCaseSensitiveFileNames: returnTrue }; + let builderProgram: EmitAndSemanticDiagnosticsBuilderProgram | undefined; let cancel = false; const cancellationToken: CancellationToken = { isCancellationRequested: () => cancel, @@ -108,7 +110,7 @@ namespace ts { cancel = false; let operationWasCancelled = false; const program = getProgram(); - builder.updateProgram(program); + builderProgram = createEmitAndSemanticDiagnosticsBuilderProgram(program, host, builderProgram); const outputFileNames: string[] = []; try { // tslint:disable-next-line no-empty @@ -117,7 +119,7 @@ namespace ts { if (outputFileNames.length === cancelAfterEmitLength) { cancel = true; } - } while (builder.emitNextAffectedFile(program, fileName => outputFileNames.push(fileName), cancellationToken)); + } while (builderProgram.emitNextAffectedFile(fileName => outputFileNames.push(fileName), cancellationToken)); } catch (e) { assert.isFalse(operationWasCancelled); diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index ba944bafd9..13a7a30d84 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -37,6 +37,7 @@ "../compiler/declarationEmitter.ts", "../compiler/emitter.ts", "../compiler/program.ts", + "../compiler/builderState.ts", "../compiler/builder.ts", "../compiler/resolutionCache.ts", "../compiler/watch.ts", diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 514c140f05..59f3916bbf 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3749,7 +3749,7 @@ declare namespace ts { result: T; affected: SourceFile | Program; } | undefined; - interface BuilderHost { + interface BuilderProgramHost { /** * return true if file names are treated with case sensitivity */ @@ -3758,78 +3758,104 @@ declare namespace ts { * If provided this would be used this hash instead of actual file shape text for detecting changes */ createHash?: (data: string) => string; + /** + * When emit or emitNextAffectedFile are called without writeFile, + * this callback if present would be used to write files + */ + writeFile?: WriteFileCallback; } /** * Builder to manage the program state changes */ - interface BaseBuilder { + interface BaseBuilderProgram { /** - * Updates the program in the builder to represent new state + * Get compiler options of the program */ - updateProgram(newProgram: Program): void; + getCompilerOptions(): CompilerOptions; + /** + * Get the source file in the program with file name + */ + getSourceFile(fileName: string): SourceFile | undefined; + /** + * Get a list of files in the program + */ + getSourceFiles(): ReadonlyArray; + /** + * Get the diagnostics for compiler options + */ + getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get the diagnostics that dont belong to any file + */ + getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Get the syntax diagnostics, for all source files if source file is not supplied + */ + getSyntacticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; /** * Get all the dependencies of the file */ - getAllDependencies(programOfThisState: Program, sourceFile: SourceFile): ReadonlyArray; + getAllDependencies(sourceFile: SourceFile): 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 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 + * In case of SemanticDiagnosticsBuilderProgram if the source file is not provided, + * it will iterate through all the affected files, to ensure that cache stays valid and yet provide a way to get all semantic diagnostics + */ + getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + /** + * Emits the JavaScript and declaration files. + * When targetSource file is specified, emits the files corresponding to that source file, + * otherwise for the whole program. + * In case of EmitAndSemanticDiagnosticsBuilderProgram, when targetSourceFile is specified, + * it is assumed that that file is handled from affected file list. If targetSourceFile is not specified, + * it will only emit all the affected files instead of whole program + * + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files + */ + emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult; } /** * The builder that caches the semantic diagnostics for the program and handles the changed files and affected files */ - interface SemanticDiagnosticsBuilder extends BaseBuilder { + interface SemanticDiagnosticsBuilderProgram extends BaseBuilderProgram { /** * Gets the semantic diagnostics from the program for the next affected file and caches it * Returns undefined if the iteration is complete */ - getSemanticDiagnosticsOfNextAffectedFile(programOfThisState: Program, cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; - /** - * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program - * The semantic diagnostics are cached and managed here - * Note that it is assumed that the when asked about semantic diagnostics through this API, - * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics - */ - getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + getSemanticDiagnosticsOfNextAffectedFile(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult>; } /** * 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 { + interface EmitAndSemanticDiagnosticsBuilderProgram extends BaseBuilderProgram { + /** + * Get the current directory of the program + */ + getCurrentDirectory(): string; /** * Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete + * The first of writeFile if provided, writeFile of BuilderProgramHost if provided, writeFile of compiler host + * in that order would be used to write the files */ - emitNextAffectedFile(programOfThisState: Program, writeFileCallback: WriteFileCallback, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): AffectedFileResult; - /** - * Gets the semantic diagnostics from the program corresponding to this state of file (if provided) or whole program - * The semantic diagnostics are cached and managed here - * Note that it is assumed that the when asked about semantic diagnostics through this API, - * the file has been taken out of affected files so it is safe to use cache or get from program and cache the diagnostics - */ - getSemanticDiagnostics(programOfThisState: Program, sourceFile?: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray; + emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult; } /** * Create the builder to manage semantic diagnostics and cache them */ - function createSemanticDiagnosticsBuilder(host: BuilderHost): SemanticDiagnosticsBuilder; + function createSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: SemanticDiagnosticsBuilderProgram): SemanticDiagnosticsBuilderProgram; /** * Create the builder that can handle the changes in program and iterate through changed files * to emit the those files and manage semantic diagnostics cache as well */ - function createEmitAndSemanticDiagnosticsBuilder(host: BuilderHost): EmitAndSemanticDiagnosticsBuilder; + function createEmitAndSemanticDiagnosticsBuilderProgram(newProgram: Program, host: BuilderProgramHost, oldProgram?: EmitAndSemanticDiagnosticsBuilderProgram): EmitAndSemanticDiagnosticsBuilderProgram; } declare namespace ts { type DiagnosticReporter = (diagnostic: Diagnostic) => void; - /** - * Host needed to emit files and report errors using builder - */ - interface BuilderEmitHost extends BuilderHost { - writeFile: WriteFileCallback; - reportDiagnostic: DiagnosticReporter; - writeFileName?: (s: string) => void; - } - /** - * Creates the function that reports the program errors and emit files every time it is called with argument as program - */ - function createEmitFilesAndReportErrorsWithBuilder(host: BuilderEmitHost): (program: Program) => void; interface WatchCompilerHost { /** If provided, callback to invoke before each program creation */ beforeProgramCreate?(compilerOptions: CompilerOptions): void;