package incremental import ( "context" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/binder" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" ) func programToSnapshot(program *compiler.Program, oldProgram *Program, hashWithText bool) *snapshot { if oldProgram != nil && oldProgram.program == program { return oldProgram.snapshot } snapshot := &snapshot{ options: program.Options(), hashWithText: hashWithText, checkPending: program.Options().NoCheck.IsTrue(), } to := &toProgramSnapshot{ program: program, oldProgram: oldProgram, snapshot: snapshot, } if to.snapshot.canUseIncrementalState() { to.reuseFromOldProgram() to.computeProgramFileChanges() to.handleFileDelete() to.handlePendingEmit() to.handlePendingCheck() } return snapshot } type toProgramSnapshot struct { program *compiler.Program oldProgram *Program snapshot *snapshot globalFileRemoved bool } func (t *toProgramSnapshot) reuseFromOldProgram() { if t.oldProgram != nil { if t.snapshot.options.Composite.IsTrue() { t.snapshot.latestChangedDtsFile = t.oldProgram.snapshot.latestChangedDtsFile } // Copy old snapshot's changed files set t.oldProgram.snapshot.changedFilesSet.Range(func(key tspath.Path) bool { t.snapshot.changedFilesSet.Add(key) return true }) t.oldProgram.snapshot.affectedFilesPendingEmit.Range(func(key tspath.Path, emitKind FileEmitKind) bool { t.snapshot.affectedFilesPendingEmit.Store(key, emitKind) return true }) t.snapshot.buildInfoEmitPending.Store(t.oldProgram.snapshot.buildInfoEmitPending.Load()) t.snapshot.hasErrorsFromOldState = t.oldProgram.snapshot.hasErrors t.snapshot.hasSemanticErrorsFromOldState = t.oldProgram.snapshot.hasSemanticErrors t.snapshot.packageJsonsFromOldState = t.oldProgram.snapshot.packageJsons t.snapshot.missingPackageJsonsFromOldState = t.oldProgram.snapshot.missingPackageJsons } else { t.snapshot.buildInfoEmitPending.Store(t.snapshot.options.IsIncremental()) } } func (t *toProgramSnapshot) computeProgramFileChanges() { canCopySemanticDiagnostics := t.oldProgram != nil && !tsoptions.CompilerOptionsAffectSemanticDiagnostics(t.oldProgram.snapshot.options, t.program.Options()) // We can only reuse emit signatures (i.e. .d.ts signatures) if the .d.ts file is unchanged, // which will eg be depedent on change in options like declarationDir and outDir options are unchanged. // We need to look in oldState.compilerOptions, rather than oldCompilerOptions (i.e.we need to disregard useOldState) because // oldCompilerOptions can be undefined if there was change in say module from None to some other option // which would make useOldState as false since we can now use reference maps that are needed to track what to emit, what to check etc // but that option change does not affect d.ts file name so emitSignatures should still be reused. canCopyEmitSignatures := t.snapshot.options.Composite.IsTrue() && t.oldProgram != nil && !tsoptions.CompilerOptionsAffectDeclarationPath(t.oldProgram.snapshot.options, t.program.Options()) copyDeclarationFileDiagnostics := canCopySemanticDiagnostics && t.snapshot.options.SkipLibCheck.IsTrue() == t.oldProgram.snapshot.options.SkipLibCheck.IsTrue() copyLibFileDiagnostics := copyDeclarationFileDiagnostics && t.snapshot.options.SkipDefaultLibCheck.IsTrue() == t.oldProgram.snapshot.options.SkipDefaultLibCheck.IsTrue() files := t.program.GetSourceFiles() wg := core.NewWorkGroup(t.program.SingleThreaded()) for _, file := range files { wg.Queue(func() { version := t.snapshot.computeHash(file.Text()) impliedNodeFormat := t.program.GetSourceFileMetaData(file.Path()).ImpliedNodeFormat affectsGlobalScope := fileAffectsGlobalScope(file) var signature string newReferences := getReferencedFiles(t.program, file) if newReferences != nil { t.snapshot.referencedMap.storeReferences(file.Path(), newReferences) } if t.oldProgram != nil { if oldFileInfo, ok := t.oldProgram.snapshot.fileInfos.Load(file.Path()); ok { signature = oldFileInfo.signature if oldFileInfo.version != version || oldFileInfo.affectsGlobalScope != affectsGlobalScope || oldFileInfo.impliedNodeFormat != impliedNodeFormat { t.snapshot.addFileToChangeSet(file.Path()) } else if oldReferences, _ := t.oldProgram.snapshot.referencedMap.getReferences(file.Path()); !newReferences.Equals(oldReferences) { // Referenced files changed t.snapshot.addFileToChangeSet(file.Path()) } else if newReferences != nil { for refPath := range newReferences.Keys() { if t.program.GetSourceFileByPath(refPath) == nil { if _, ok := t.oldProgram.snapshot.fileInfos.Load(refPath); ok { // Referenced file was deleted in the new program t.snapshot.addFileToChangeSet(file.Path()) break } } } } } else { t.snapshot.addFileToChangeSet(file.Path()) } if !t.snapshot.changedFilesSet.Has(file.Path()) { if emitDiagnostics, ok := t.oldProgram.snapshot.emitDiagnosticsPerFile.Load(file.Path()); ok { t.snapshot.emitDiagnosticsPerFile.Store(file.Path(), repopulateDiagnosticsOfFile(emitDiagnostics, t.program, file)) } if canCopySemanticDiagnostics { if (!file.IsDeclarationFile || copyDeclarationFileDiagnostics) && (!t.program.IsSourceFileDefaultLibrary(file.Path()) || copyLibFileDiagnostics) { // Unchanged file copy diagnostics if diagnostics, ok := t.oldProgram.snapshot.semanticDiagnosticsPerFile.Load(file.Path()); ok { t.snapshot.semanticDiagnosticsPerFile.Store(file.Path(), repopulateDiagnosticsOfFile(diagnostics, t.program, file)) } } } } if canCopyEmitSignatures { if oldEmitSignature, ok := t.oldProgram.snapshot.emitSignatures.Load(file.Path()); ok { t.snapshot.emitSignatures.Store(file.Path(), oldEmitSignature.getNewEmitSignature(t.oldProgram.snapshot.options, t.snapshot.options)) } } } else { t.snapshot.addFileToAffectedFilesPendingEmit(file.Path(), GetFileEmitKind(t.snapshot.options)) signature = version } t.snapshot.fileInfos.Store(file.Path(), &FileInfo{ version: version, signature: signature, affectsGlobalScope: affectsGlobalScope, impliedNodeFormat: impliedNodeFormat, }) }) } wg.RunAndWait() } func (t *toProgramSnapshot) handleFileDelete() { if t.oldProgram != nil { // If the global file is removed, add all files as changed t.oldProgram.snapshot.fileInfos.Range(func(filePath tspath.Path, oldInfo *FileInfo) bool { if _, ok := t.snapshot.fileInfos.Load(filePath); !ok { if oldInfo.affectsGlobalScope { for _, file := range t.snapshot.getAllFilesExcludingDefaultLibraryFile(t.program, nil) { t.snapshot.addFileToChangeSet(file.Path()) } t.globalFileRemoved = true } else { t.snapshot.buildInfoEmitPending.Store(true) } return false } return true }) } } func (t *toProgramSnapshot) handlePendingEmit() { if t.oldProgram != nil && !t.globalFileRemoved { // If options affect emit, then we need to do complete emit per compiler options // otherwise only the js or dts that needs to emitted because its different from previously emitted options var pendingEmitKind FileEmitKind if tsoptions.CompilerOptionsAffectEmit(t.oldProgram.snapshot.options, t.snapshot.options) { pendingEmitKind = GetFileEmitKind(t.snapshot.options) } else { pendingEmitKind = getPendingEmitKindWithOptions(t.snapshot.options, t.oldProgram.snapshot.options) } if pendingEmitKind != FileEmitKindNone { // Add all files to affectedFilesPendingEmit since emit changed for _, file := range t.program.GetSourceFiles() { // Add to affectedFilesPending emit only if not changed since any changed file will do full emit if !t.snapshot.changedFilesSet.Has(file.Path()) { t.snapshot.addFileToAffectedFilesPendingEmit(file.Path(), pendingEmitKind) } } t.snapshot.buildInfoEmitPending.Store(true) } } } func (t *toProgramSnapshot) handlePendingCheck() { if t.oldProgram != nil && t.snapshot.semanticDiagnosticsPerFile.Size() != len(t.program.GetSourceFiles()) && t.oldProgram.snapshot.checkPending != t.snapshot.checkPending { t.snapshot.buildInfoEmitPending.Store(true) } } func fileAffectsGlobalScope(file *ast.SourceFile) bool { binder.BindSourceFile(file) // if file contains anything that augments to global scope we need to build them as if // they are global files as well as module if core.Some(file.ModuleAugmentations, func(augmentation *ast.ModuleName) bool { return ast.IsGlobalScopeAugmentation(augmentation.Parent) }) { return true } if ast.IsExternalOrCommonJSModule(file) || ast.IsJsonSourceFile(file) { return false } // For script files that contains only ambient external modules, although they are not actually external module files, // they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, // there are no point to rebuild all script files if these special files have changed. However, if any statement // in the file is not ambient external module, we treat it as a regular script file. return file.Statements != nil && file.Statements.Nodes != nil && core.Some(file.Statements.Nodes, func(stmt *ast.Node) bool { return !ast.IsModuleWithStringLiteralName(stmt) }) } func addReferencedFilesFromSymbol(file *ast.SourceFile, referencedFiles *collections.Set[tspath.Path], symbol *ast.Symbol) { if symbol == nil { return } for _, declaration := range symbol.Declarations { fileOfDecl := ast.GetSourceFileOfNode(declaration) if fileOfDecl == nil { continue } if file != fileOfDecl { referencedFiles.Add(fileOfDecl.Path()) } } } // Get the module source file and all augmenting files from the import name node from file func addReferencedFilesFromImportLiteral(file *ast.SourceFile, referencedFiles *collections.Set[tspath.Path], checker *checker.Checker, importName *ast.LiteralLikeNode) { symbol := checker.GetSymbolAtLocation(importName) addReferencedFilesFromSymbol(file, referencedFiles, symbol) } // Gets the path to reference file from file name, it could be resolvedPath if present otherwise path func addReferencedFileFromFileName(program *compiler.Program, fileName string, referencedFiles *collections.Set[tspath.Path], sourceFileDirectory string) { if redirect := program.GetParseFileRedirect(fileName); redirect != "" { referencedFiles.Add(tspath.ToPath(redirect, program.GetCurrentDirectory(), program.UseCaseSensitiveFileNames())) } else { referencedFiles.Add(tspath.ToPath(fileName, sourceFileDirectory, program.UseCaseSensitiveFileNames())) } } // Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true func getReferencedFiles(program *compiler.Program, file *ast.SourceFile) *collections.Set[tspath.Path] { referencedFiles := collections.Set[tspath.Path]{} // We need to use a set here since the code can contain the same import twice, // but that will only be one dependency. // To avoid invernal conversion, the key of the referencedFiles map must be of type Path checker, done := program.GetTypeCheckerForFileExclusive(context.TODO(), file) defer done() for _, importName := range file.Imports() { addReferencedFilesFromImportLiteral(file, &referencedFiles, checker, importName) } sourceFileDirectory := tspath.GetDirectoryPath(file.FileName()) // Handle triple slash references for _, referencedFile := range file.ReferencedFiles { addReferencedFileFromFileName(program, referencedFile.FileName, &referencedFiles, sourceFileDirectory) } // Handle type reference directives if typeRefsInFile, ok := program.GetResolvedTypeReferenceDirectives()[file.Path()]; ok { for _, typeRef := range typeRefsInFile { if typeRef.ResolvedFileName != "" { addReferencedFileFromFileName(program, typeRef.ResolvedFileName, &referencedFiles, sourceFileDirectory) } } } // Add module augmentation as references for _, moduleName := range file.ModuleAugmentations { if !ast.IsStringLiteral(moduleName) { continue } addReferencedFilesFromImportLiteral(file, &referencedFiles, checker, moduleName) } // From ambient modules for _, ambientModule := range checker.GetAmbientModules() { addReferencedFilesFromSymbol(file, &referencedFiles, ambientModule) } return core.IfElse(referencedFiles.Len() > 0, &referencedFiles, nil) } // repopulateDiagnosticsOfFile repopulates diagnostic chains that depend on program state. // When diagnostics are copied from a previous build, their message chains may reference // stale program state (e.g., resolved module alternate results, package.json scope). // This function recomputes those chains using the current program's state. func repopulateDiagnosticsOfFile(diags *DiagnosticsOrBuildInfoDiagnosticsWithFileName, p *compiler.Program, file *ast.SourceFile) *DiagnosticsOrBuildInfoDiagnosticsWithFileName { if diags.diagnostics != nil { repopulated := repopulateDiagnosticsList(diags.diagnostics, p, file) if repopulated == nil { return diags } return &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: repopulated} } // buildInfoDiagnostics will be repopulated via toDiagnostic's repopulateInfo handling return diags } // repopulateDiagnosticsList repopulates diagnostic chains in a list of diagnostics. // Returns nil if no diagnostics needed repopulation (i.e., no changes were made). func repopulateDiagnosticsList(diags []*ast.Diagnostic, p *compiler.Program, file *ast.SourceFile) []*ast.Diagnostic { changed := false result := make([]*ast.Diagnostic, len(diags)) for i, d := range diags { repopulated := repopulateDiagnosticMessageChain(d.MessageChain(), p, file) if repopulated != nil { clone := d.Clone() clone.SetMessageChain(repopulated) result[i] = clone changed = true } else { result[i] = d } } if !changed { return nil } return result } // repopulateDiagnosticMessageChain repopulates chains that have repopulate info. // Returns nil if no changes were made. func repopulateDiagnosticMessageChain(chain []*ast.Diagnostic, p *compiler.Program, file *ast.SourceFile) []*ast.Diagnostic { if len(chain) == 0 { return nil } changed := false result := make([]*ast.Diagnostic, len(chain)) for i, c := range chain { if c.RepopulateInfo() != nil { // Convert to buildInfoDiagnosticWithFileName and repopulate b := &buildInfoDiagnosticWithFileName{ pos: c.Pos(), end: c.End(), code: c.Code(), category: c.Category(), messageKey: c.MessageKey(), messageArgs: c.MessageArgs(), repopulateInfo: c.RepopulateInfo(), } // Recursively handle nested chains for _, nested := range c.MessageChain() { b.messageChain = append(b.messageChain, astDiagToBuildInfoDiag(nested)) } result[i] = repopulateDiagnosticChain(b, p, file) changed = true } else { // Check nested chains nested := repopulateDiagnosticMessageChain(c.MessageChain(), p, file) if nested != nil { clone := c.Clone() clone.SetMessageChain(nested) result[i] = clone changed = true } else { result[i] = c } } } if !changed { return nil } return result } func astDiagToBuildInfoDiag(d *ast.Diagnostic) *buildInfoDiagnosticWithFileName { b := &buildInfoDiagnosticWithFileName{ pos: d.Pos(), end: d.End(), code: d.Code(), category: d.Category(), messageKey: d.MessageKey(), messageArgs: d.MessageArgs(), repopulateInfo: d.RepopulateInfo(), } for _, nested := range d.MessageChain() { b.messageChain = append(b.messageChain, astDiagToBuildInfoDiag(nested)) } return b }