/*@internal*/ namespace ts { export interface ReusableDiagnostic extends ReusableDiagnosticRelatedInformation { /** May store more in future. For now, this will simply be `true` to indicate when a diagnostic is an unused-identifier diagnostic. */ reportsUnnecessary?: {}; reportDeprecated?: {} source?: string; relatedInformation?: ReusableDiagnosticRelatedInformation[]; skippedOn?: keyof CompilerOptions; } export interface ReusableDiagnosticRelatedInformation { category: DiagnosticCategory; code: number; file: string | undefined; start: number | undefined; length: number | undefined; messageText: string | ReusableDiagnosticMessageChain; } export type ReusableDiagnosticMessageChain = DiagnosticMessageChain; export interface ReusableBuilderProgramState extends ReusableBuilderState { /** * Cache of bind and check diagnostics for files with their Path being the key */ semanticDiagnosticsPerFile?: ReadonlyESMap | undefined; /** * The map has key by source file's path that has been changed */ changedFilesSet?: ReadonlySet; /** * Set of affected files being iterated */ affectedFiles?: readonly SourceFile[] | undefined; /** * Current changed file for iterating over affected files */ currentChangedFilePath?: Path | undefined; /** * Map of file signatures, with key being file path, calculated while getting current changed file's affected files * These will be committed whenever the iteration through affected files of current changed file is complete */ currentAffectedFilesSignatures?: ReadonlyESMap | undefined; /** * Newly computed visible to outside referencedSet */ currentAffectedFilesExportedModulesMap?: BuilderState.ReadonlyManyToManyPathMap | undefined; /** * True if the semantic diagnostics were copied from the old state */ semanticDiagnosticsFromOldState?: Set; /** * program corresponding to this state */ program?: Program | undefined; /** * compilerOptions for the program */ compilerOptions: CompilerOptions; /** * Files pending to be emitted */ affectedFilesPendingEmit?: readonly Path[] | undefined; /** * Files pending to be emitted kind. */ affectedFilesPendingEmitKind?: ReadonlyESMap | undefined; /** * Current index to retrieve pending affected file */ affectedFilesPendingEmitIndex?: number | undefined; /* * true if semantic diagnostics are ReusableDiagnostic instead of Diagnostic */ hasReusableDiagnostic?: true; } export const enum BuilderFileEmit { DtsOnly, Full } /** * State to store the changed files, affected files and cache semantic diagnostics */ // TODO: GH#18217 Properties of this interface are frequently asserted to be defined. export interface BuilderProgramState extends BuilderState { /** * Cache of bind and check diagnostics for files with their Path being the key */ semanticDiagnosticsPerFile: ESMap | undefined; /** * The map has key by source file's path that has been changed */ changedFilesSet: Set; /** * Set of affected files being iterated */ affectedFiles: readonly SourceFile[] | undefined; /** * Current index to retrieve affected file from */ affectedFilesIndex: number | undefined; /** * Current changed file for iterating over affected files */ currentChangedFilePath: Path | undefined; /** * Map of file signatures, with key being file path, calculated while getting current changed file's affected files * These will be committed whenever the iteration through affected files of current changed file is complete */ currentAffectedFilesSignatures: ESMap | undefined; /** * Newly computed visible to outside referencedSet * We need to store the updates separately in case the in-progress build is cancelled * and we need to roll back. */ currentAffectedFilesExportedModulesMap: BuilderState.ManyToManyPathMap | undefined; /** * Already seen affected files */ seenAffectedFiles: Set | undefined; /** * whether this program has cleaned semantic diagnostics cache for lib files */ cleanedDiagnosticsOfLibFiles?: boolean; /** * True if the semantic diagnostics were copied from the old state */ semanticDiagnosticsFromOldState?: Set; /** * program corresponding to this state */ program: Program | undefined; /** * compilerOptions for the program */ compilerOptions: CompilerOptions; /** * Files pending to be emitted */ affectedFilesPendingEmit: Path[] | undefined; /** * Files pending to be emitted kind. */ affectedFilesPendingEmitKind: ESMap | undefined; /** * Current index to retrieve pending affected file */ affectedFilesPendingEmitIndex: number | undefined; /** * true if build info is emitted */ buildInfoEmitPending: boolean; /** * Already seen emitted files */ seenEmittedFiles: ESMap | undefined; /** * true if program has been emitted */ programEmitComplete?: true; /** Stores list of files that change signature during emit - test only */ filesChangingSignature?: Set; } function hasSameKeys(map1: ReadonlyCollection | undefined, map2: ReadonlyCollection | undefined): boolean { // Has same size and every key is present in both maps return map1 === map2 || map1 !== undefined && map2 !== undefined && map1.size === map2.size && !forEachKey(map1, key => !map2.has(key)); } /** * Create the state so that we can iterate on changedFiles/affected files */ function createBuilderProgramState(newProgram: Program, getCanonicalFileName: GetCanonicalFileName, oldState: Readonly | undefined, disableUseFileVersionAsSignature: boolean | undefined): BuilderProgramState { const state = BuilderState.create(newProgram, getCanonicalFileName, oldState, disableUseFileVersionAsSignature) as BuilderProgramState; state.program = newProgram; const compilerOptions = newProgram.getCompilerOptions(); state.compilerOptions = compilerOptions; // With --out or --outFile, any change affects all semantic diagnostics so no need to cache them if (!outFile(compilerOptions)) { state.semanticDiagnosticsPerFile = new Map(); } state.changedFilesSet = new Set(); const useOldState = BuilderState.canReuseOldState(state.referencedMap, oldState); const oldCompilerOptions = useOldState ? oldState!.compilerOptions : undefined; const canCopySemanticDiagnostics = useOldState && oldState!.semanticDiagnosticsPerFile && !!state.semanticDiagnosticsPerFile && !compilerOptionsAffectSemanticDiagnostics(compilerOptions, oldCompilerOptions!); if (useOldState) { // Verify the sanity of old state if (!oldState!.currentChangedFilePath) { const affectedSignatures = oldState!.currentAffectedFilesSignatures; Debug.assert(!oldState!.affectedFiles && (!affectedSignatures || !affectedSignatures.size), "Cannot reuse if only few affected files of currentChangedFile were iterated"); } const changedFilesSet = oldState!.changedFilesSet; if (canCopySemanticDiagnostics) { Debug.assert(!changedFilesSet || !forEachKey(changedFilesSet, path => oldState!.semanticDiagnosticsPerFile!.has(path)), "Semantic diagnostics shouldnt be available for changed files"); } // Copy old state's changed files set changedFilesSet?.forEach(value => state.changedFilesSet.add(value)); if (!outFile(compilerOptions) && oldState!.affectedFilesPendingEmit) { state.affectedFilesPendingEmit = oldState!.affectedFilesPendingEmit.slice(); state.affectedFilesPendingEmitKind = oldState!.affectedFilesPendingEmitKind && new Map(oldState!.affectedFilesPendingEmitKind); state.affectedFilesPendingEmitIndex = oldState!.affectedFilesPendingEmitIndex; state.seenAffectedFiles = new Set(); } } // Update changed files and copy semantic diagnostics if we can const referencedMap = state.referencedMap; const oldReferencedMap = useOldState ? oldState!.referencedMap : undefined; const copyDeclarationFileDiagnostics = canCopySemanticDiagnostics && !compilerOptions.skipLibCheck === !oldCompilerOptions!.skipLibCheck; const copyLibFileDiagnostics = copyDeclarationFileDiagnostics && !compilerOptions.skipDefaultLibCheck === !oldCompilerOptions!.skipDefaultLibCheck; state.fileInfos.forEach((info, sourceFilePath) => { let oldInfo: Readonly | undefined; let newReferences: ReadonlySet | undefined; // if not using old state, every file is changed if (!useOldState || // File wasn't present in old state !(oldInfo = oldState!.fileInfos.get(sourceFilePath)) || // versions dont match oldInfo.version !== info.version || // Referenced files changed !hasSameKeys(newReferences = referencedMap && referencedMap.getValues(sourceFilePath), oldReferencedMap && oldReferencedMap.getValues(sourceFilePath)) || // Referenced file was deleted in the new program newReferences && forEachKey(newReferences, path => !state.fileInfos.has(path) && oldState!.fileInfos.has(path))) { // Register file as changed file and do not copy semantic diagnostics, since all changed files need to be re-evaluated state.changedFilesSet.add(sourceFilePath); } else if (canCopySemanticDiagnostics) { const sourceFile = newProgram.getSourceFileByPath(sourceFilePath)!; if (sourceFile.isDeclarationFile && !copyDeclarationFileDiagnostics) return; if (sourceFile.hasNoDefaultLib && !copyLibFileDiagnostics) return; // Unchanged file copy diagnostics const diagnostics = oldState!.semanticDiagnosticsPerFile!.get(sourceFilePath); if (diagnostics) { state.semanticDiagnosticsPerFile!.set(sourceFilePath, oldState!.hasReusableDiagnostic ? convertToDiagnostics(diagnostics as readonly ReusableDiagnostic[], newProgram, getCanonicalFileName) : diagnostics as readonly Diagnostic[]); if (!state.semanticDiagnosticsFromOldState) { state.semanticDiagnosticsFromOldState = new Set(); } state.semanticDiagnosticsFromOldState.add(sourceFilePath); } } }); // If the global file is removed, add all files as changed if (useOldState && forEachEntry(oldState!.fileInfos, (info, sourceFilePath) => info.affectsGlobalScope && !state.fileInfos.has(sourceFilePath))) { BuilderState.getAllFilesExcludingDefaultLibraryFile(state, newProgram, /*firstSourceFile*/ undefined) .forEach(file => state.changedFilesSet.add(file.resolvedPath)); } else if (oldCompilerOptions && !outFile(compilerOptions) && compilerOptionsAffectEmit(compilerOptions, oldCompilerOptions)) { // Add all files to affectedFilesPendingEmit since emit changed newProgram.getSourceFiles().forEach(f => addToAffectedFilesPendingEmit(state, f.resolvedPath, BuilderFileEmit.Full)); Debug.assert(!state.seenAffectedFiles || !state.seenAffectedFiles.size); state.seenAffectedFiles = state.seenAffectedFiles || new Set(); } if (useOldState) { // Any time the interpretation of a source file changes, mark it as changed forEachEntry(oldState!.fileInfos, (info, sourceFilePath) => { if (state.fileInfos.has(sourceFilePath) && state.fileInfos.get(sourceFilePath)!.impliedFormat !== info.impliedFormat) { state.changedFilesSet.add(sourceFilePath); } }); } state.buildInfoEmitPending = !!state.changedFilesSet.size; return state; } function convertToDiagnostics(diagnostics: readonly ReusableDiagnostic[], newProgram: Program, getCanonicalFileName: GetCanonicalFileName): readonly Diagnostic[] { if (!diagnostics.length) return emptyArray; const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(getTsBuildInfoEmitOutputFilePath(newProgram.getCompilerOptions())!, newProgram.getCurrentDirectory())); return diagnostics.map(diagnostic => { const result: Diagnostic = convertToDiagnosticRelatedInformation(diagnostic, newProgram, toPath); result.reportsUnnecessary = diagnostic.reportsUnnecessary; result.reportsDeprecated = diagnostic.reportDeprecated; result.source = diagnostic.source; result.skippedOn = diagnostic.skippedOn; const { relatedInformation } = diagnostic; result.relatedInformation = relatedInformation ? relatedInformation.length ? relatedInformation.map(r => convertToDiagnosticRelatedInformation(r, newProgram, toPath)) : [] : undefined; return result; }); function toPath(path: string) { return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); } } function convertToDiagnosticRelatedInformation(diagnostic: ReusableDiagnosticRelatedInformation, newProgram: Program, toPath: (path: string) => Path): DiagnosticRelatedInformation { const { file } = diagnostic; return { ...diagnostic, file: file ? newProgram.getSourceFileByPath(toPath(file)) : undefined }; } /** * Releases program and other related not needed properties */ function releaseCache(state: BuilderProgramState) { BuilderState.releaseCache(state); state.program = undefined; } /** * Creates a clone of the state */ function cloneBuilderProgramState(state: Readonly): BuilderProgramState { const newState = BuilderState.clone(state) as BuilderProgramState; newState.semanticDiagnosticsPerFile = state.semanticDiagnosticsPerFile && new Map(state.semanticDiagnosticsPerFile); newState.changedFilesSet = new Set(state.changedFilesSet); newState.affectedFiles = state.affectedFiles; newState.affectedFilesIndex = state.affectedFilesIndex; newState.currentChangedFilePath = state.currentChangedFilePath; newState.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures && new Map(state.currentAffectedFilesSignatures); newState.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap?.clone(); newState.seenAffectedFiles = state.seenAffectedFiles && new Set(state.seenAffectedFiles); newState.cleanedDiagnosticsOfLibFiles = state.cleanedDiagnosticsOfLibFiles; newState.semanticDiagnosticsFromOldState = state.semanticDiagnosticsFromOldState && new Set(state.semanticDiagnosticsFromOldState); newState.program = state.program; newState.compilerOptions = state.compilerOptions; newState.affectedFilesPendingEmit = state.affectedFilesPendingEmit && state.affectedFilesPendingEmit.slice(); newState.affectedFilesPendingEmitKind = state.affectedFilesPendingEmitKind && new Map(state.affectedFilesPendingEmitKind); newState.affectedFilesPendingEmitIndex = state.affectedFilesPendingEmitIndex; newState.seenEmittedFiles = state.seenEmittedFiles && new Map(state.seenEmittedFiles); newState.programEmitComplete = state.programEmitComplete; return newState; } /** * 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.resolvedPath)); } /** * 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, host: BuilderProgramHost): SourceFile | Program | undefined { while (true) { const { affectedFiles } = state; if (affectedFiles) { const seenAffectedFiles = state.seenAffectedFiles!; let affectedFilesIndex = state.affectedFilesIndex!; // TODO: GH#18217 while (affectedFilesIndex < affectedFiles.length) { const affectedFile = affectedFiles[affectedFilesIndex]; if (!seenAffectedFiles.has(affectedFile.resolvedPath)) { // Set the next affected file as seen and remove the cached semantic diagnostics state.affectedFilesIndex = affectedFilesIndex; handleDtsMayChangeOfAffectedFile(state, affectedFile, cancellationToken, computeHash, host); return affectedFile; } 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(); BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); state.currentAffectedFilesExportedModulesMap?.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 program = Debug.checkDefined(state.program); const compilerOptions = program.getCompilerOptions(); if (outFile(compilerOptions)) { Debug.assert(!state.semanticDiagnosticsPerFile); return program; } // Get next batch of affected files if (!state.currentAffectedFilesSignatures) state.currentAffectedFilesSignatures = new Map(); if (state.exportedModulesMap) { state.currentAffectedFilesExportedModulesMap ||= BuilderState.createManyToManyPathMap(); } state.affectedFiles = BuilderState.getFilesAffectedBy(state, program, nextKey.value, cancellationToken, computeHash, state.currentAffectedFilesSignatures, state.currentAffectedFilesExportedModulesMap); state.currentChangedFilePath = nextKey.value; state.affectedFilesIndex = 0; if (!state.seenAffectedFiles) state.seenAffectedFiles = new Set(); } } function clearAffectedFilesPendingEmit(state: BuilderProgramState) { state.affectedFilesPendingEmit = undefined; state.affectedFilesPendingEmitKind = undefined; state.affectedFilesPendingEmitIndex = undefined; } /** * Returns next file to be emitted from files that retrieved semantic diagnostics but did not emit yet */ function getNextAffectedFilePendingEmit(state: BuilderProgramState) { const { affectedFilesPendingEmit } = state; if (affectedFilesPendingEmit) { const seenEmittedFiles = (state.seenEmittedFiles || (state.seenEmittedFiles = new Map())); for (let i = state.affectedFilesPendingEmitIndex!; i < affectedFilesPendingEmit.length; i++) { const affectedFile = Debug.checkDefined(state.program).getSourceFileByPath(affectedFilesPendingEmit[i]); if (affectedFile) { const seenKind = seenEmittedFiles.get(affectedFile.resolvedPath); const emitKind = Debug.checkDefined(Debug.checkDefined(state.affectedFilesPendingEmitKind).get(affectedFile.resolvedPath)); if (seenKind === undefined || seenKind < emitKind) { // emit this file state.affectedFilesPendingEmitIndex = i; return { affectedFile, emitKind }; } } } clearAffectedFilesPendingEmit(state); } return undefined; } function removeDiagnosticsOfLibraryFiles(state: BuilderProgramState) { if (!state.cleanedDiagnosticsOfLibFiles) { state.cleanedDiagnosticsOfLibFiles = true; const program = Debug.checkDefined(state.program); const options = program.getCompilerOptions(); forEach(program.getSourceFiles(), f => program.isSourceFileDefaultLibrary(f) && !skipTypeChecking(f, options, program) && removeSemanticDiagnosticsOf(state, f.resolvedPath) ); } } /** * Handles semantic diagnostics and dts emit for affectedFile and files, that are referencing modules that export entities from affected file * This is because even though js emit doesnt change, dts emit / type used can change resulting in need for dts emit and js change */ function handleDtsMayChangeOfAffectedFile( state: BuilderProgramState, affectedFile: SourceFile, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash, host: BuilderProgramHost, ) { removeSemanticDiagnosticsOf(state, affectedFile.resolvedPath); // If affected files is everything except default library, then nothing more to do if (state.allFilesExcludingDefaultLibraryFile === state.affectedFiles) { removeDiagnosticsOfLibraryFiles(state); // When a change affects the global scope, all files are considered to be affected without updating their signature // That means when affected file is handled, its signature can be out of date // To avoid this, ensure that we update the signature for any affected file in this scenario. BuilderState.updateShapeSignature( state, Debug.checkDefined(state.program), affectedFile, Debug.checkDefined(state.currentAffectedFilesSignatures), cancellationToken, computeHash, state.currentAffectedFilesExportedModulesMap ); return; } Debug.assert(state.hasCalledUpdateShapeSignature.has(affectedFile.resolvedPath) || state.currentAffectedFilesSignatures?.has(affectedFile.resolvedPath), `Signature not updated for affected file: ${affectedFile.fileName}`); if (state.compilerOptions.assumeChangesOnlyAffectDirectDependencies) return; handleDtsMayChangeOfReferencingExportOfAffectedFile(state, affectedFile, cancellationToken, computeHash, host); } /** * Handle the dts may change, so they need to be added to pending emit if dts emit is enabled, * Also we need to make sure signature is updated for these files */ function handleDtsMayChangeOf( state: BuilderProgramState, path: Path, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash, host: BuilderProgramHost ): void { removeSemanticDiagnosticsOf(state, path); if (!state.changedFilesSet.has(path)) { const program = Debug.checkDefined(state.program); const sourceFile = program.getSourceFileByPath(path); if (sourceFile) { // Even though the js emit doesnt change and we are already handling dts emit and semantic diagnostics // we need to update the signature to reflect correctness of the signature(which is output d.ts emit) of this file // This ensures that we dont later during incremental builds considering wrong signature. // Eg where this also is needed to ensure that .tsbuildinfo generated by incremental build should be same as if it was first fresh build // But we avoid expensive full shape computation, as using file version as shape is enough for correctness. BuilderState.updateShapeSignature( state, program, sourceFile, Debug.checkDefined(state.currentAffectedFilesSignatures), cancellationToken, computeHash, state.currentAffectedFilesExportedModulesMap, !host.disableUseFileVersionAsSignature ); // If not dts emit, nothing more to do if (getEmitDeclarations(state.compilerOptions)) { addToAffectedFilesPendingEmit(state, path, BuilderFileEmit.DtsOnly); } } } } /** * Removes semantic diagnostics for path and * returns true if there are no more semantic diagnostics from the old state */ function removeSemanticDiagnosticsOf(state: BuilderProgramState, path: Path) { if (!state.semanticDiagnosticsFromOldState) { return true; } state.semanticDiagnosticsFromOldState.delete(path); state.semanticDiagnosticsPerFile!.delete(path); return !state.semanticDiagnosticsFromOldState.size; } function isChangedSignature(state: BuilderProgramState, path: Path) { const newSignature = Debug.checkDefined(state.currentAffectedFilesSignatures).get(path); const oldSignature = Debug.checkDefined(state.fileInfos.get(path)).signature; return newSignature !== oldSignature; } function forEachKeyOfExportedModulesMap( state: BuilderProgramState, filePath: Path, fn: (exportedFromPath: Path) => T | undefined, ): T | undefined { // Go through exported modules from cache first let keys = state.currentAffectedFilesExportedModulesMap!.getKeys(filePath); const result = keys && forEachKey(keys, fn); if (result) return result; // If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected keys = state.exportedModulesMap!.getKeys(filePath); return keys && forEachKey(keys, exportedFromPath => // If the cache had an updated value, skip !state.currentAffectedFilesExportedModulesMap!.hasKey(exportedFromPath) && !state.currentAffectedFilesExportedModulesMap!.deletedKeys()?.has(exportedFromPath) ? fn(exportedFromPath) : undefined ); } function handleDtsMayChangeOfGlobalScope( state: BuilderProgramState, filePath: Path, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash, host: BuilderProgramHost, ): boolean { if (!state.fileInfos.get(filePath)?.affectsGlobalScope) return false; // Every file needs to be handled BuilderState.getAllFilesExcludingDefaultLibraryFile(state, state.program!, /*firstSourceFile*/ undefined) .forEach(file => handleDtsMayChangeOf( state, file.resolvedPath, cancellationToken, computeHash, host, )); removeDiagnosticsOfLibraryFiles(state); return true; } /** * Iterate on referencing modules that export entities from affected file and delete diagnostics and add pending emit */ function handleDtsMayChangeOfReferencingExportOfAffectedFile( state: BuilderProgramState, affectedFile: SourceFile, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash, host: BuilderProgramHost ) { // If there was change in signature (dts output) for the changed file, // then only we need to handle pending file emit if (!state.exportedModulesMap || !state.changedFilesSet.has(affectedFile.resolvedPath)) return; if (!isChangedSignature(state, affectedFile.resolvedPath)) return; // Since isolated modules dont change js files, files affected by change in signature is itself // But we need to cleanup semantic diagnostics and queue dts emit for affected files if (state.compilerOptions.isolatedModules) { const seenFileNamesMap = new Map(); seenFileNamesMap.set(affectedFile.resolvedPath, true); const queue = BuilderState.getReferencedByPaths(state, affectedFile.resolvedPath); while (queue.length > 0) { const currentPath = queue.pop()!; if (!seenFileNamesMap.has(currentPath)) { seenFileNamesMap.set(currentPath, true); if (handleDtsMayChangeOfGlobalScope(state, currentPath, cancellationToken, computeHash, host)) return; handleDtsMayChangeOf(state, currentPath, cancellationToken, computeHash, host); if (isChangedSignature(state, currentPath)) { const currentSourceFile = Debug.checkDefined(state.program).getSourceFileByPath(currentPath)!; queue.push(...BuilderState.getReferencedByPaths(state, currentSourceFile.resolvedPath)); } } } } Debug.assert(!!state.currentAffectedFilesExportedModulesMap); const seenFileAndExportsOfFile = new Set(); // Go through exported modules from cache first // If exported modules has path, all files referencing file exported from are affected forEachKeyOfExportedModulesMap(state, affectedFile.resolvedPath, exportedFromPath => { if (handleDtsMayChangeOfGlobalScope(state, exportedFromPath, cancellationToken, computeHash, host)) return true; const references = state.referencedMap!.getKeys(exportedFromPath); return references && forEachKey(references, filePath => handleDtsMayChangeOfFileAndExportsOfFile( state, filePath, seenFileAndExportsOfFile, cancellationToken, computeHash, host, ) ); }); } /** * handle dts and semantic diagnostics on file and iterate on anything that exports this file * return true when all work is done and we can exit handling dts emit and semantic diagnostics */ function handleDtsMayChangeOfFileAndExportsOfFile( state: BuilderProgramState, filePath: Path, seenFileAndExportsOfFile: Set, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash, host: BuilderProgramHost, ): boolean | undefined { if (!tryAddToSet(seenFileAndExportsOfFile, filePath)) return undefined; if (handleDtsMayChangeOfGlobalScope(state, filePath, cancellationToken, computeHash, host)) return true; handleDtsMayChangeOf(state, filePath, cancellationToken, computeHash, host); Debug.assert(!!state.currentAffectedFilesExportedModulesMap); // If exported modules has path, all files referencing file exported from are affected forEachKeyOfExportedModulesMap(state, filePath, exportedFromPath => handleDtsMayChangeOfFileAndExportsOfFile( state, exportedFromPath, seenFileAndExportsOfFile, cancellationToken, computeHash, host, ) ); // Remove diagnostics of files that import this file (without going to exports of referencing files) state.referencedMap!.getKeys(filePath)?.forEach(referencingFilePath => !seenFileAndExportsOfFile.has(referencingFilePath) && // Not already removed diagnostic file handleDtsMayChangeOf( // Dont add to seen since this is not yet done with the export removal state, referencingFilePath, cancellationToken, computeHash, host, ) ); return undefined; } /** * 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, emitKind?: BuilderFileEmit, isPendingEmit?: boolean, isBuildInfoEmit?: boolean ) { if (isBuildInfoEmit) { state.buildInfoEmitPending = false; } else if (affected === state.program) { state.changedFilesSet.clear(); state.programEmitComplete = true; } else { state.seenAffectedFiles!.add((affected as SourceFile).resolvedPath); if (emitKind !== undefined) { (state.seenEmittedFiles || (state.seenEmittedFiles = new Map())).set((affected as SourceFile).resolvedPath, emitKind); } if (isPendingEmit) { state.affectedFilesPendingEmitIndex!++; state.buildInfoEmitPending = true; } else { state.affectedFilesIndex!++; } } } /** * Returns the result with affected file */ function toAffectedFileResult(state: BuilderProgramState, result: T, affected: SourceFile | Program): AffectedFileResult { doneWithAffectedFile(state, affected); return { result, affected }; } /** * Returns the result with affected file */ function toAffectedFileEmitResult( state: BuilderProgramState, result: EmitResult, affected: SourceFile | Program, emitKind: BuilderFileEmit, isPendingEmit?: boolean, isBuildInfoEmit?: boolean ): AffectedFileResult { doneWithAffectedFile(state, affected, emitKind, isPendingEmit, isBuildInfoEmit); return { result, affected }; } /** * Gets semantic diagnostics for the file which are * bindAndCheckDiagnostics (from cache) and program diagnostics */ function getSemanticDiagnosticsOfFile(state: BuilderProgramState, sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { return concatenate( getBinderAndCheckerDiagnosticsOfFile(state, sourceFile, cancellationToken), Debug.checkDefined(state.program).getProgramDiagnostics(sourceFile) ); } /** * Gets the binder and checker diagnostics either from cache if present, or otherwise from program and caches it * Note that it is assumed that when asked about binder and checker diagnostics, the file has been taken out of affected files/changed file set */ function getBinderAndCheckerDiagnosticsOfFile(state: BuilderProgramState, sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { const path = sourceFile.resolvedPath; if (state.semanticDiagnosticsPerFile) { const cachedDiagnostics = state.semanticDiagnosticsPerFile.get(path); // Report the bind and check diagnostics from the cache if we already have those diagnostics present if (cachedDiagnostics) { return filterSemanticDiagnostics(cachedDiagnostics, state.compilerOptions); } } // Diagnostics werent cached, get them from program, and cache the result const diagnostics = Debug.checkDefined(state.program).getBindAndCheckDiagnostics(sourceFile, cancellationToken); if (state.semanticDiagnosticsPerFile) { state.semanticDiagnosticsPerFile.set(path, diagnostics); } return filterSemanticDiagnostics(diagnostics, state.compilerOptions); } export type ProgramBuildInfoFileId = number & { __programBuildInfoFileIdBrand: any }; export type ProgramBuildInfoFileIdListId = number & { __programBuildInfoFileIdListIdBrand: any }; export type ProgramBuildInfoDiagnostic = ProgramBuildInfoFileId | [fileId: ProgramBuildInfoFileId, diagnostics: readonly ReusableDiagnostic[]]; export type ProgramBuilderInfoFilePendingEmit = [fileId: ProgramBuildInfoFileId, emitKind: BuilderFileEmit]; export type ProgramBuildInfoReferencedMap = [fileId: ProgramBuildInfoFileId, fileIdListId: ProgramBuildInfoFileIdListId][]; export type ProgramBuildInfoBuilderStateFileInfo = Omit & { /** * Signature is * - undefined if FileInfo.version === FileInfo.signature * - false if FileInfo has signature as undefined (not calculated) * - string actual signature */ signature: string | false | undefined; }; /** * ProgramBuildInfoFileInfo is string if FileInfo.version === FileInfo.signature && !FileInfo.affectsGlobalScope otherwise encoded FileInfo */ export type ProgramBuildInfoFileInfo = string | ProgramBuildInfoBuilderStateFileInfo; export interface ProgramBuildInfo { fileNames: readonly string[]; fileInfos: readonly ProgramBuildInfoFileInfo[]; options: CompilerOptions | undefined; fileIdsList?: readonly (readonly ProgramBuildInfoFileId[])[]; referencedMap?: ProgramBuildInfoReferencedMap; exportedModulesMap?: ProgramBuildInfoReferencedMap; semanticDiagnosticsPerFile?: ProgramBuildInfoDiagnostic[]; affectedFilesPendingEmit?: ProgramBuilderInfoFilePendingEmit[]; } /** * Gets the program information to be emitted in buildInfo so that we can use it to create new program */ function getProgramBuildInfo(state: Readonly, getCanonicalFileName: GetCanonicalFileName): ProgramBuildInfo | undefined { if (outFile(state.compilerOptions)) return undefined; const currentDirectory = Debug.checkDefined(state.program).getCurrentDirectory(); const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(getTsBuildInfoEmitOutputFilePath(state.compilerOptions)!, currentDirectory)); const fileNames: string[] = []; const fileNameToFileId = new Map(); let fileIdsList: (readonly ProgramBuildInfoFileId[])[] | undefined; let fileNamesToFileIdListId: ESMap | undefined; const fileInfos = arrayFrom(state.fileInfos.entries(), ([key, value]): ProgramBuildInfoFileInfo => { // Ensure fileId const fileId = toFileId(key); Debug.assert(fileNames[fileId - 1] === relativeToBuildInfo(key)); const signature = state.currentAffectedFilesSignatures && state.currentAffectedFilesSignatures.get(key); const actualSignature = signature ?? value.signature; return value.version === actualSignature ? value.affectsGlobalScope || value.impliedFormat ? // If file version is same as signature, dont serialize signature { version: value.version, signature: undefined, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat } : // If file info only contains version and signature and both are same we can just write string value.version : actualSignature !== undefined ? // If signature is not same as version, encode signature in the fileInfo signature === undefined ? // If we havent computed signature, use fileInfo as is value : // Serialize fileInfo with new updated signature { version: value.version, signature, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat } : // Signature of the FileInfo is undefined, serialize it as false { version: value.version, signature: false, affectsGlobalScope: value.affectsGlobalScope, impliedFormat: value.impliedFormat }; }); let referencedMap: ProgramBuildInfoReferencedMap | undefined; if (state.referencedMap) { referencedMap = arrayFrom(state.referencedMap.keys()).sort(compareStringsCaseSensitive).map(key => [ toFileId(key), toFileIdListId(state.referencedMap!.getValues(key)!) ]); } let exportedModulesMap: ProgramBuildInfoReferencedMap | undefined; if (state.exportedModulesMap) { exportedModulesMap = mapDefined(arrayFrom(state.exportedModulesMap.keys()).sort(compareStringsCaseSensitive), key => { if (state.currentAffectedFilesExportedModulesMap) { if (state.currentAffectedFilesExportedModulesMap.deletedKeys()?.has(key)) { return undefined; } const newValue = state.currentAffectedFilesExportedModulesMap.getValues(key); if (newValue) { return [toFileId(key), toFileIdListId(newValue)]; } } // Not in temporary cache, use existing value return [toFileId(key), toFileIdListId(state.exportedModulesMap!.getValues(key)!)]; }); } let semanticDiagnosticsPerFile: ProgramBuildInfoDiagnostic[] | undefined; if (state.semanticDiagnosticsPerFile) { for (const key of arrayFrom(state.semanticDiagnosticsPerFile.keys()).sort(compareStringsCaseSensitive)) { const value = state.semanticDiagnosticsPerFile.get(key)!; (semanticDiagnosticsPerFile ||= []).push( value.length ? [ toFileId(key), state.hasReusableDiagnostic ? value as readonly ReusableDiagnostic[] : convertToReusableDiagnostics(value as readonly Diagnostic[], relativeToBuildInfo) ] : toFileId(key) ); } } let affectedFilesPendingEmit: ProgramBuilderInfoFilePendingEmit[] | undefined; if (state.affectedFilesPendingEmit) { const seenFiles = new Set(); for (const path of state.affectedFilesPendingEmit.slice(state.affectedFilesPendingEmitIndex).sort(compareStringsCaseSensitive)) { if (tryAddToSet(seenFiles, path)) { (affectedFilesPendingEmit ||= []).push([toFileId(path), state.affectedFilesPendingEmitKind!.get(path)!]); } } } return { fileNames, fileInfos, options: convertToProgramBuildInfoCompilerOptions(state.compilerOptions, relativeToBuildInfoEnsuringAbsolutePath), fileIdsList, referencedMap, exportedModulesMap, semanticDiagnosticsPerFile, affectedFilesPendingEmit, }; function relativeToBuildInfoEnsuringAbsolutePath(path: string) { return relativeToBuildInfo(getNormalizedAbsolutePath(path, currentDirectory)); } function relativeToBuildInfo(path: string) { return ensurePathIsNonModuleName(getRelativePathFromDirectory(buildInfoDirectory, path, getCanonicalFileName)); } function toFileId(path: Path): ProgramBuildInfoFileId { let fileId = fileNameToFileId.get(path); if (fileId === undefined) { fileNames.push(relativeToBuildInfo(path)); fileNameToFileId.set(path, fileId = fileNames.length as ProgramBuildInfoFileId); } return fileId; } function toFileIdListId(set: ReadonlySet): ProgramBuildInfoFileIdListId { const fileIds = arrayFrom(set.keys(), toFileId).sort(compareValues); const key = fileIds.join(); let fileIdListId = fileNamesToFileIdListId?.get(key); if (fileIdListId === undefined) { (fileIdsList ||= []).push(fileIds); (fileNamesToFileIdListId ||= new Map()).set(key, fileIdListId = fileIdsList.length as ProgramBuildInfoFileIdListId); } return fileIdListId; } } function convertToProgramBuildInfoCompilerOptions(options: CompilerOptions, relativeToBuildInfo: (path: string) => string) { let result: CompilerOptions | undefined; const { optionsNameMap } = getOptionsNameMap(); for (const name of getOwnKeys(options).sort(compareStringsCaseSensitive)) { const optionKey = name.toLowerCase(); const optionInfo = optionsNameMap.get(optionKey); if (optionInfo?.affectsEmit || optionInfo?.affectsSemanticDiagnostics || // We need to store `strict`, even though it won't be examined directly, so that the // flags it controls (e.g. `strictNullChecks`) will be retrieved correctly from the buildinfo optionKey === "strict" || // We need to store these to determine whether `lib` files need to be rechecked. optionKey === "skiplibcheck" || optionKey === "skipdefaultlibcheck") { (result ||= {})[name] = convertToReusableCompilerOptionValue( optionInfo, options[name] as CompilerOptionsValue, relativeToBuildInfo ); } } return result; } function convertToReusableCompilerOptionValue(option: CommandLineOption | undefined, value: CompilerOptionsValue, relativeToBuildInfo: (path: string) => string) { if (option) { if (option.type === "list") { const values = value as readonly (string | number)[]; if (option.element.isFilePath && values.length) { return values.map(relativeToBuildInfo); } } else if (option.isFilePath) { return relativeToBuildInfo(value as string); } } return value; } function convertToReusableDiagnostics(diagnostics: readonly Diagnostic[], relativeToBuildInfo: (path: string) => string): readonly ReusableDiagnostic[] { Debug.assert(!!diagnostics.length); return diagnostics.map(diagnostic => { const result: ReusableDiagnostic = convertToReusableDiagnosticRelatedInformation(diagnostic, relativeToBuildInfo); result.reportsUnnecessary = diagnostic.reportsUnnecessary; result.reportDeprecated = diagnostic.reportsDeprecated; result.source = diagnostic.source; result.skippedOn = diagnostic.skippedOn; const { relatedInformation } = diagnostic; result.relatedInformation = relatedInformation ? relatedInformation.length ? relatedInformation.map(r => convertToReusableDiagnosticRelatedInformation(r, relativeToBuildInfo)) : [] : undefined; return result; }); } function convertToReusableDiagnosticRelatedInformation(diagnostic: DiagnosticRelatedInformation, relativeToBuildInfo: (path: string) => string): ReusableDiagnosticRelatedInformation { const { file } = diagnostic; return { ...diagnostic, file: file ? relativeToBuildInfo(file.resolvedPath) : undefined }; } export enum BuilderProgramKind { SemanticDiagnosticsBuilderProgram, EmitAndSemanticDiagnosticsBuilderProgram } export interface BuilderCreationParameters { newProgram: Program; host: BuilderProgramHost; oldProgram: BuilderProgram | undefined; configFileParsingDiagnostics: readonly Diagnostic[]; } export function getBuilderCreationParameters(newProgramOrRootNames: Program | readonly string[] | undefined, hostOrOptions: BuilderProgramHost | CompilerOptions | undefined, oldProgramOrHost?: BuilderProgram | CompilerHost, configFileParsingDiagnosticsOrOldProgram?: readonly Diagnostic[] | BuilderProgram, configFileParsingDiagnostics?: readonly Diagnostic[], projectReferences?: readonly ProjectReference[]): BuilderCreationParameters { let host: BuilderProgramHost; let newProgram: Program; let oldProgram: BuilderProgram; if (newProgramOrRootNames === undefined) { Debug.assert(hostOrOptions === undefined); host = oldProgramOrHost as CompilerHost; oldProgram = configFileParsingDiagnosticsOrOldProgram as BuilderProgram; Debug.assert(!!oldProgram); newProgram = oldProgram.getProgram(); } else if (isArray(newProgramOrRootNames)) { oldProgram = configFileParsingDiagnosticsOrOldProgram as BuilderProgram; newProgram = createProgram({ rootNames: newProgramOrRootNames, options: hostOrOptions as CompilerOptions, host: oldProgramOrHost as CompilerHost, oldProgram: oldProgram && oldProgram.getProgramOrUndefined(), configFileParsingDiagnostics, projectReferences }); host = oldProgramOrHost as CompilerHost; } else { newProgram = newProgramOrRootNames; host = hostOrOptions as BuilderProgramHost; oldProgram = oldProgramOrHost as BuilderProgram; configFileParsingDiagnostics = configFileParsingDiagnosticsOrOldProgram as readonly Diagnostic[]; } return { host, newProgram, oldProgram, configFileParsingDiagnostics: configFileParsingDiagnostics || emptyArray }; } export function createBuilderProgram(kind: BuilderProgramKind.SemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): SemanticDiagnosticsBuilderProgram; export function createBuilderProgram(kind: BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram, builderCreationParameters: BuilderCreationParameters): EmitAndSemanticDiagnosticsBuilderProgram; export function createBuilderProgram(kind: BuilderProgramKind, { newProgram, host, oldProgram, configFileParsingDiagnostics }: BuilderCreationParameters) { // Return same program if underlying program doesnt change let oldState = oldProgram && oldProgram.getState(); if (oldState && newProgram === oldState.program && configFileParsingDiagnostics === newProgram.getConfigFileParsingDiagnostics()) { newProgram = undefined!; // TODO: GH#18217 oldState = undefined; return oldProgram; } /** * Create the canonical file name for identity */ const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); /** * Computing hash to for signature verification */ const computeHash = maybeBind(host, host.createHash); let state = createBuilderProgramState(newProgram, getCanonicalFileName, oldState, host.disableUseFileVersionAsSignature); let backupState: BuilderProgramState | undefined; newProgram.getProgramBuildInfo = () => getProgramBuildInfo(state, getCanonicalFileName); // To ensure that we arent storing any references to old program or new program without state newProgram = undefined!; // TODO: GH#18217 oldProgram = undefined; oldState = undefined; const getState = () => state; const builderProgram = createRedirectedBuilderProgram(getState, configFileParsingDiagnostics); builderProgram.getState = getState; builderProgram.backupState = () => { Debug.assert(backupState === undefined); backupState = cloneBuilderProgramState(state); }; builderProgram.restoreState = () => { state = Debug.checkDefined(backupState); backupState = undefined; }; builderProgram.getAllDependencies = sourceFile => BuilderState.getAllDependencies(state, Debug.checkDefined(state.program), sourceFile); builderProgram.getSemanticDiagnostics = getSemanticDiagnostics; builderProgram.emit = emit; builderProgram.releaseProgram = () => { releaseCache(state); backupState = undefined; }; if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) { (builderProgram as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; } else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { (builderProgram as EmitAndSemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile; (builderProgram as EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile; builderProgram.emitBuildInfo = emitBuildInfo; } else { notImplemented(); } return builderProgram; function emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult { if (state.buildInfoEmitPending) { const result = Debug.checkDefined(state.program).emitBuildInfo(writeFile || maybeBind(host, host.writeFile), cancellationToken); state.buildInfoEmitPending = false; return result; } return emitSkippedWithNoDiagnostics; } /** * 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 emitNextAffectedFile(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): AffectedFileResult { let affected = getNextAffectedFile(state, cancellationToken, computeHash, host); let emitKind = BuilderFileEmit.Full; let isPendingEmitFile = false; if (!affected) { if (!outFile(state.compilerOptions)) { const pendingAffectedFile = getNextAffectedFilePendingEmit(state); if (!pendingAffectedFile) { if (!state.buildInfoEmitPending) { return undefined; } const affected = Debug.checkDefined(state.program); return toAffectedFileEmitResult( state, // When whole program is affected, do emit only once (eg when --out or --outFile is specified) // Otherwise just affected file affected.emitBuildInfo(writeFile || maybeBind(host, host.writeFile), cancellationToken), affected, /*emitKind*/ BuilderFileEmit.Full, /*isPendingEmitFile*/ false, /*isBuildInfoEmit*/ true ); } ({ affectedFile: affected, emitKind } = pendingAffectedFile); isPendingEmitFile = true; } else { const program = Debug.checkDefined(state.program); if (state.programEmitComplete) return undefined; affected = program; } } return toAffectedFileEmitResult( state, // When whole program is affected, do emit only once (eg when --out or --outFile is specified) // Otherwise just affected file Debug.checkDefined(state.program).emit( affected === state.program ? undefined : affected as SourceFile, affected !== state.program && getEmitDeclarations(state.compilerOptions) && !customTransformers ? getWriteFileUpdatingSignatureCallback(writeFile) : writeFile || maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles || emitKind === BuilderFileEmit.DtsOnly, customTransformers ), affected, emitKind, isPendingEmitFile, ); } function getWriteFileUpdatingSignatureCallback(writeFile: WriteFileCallback | undefined): WriteFileCallback { return (fileName, text, writeByteOrderMark, onError, sourceFiles, data) => { if (isDeclarationFileName(fileName)) { Debug.assert(sourceFiles?.length === 1); const file = sourceFiles[0]; const info = state.fileInfos.get(file.resolvedPath)!; const signature = state.currentAffectedFilesSignatures?.get(file.resolvedPath) || info.signature; if (signature === file.version) { const newSignature = (computeHash || generateDjb2Hash)(data?.sourceMapUrlPos !== undefined ? text.substring(0, data.sourceMapUrlPos) : text); if (newSignature !== file.version) { // Update it if (host.storeFilesChangingSignatureDuringEmit) (state.filesChangingSignature ||= new Set()).add(file.resolvedPath); if (state.exportedModulesMap) BuilderState.updateExportedModules(file, file.exportedModulesFromDeclarationEmit, state.currentAffectedFilesExportedModulesMap ||= BuilderState.createManyToManyPathMap()); if (state.affectedFiles && state.affectedFilesIndex! < state.affectedFiles.length) { state.currentAffectedFilesSignatures!.set(file.resolvedPath, newSignature); } else { info.signature = newSignature; if (state.exportedModulesMap) BuilderState.updateExportedFilesMapFromCache(state, state.currentAffectedFilesExportedModulesMap); } } } } if (writeFile) writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); else if (host.writeFile) host.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); else state.program!.writeFile(fileName, text, writeByteOrderMark, onError, sourceFiles, data); }; } /** * 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); } const result = handleNoEmitOptions(builderProgram, targetSourceFile, writeFile, cancellationToken); if (result) return result; // Emit only affected files if using builder for emit if (!targetSourceFile) { if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) { // Emit and report any errors we ran into. let sourceMaps: SourceMapEmitResult[] = []; let emitSkipped = false; let diagnostics: Diagnostic[] | undefined; 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 }; } // In non Emit builder, clear affected files pending emit else if (state.affectedFilesPendingEmitKind?.size) { Debug.assert(kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram); // State can clear affected files pending emit if if (!emitOnlyDtsFiles // If we are doing complete emit, affected files pending emit can be cleared // If every file pending emit is pending on only dts emit || every(state.affectedFilesPendingEmit, (path, index) => index < state.affectedFilesPendingEmitIndex! || state.affectedFilesPendingEmitKind!.get(path) === BuilderFileEmit.DtsOnly)) { clearAffectedFilesPendingEmit(state); } } } return Debug.checkDefined(state.program).emit( targetSourceFile, !outFile(state.compilerOptions) && getEmitDeclarations(state.compilerOptions) && !customTransformers ? getWriteFileUpdatingSignatureCallback(writeFile) : writeFile || maybeBind(host, 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(cancellationToken?: CancellationToken, ignoreSourceFile?: (sourceFile: SourceFile) => boolean): AffectedFileResult { while (true) { const affected = getNextAffectedFile(state, cancellationToken, computeHash, host); if (!affected) { // Done return undefined; } else if (affected === state.program) { // When whole program is affected, get all semantic diagnostics (eg when --out or --outFile is specified) return toAffectedFileResult( state, state.program.getSemanticDiagnostics(/*targetSourceFile*/ undefined, cancellationToken), affected ); } // Add file to affected file pending emit to handle for later emit time // Apart for emit builder do this for tsbuildinfo, do this for non emit builder when noEmit is set as tsbuildinfo is written and reused between emitters if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram || state.compilerOptions.noEmit || state.compilerOptions.noEmitOnError) { addToAffectedFilesPendingEmit(state, (affected as SourceFile).resolvedPath, BuilderFileEmit.Full); } // Get diagnostics for the affected file if its not ignored if (ignoreSourceFile && ignoreSourceFile(affected as SourceFile)) { // Get next affected file doneWithAffectedFile(state, affected); continue; } return toAffectedFileResult( state, getSemanticDiagnosticsOfFile(state, affected as SourceFile, cancellationToken), affected ); } } /** * 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 */ function getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] { assertSourceFileOkWithoutNextAffectedCall(state, sourceFile); const compilerOptions = Debug.checkDefined(state.program).getCompilerOptions(); if (outFile(compilerOptions)) { Debug.assert(!state.semanticDiagnosticsPerFile); // We dont need to cache the diagnostics just return them from program return Debug.checkDefined(state.program).getSemanticDiagnostics(sourceFile, cancellationToken); } if (sourceFile) { return getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken); } // When semantic builder asks for diagnostics of the whole program, // ensure that all the affected files are handled // eslint-disable-next-line no-empty while (getSemanticDiagnosticsOfNextAffectedFile(cancellationToken)) { } let diagnostics: Diagnostic[] | undefined; for (const sourceFile of Debug.checkDefined(state.program).getSourceFiles()) { diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(state, sourceFile, cancellationToken)); } return diagnostics || emptyArray; } } function addToAffectedFilesPendingEmit(state: BuilderProgramState, affectedFilePendingEmit: Path, kind: BuilderFileEmit) { if (!state.affectedFilesPendingEmit) state.affectedFilesPendingEmit = []; if (!state.affectedFilesPendingEmitKind) state.affectedFilesPendingEmitKind = new Map(); const existingKind = state.affectedFilesPendingEmitKind.get(affectedFilePendingEmit); state.affectedFilesPendingEmit.push(affectedFilePendingEmit); state.affectedFilesPendingEmitKind.set(affectedFilePendingEmit, existingKind || kind); // affectedFilesPendingEmitIndex === undefined // - means the emit state.affectedFilesPendingEmit was undefined before adding current affected files // so start from 0 as array would be affectedFilesPendingEmit // else, continue to iterate from existing index, the current set is appended to existing files if (state.affectedFilesPendingEmitIndex === undefined) { state.affectedFilesPendingEmitIndex = 0; } } export function toBuilderStateFileInfo(fileInfo: ProgramBuildInfoFileInfo): BuilderState.FileInfo { return isString(fileInfo) ? { version: fileInfo, signature: fileInfo, affectsGlobalScope: undefined, impliedFormat: undefined } : isString(fileInfo.signature) ? fileInfo as BuilderState.FileInfo : { version: fileInfo.version, signature: fileInfo.signature === false ? undefined : fileInfo.version, affectsGlobalScope: fileInfo.affectsGlobalScope, impliedFormat: fileInfo.impliedFormat }; } export function createBuildProgramUsingProgramBuildInfo(program: ProgramBuildInfo, buildInfoPath: string, host: ReadBuildProgramHost): EmitAndSemanticDiagnosticsBuilderProgram { const buildInfoDirectory = getDirectoryPath(getNormalizedAbsolutePath(buildInfoPath, host.getCurrentDirectory())); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames()); const filePaths = program.fileNames.map(toPath); const filePathsSetList = program.fileIdsList?.map(fileIds => new Set(fileIds.map(toFilePath))); const fileInfos = new Map(); program.fileInfos.forEach((fileInfo, index) => fileInfos.set(toFilePath(index + 1 as ProgramBuildInfoFileId), toBuilderStateFileInfo(fileInfo))); const state: ReusableBuilderProgramState = { fileInfos, compilerOptions: program.options ? convertToOptionsWithAbsolutePaths(program.options, toAbsolutePath) : {}, referencedMap: toManyToManyPathMap(program.referencedMap), exportedModulesMap: toManyToManyPathMap(program.exportedModulesMap), semanticDiagnosticsPerFile: program.semanticDiagnosticsPerFile && arrayToMap(program.semanticDiagnosticsPerFile, value => toFilePath(isNumber(value) ? value : value[0]), value => isNumber(value) ? emptyArray : value[1]), hasReusableDiagnostic: true, affectedFilesPendingEmit: map(program.affectedFilesPendingEmit, value => toFilePath(value[0])), affectedFilesPendingEmitKind: program.affectedFilesPendingEmit && arrayToMap(program.affectedFilesPendingEmit, value => toFilePath(value[0]), value => value[1]), affectedFilesPendingEmitIndex: program.affectedFilesPendingEmit && 0, }; return { getState: () => state, backupState: noop, restoreState: noop, getProgram: notImplemented, getProgramOrUndefined: returnUndefined, releaseProgram: noop, getCompilerOptions: () => state.compilerOptions, getSourceFile: notImplemented, getSourceFiles: notImplemented, getOptionsDiagnostics: notImplemented, getGlobalDiagnostics: notImplemented, getConfigFileParsingDiagnostics: notImplemented, getSyntacticDiagnostics: notImplemented, getDeclarationDiagnostics: notImplemented, getSemanticDiagnostics: notImplemented, emit: notImplemented, getAllDependencies: notImplemented, getCurrentDirectory: notImplemented, emitNextAffectedFile: notImplemented, getSemanticDiagnosticsOfNextAffectedFile: notImplemented, emitBuildInfo: notImplemented, close: noop, }; function toPath(path: string) { return ts.toPath(path, buildInfoDirectory, getCanonicalFileName); } function toAbsolutePath(path: string) { return getNormalizedAbsolutePath(path, buildInfoDirectory); } function toFilePath(fileId: ProgramBuildInfoFileId) { return filePaths[fileId - 1]; } function toFilePathsSet(fileIdsListId: ProgramBuildInfoFileIdListId) { return filePathsSetList![fileIdsListId - 1]; } function toManyToManyPathMap(referenceMap: ProgramBuildInfoReferencedMap | undefined): BuilderState.ManyToManyPathMap | undefined { if (!referenceMap) { return undefined; } const map = BuilderState.createManyToManyPathMap(); referenceMap.forEach(([fileId, fileIdListId]) => map.set(toFilePath(fileId), toFilePathsSet(fileIdListId)) ); return map; } } export function createRedirectedBuilderProgram(getState: () => { program: Program | undefined; compilerOptions: CompilerOptions; }, configFileParsingDiagnostics: readonly Diagnostic[]): BuilderProgram { return { getState: notImplemented, backupState: noop, restoreState: noop, getProgram, getProgramOrUndefined: () => getState().program, releaseProgram: () => getState().program = undefined, getCompilerOptions: () => getState().compilerOptions, getSourceFile: fileName => getProgram().getSourceFile(fileName), getSourceFiles: () => getProgram().getSourceFiles(), getOptionsDiagnostics: cancellationToken => getProgram().getOptionsDiagnostics(cancellationToken), getGlobalDiagnostics: cancellationToken => getProgram().getGlobalDiagnostics(cancellationToken), getConfigFileParsingDiagnostics: () => configFileParsingDiagnostics, getSyntacticDiagnostics: (sourceFile, cancellationToken) => getProgram().getSyntacticDiagnostics(sourceFile, cancellationToken), getDeclarationDiagnostics: (sourceFile, cancellationToken) => getProgram().getDeclarationDiagnostics(sourceFile, cancellationToken), getSemanticDiagnostics: (sourceFile, cancellationToken) => getProgram().getSemanticDiagnostics(sourceFile, cancellationToken), emit: (sourceFile, writeFile, cancellationToken, emitOnlyDts, customTransformers) => getProgram().emit(sourceFile, writeFile, cancellationToken, emitOnlyDts, customTransformers), emitBuildInfo: (writeFile, cancellationToken) => getProgram().emitBuildInfo(writeFile, cancellationToken), getAllDependencies: notImplemented, getCurrentDirectory: () => getProgram().getCurrentDirectory(), close: noop, }; function getProgram() { return Debug.checkDefined(getState().program); } } }