package incremental import ( "context" "maps" "slices" "sync" "sync/atomic" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/collections" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/compiler" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" ) type dtsMayChange map[tspath.Path]FileEmitKind func (c dtsMayChange) addFileToAffectedFilesPendingEmit(filePath tspath.Path, emitKind FileEmitKind) { c[filePath] = emitKind } type updatedSignature struct { mu sync.Mutex signature string kind SignatureUpdateKind } type affectedFilesHandler struct { ctx context.Context program *Program hasAllFilesExcludingDefaultLibraryFile atomic.Bool updatedSignatures collections.SyncMap[tspath.Path, *updatedSignature] dtsMayChange []dtsMayChange filesToRemoveDiagnostics collections.SyncSet[tspath.Path] cleanedDiagnosticsOfLibFiles sync.Once seenFileAndExportsOfFile collections.SyncMap[tspath.Path, bool] } func (h *affectedFilesHandler) getDtsMayChange(affectedFilePath tspath.Path, affectedFileEmitKind FileEmitKind) dtsMayChange { result := dtsMayChange(map[tspath.Path]FileEmitKind{affectedFilePath: affectedFileEmitKind}) h.dtsMayChange = append(h.dtsMayChange, result) return result } func (h *affectedFilesHandler) isChangedSignature(path tspath.Path) bool { newSignature, _ := h.updatedSignatures.Load(path) // This method is called after updating signatures of that path, so signature is present in updatedSignatures // And is already calculated, so no need to lock and unlock mutex on the entry oldInfo, _ := h.program.snapshot.fileInfos.Load(path) return newSignature.signature != oldInfo.signature } func (h *affectedFilesHandler) removeSemanticDiagnosticsOf(path tspath.Path) { h.filesToRemoveDiagnostics.Add(path) } func (h *affectedFilesHandler) removeDiagnosticsOfLibraryFiles() { h.cleanedDiagnosticsOfLibFiles.Do(func() { for _, file := range h.program.GetSourceFiles() { if h.program.program.IsSourceFileDefaultLibrary(file.Path()) && !checker.SkipTypeChecking(file, h.program.snapshot.options, h.program.program, true) { h.removeSemanticDiagnosticsOf(file.Path()) } } }) } func (h *affectedFilesHandler) computeDtsSignature(file *ast.SourceFile) string { var signature string h.program.program.Emit(h.ctx, compiler.EmitOptions{ TargetSourceFile: file, EmitOnly: compiler.EmitOnlyForcedDts, WriteFile: func(fileName string, text string, writeByteOrderMark bool, data *compiler.WriteFileData) error { if !tspath.IsDeclarationFileName(fileName) { panic("File extension for signature expected to be dts, got : " + fileName) } signature = h.program.snapshot.computeSignatureWithDiagnostics(file, text, data) return nil }, }) return signature } func (h *affectedFilesHandler) updateShapeSignature(file *ast.SourceFile, useFileVersionAsSignature bool) bool { update := &updatedSignature{} update.mu.Lock() defer update.mu.Unlock() // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate if existing, ok := h.updatedSignatures.LoadOrStore(file.Path(), update); ok { // Ensure calculations for existing ones are complete before using the value existing.mu.Lock() defer existing.mu.Unlock() return false } info, _ := h.program.snapshot.fileInfos.Load(file.Path()) prevSignature := info.signature if !file.IsDeclarationFile && !useFileVersionAsSignature { update.signature = h.computeDtsSignature(file) } // Default is to use file version as signature if update.signature == "" { update.signature = info.version update.kind = SignatureUpdateKindUsedVersion } return update.signature != prevSignature } func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.Path) []*ast.SourceFile { file := h.program.program.GetSourceFileByPath(path) if file == nil { return nil } if !h.updateShapeSignature(file, false) { return []*ast.SourceFile{file} } if info, _ := h.program.snapshot.fileInfos.Load(file.Path()); info.affectsGlobalScope { h.hasAllFilesExcludingDefaultLibraryFile.Store(true) h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, file) } if h.program.snapshot.options.IsolatedModules.IsTrue() { return []*ast.SourceFile{file} } // Now we need to if each file in the referencedBy list has a shape change as well. // Because if so, its own referencedBy files need to be saved as well to make the // emitting result consistent with files on disk. seenFileNamesMap := h.forEachFileReferencedBy( file, func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) { // If the current file is not nil and has a shape change, we need to queue it for processing if currentFile != nil && h.updateShapeSignature(currentFile, false) { return true, false } return false, false }, ) // Return array of values that needs emit return core.Filter(slices.Collect(maps.Values(seenFileNamesMap)), func(file *ast.SourceFile) bool { return file != nil }) } func (h *affectedFilesHandler) forEachFileReferencedBy(file *ast.SourceFile, fn func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool)) map[tspath.Path]*ast.SourceFile { // Now we need to if each file in the referencedBy list has a shape change as well. // Because if so, its own referencedBy files need to be saved as well to make the // emitting result consistent with files on disk. seenFileNamesMap := map[tspath.Path]*ast.SourceFile{} // Start with the paths this file was referenced by seenFileNamesMap[file.Path()] = file queue := slices.Collect(h.program.snapshot.referencedMap.getReferencedBy(file.Path())) for len(queue) > 0 { currentPath := queue[len(queue)-1] queue = queue[:len(queue)-1] if _, ok := seenFileNamesMap[currentPath]; !ok { currentFile := h.program.program.GetSourceFileByPath(currentPath) seenFileNamesMap[currentPath] = currentFile queueForFile, fastReturn := fn(currentFile, currentPath) if fastReturn { return seenFileNamesMap } if queueForFile { for ref := range h.program.snapshot.referencedMap.getReferencedBy(currentFile.Path()) { queue = append(queue, ref) } } } } return seenFileNamesMap } // 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 func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dtsMayChange, affectedFile *ast.SourceFile) { h.removeSemanticDiagnosticsOf(affectedFile.Path()) // If affected files is everything except default library, then nothing more to do if h.hasAllFilesExcludingDefaultLibraryFile.Load() { h.removeDiagnosticsOfLibraryFiles() // 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. h.updateShapeSignature(affectedFile, false) return } if h.program.snapshot.options.AssumeChangesOnlyAffectDirectDependencies.IsTrue() { return } // Iterate on referencing modules that export entities from affected file and delete diagnostics and add pending emit // If there was change in signature (dts output) for the changed file, // then only we need to handle pending file emit if !h.program.snapshot.changedFilesSet.Has(affectedFile.Path()) || !h.isChangedSignature(affectedFile.Path()) { 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 h.program.snapshot.options.IsolatedModules.IsTrue() { h.forEachFileReferencedBy( affectedFile, func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) { if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, currentPath /*invalidateJsFiles*/, false) { return false, true } h.handleDtsMayChangeOf(dtsMayChange, currentPath /*invalidateJsFiles*/, false) if h.isChangedSignature(currentPath) { return true, false } return false, false }, ) } invalidateJsFiles := false var typeChecker *checker.Checker var done func() // If exported const enum, we need to ensure that js files are emitted as well since the const enum value changed if affectedFile.Symbol != nil { for _, exported := range affectedFile.Symbol.Exports { if exported.Flags&ast.SymbolFlagsConstEnum != 0 { invalidateJsFiles = true break } if typeChecker == nil { typeChecker, done = h.program.program.GetTypeCheckerForFile(h.ctx, affectedFile) } aliased := checker.SkipAlias(exported, typeChecker) if aliased == exported { continue } if (aliased.Flags & ast.SymbolFlagsConstEnum) != 0 { if slices.ContainsFunc(aliased.Declarations, func(d *ast.Node) bool { return ast.GetSourceFileOfNode(d) == affectedFile }) { invalidateJsFiles = true break } } } } if done != nil { done() } // Go through files that reference affected file and handle dts emit and semantic diagnostics for them and their references for exportedFromPath := range h.program.snapshot.referencedMap.getReferencedBy(affectedFile.Path()) { if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, exportedFromPath, invalidateJsFiles) { return } for filePath := range h.program.snapshot.referencedMap.getReferencedBy(exportedFromPath) { if h.handleDtsMayChangeOfFileAndExportsOfFile(dtsMayChange, filePath, invalidateJsFiles) { return } } } } func (h *affectedFilesHandler) handleDtsMayChangeOfFileAndExportsOfFile(dtsMayChange dtsMayChange, filePath tspath.Path, invalidateJsFiles bool) bool { if existing, loaded := h.seenFileAndExportsOfFile.LoadOrStore(filePath, invalidateJsFiles); loaded && (existing || !invalidateJsFiles) { return false } if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, filePath, invalidateJsFiles) { return true } h.handleDtsMayChangeOf(dtsMayChange, filePath, invalidateJsFiles) // Remove the diagnostics of files that import this file and handle all its exports too for referencingFilePath := range h.program.snapshot.referencedMap.getReferencedBy(filePath) { if h.handleDtsMayChangeOfFileAndExportsOfFile(dtsMayChange, referencingFilePath, invalidateJsFiles) { return true } } return false } func (h *affectedFilesHandler) handleDtsMayChangeOfGlobalScope(dtsMayChange dtsMayChange, filePath tspath.Path, invalidateJsFiles bool) bool { if info, ok := h.program.snapshot.fileInfos.Load(filePath); !ok || !info.affectsGlobalScope { return false } // Every file needs to be handled for _, file := range h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, nil) { h.handleDtsMayChangeOf(dtsMayChange, file.Path(), invalidateJsFiles) } h.removeDiagnosticsOfLibraryFiles() return true } // 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 func (h *affectedFilesHandler) handleDtsMayChangeOf(dtsMayChange dtsMayChange, path tspath.Path, invalidateJsFiles bool) { if h.program.snapshot.changedFilesSet.Has(path) { return } file := h.program.program.GetSourceFileByPath(path) if file == nil { return } h.removeSemanticDiagnosticsOf(path) // 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. h.updateShapeSignature(file, true) // If not dts emit, nothing more to do if invalidateJsFiles { dtsMayChange.addFileToAffectedFilesPendingEmit(path, GetFileEmitKind(h.program.snapshot.options)) } else if h.program.snapshot.options.GetEmitDeclarations() { dtsMayChange.addFileToAffectedFilesPendingEmit(path, core.IfElse(h.program.snapshot.options.DeclarationMap.IsTrue(), FileEmitKindAllDts, FileEmitKindDts)) } } func (h *affectedFilesHandler) updateSnapshot() { if h.ctx.Err() != nil { return } h.updatedSignatures.Range(func(filePath tspath.Path, update *updatedSignature) bool { if info, ok := h.program.snapshot.fileInfos.Load(filePath); ok { info.signature = update.signature if h.program.testingData != nil { h.program.testingData.UpdatedSignatureKinds[filePath] = update.kind } } return true }) h.filesToRemoveDiagnostics.Range(func(file tspath.Path) bool { h.program.snapshot.semanticDiagnosticsPerFile.Delete(file) return true }) for _, change := range h.dtsMayChange { for filePath, emitKind := range change { h.program.snapshot.addFileToAffectedFilesPendingEmit(filePath, emitKind) } } h.program.snapshot.changedFilesSet = collections.SyncSet[tspath.Path]{} h.program.snapshot.buildInfoEmitPending.Store(true) } func collectAllAffectedFiles(ctx context.Context, program *Program) { if program.snapshot.changedFilesSet.Size() == 0 { return } handler := affectedFilesHandler{ctx: ctx, program: program} wg := core.NewWorkGroup(handler.program.program.SingleThreaded()) var result collections.SyncSet[*ast.SourceFile] program.snapshot.changedFilesSet.Range(func(file tspath.Path) bool { wg.Queue(func() { for _, affectedFile := range handler.getFilesAffectedBy(file) { result.Add(affectedFile) } }) return true }) wg.RunAndWait() if ctx.Err() != nil { return } // For all the affected files, get all the files that would need to change their dts or js files, // update their diagnostics wg = core.NewWorkGroup(program.program.SingleThreaded()) emitKind := GetFileEmitKind(program.snapshot.options) result.Range(func(file *ast.SourceFile) bool { // remove the cached semantic diagnostics and handle dts emit and js emit if needed dtsMayChange := handler.getDtsMayChange(file.Path(), emitKind) wg.Queue(func() { handler.handleDtsMayChangeOfAffectedFile(dtsMayChange, file) }) return true }) wg.RunAndWait() // Update the snapshot with the new state handler.updateSnapshot() }