package incremental import ( "context" "fmt" "slices" "strings" "github.com/microsoft/typescript-go/internal/ast" "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/diagnostics" "github.com/microsoft/typescript-go/internal/json" "github.com/microsoft/typescript-go/internal/outputpaths" "github.com/microsoft/typescript-go/internal/packagejson" "github.com/microsoft/typescript-go/internal/tracing" "github.com/microsoft/typescript-go/internal/tspath" ) type SignatureUpdateKind byte const ( SignatureUpdateKindComputedDts SignatureUpdateKind = iota SignatureUpdateKindStoredAtEmit SignatureUpdateKindUsedVersion ) type Program struct { snapshot *snapshot program *compiler.Program host Host // Testing data testingData *TestingData } var _ compiler.ProgramLike = (*Program)(nil) func NewProgram(program *compiler.Program, oldProgram *Program, host Host, testing bool) *Program { incrementalProgram := &Program{ snapshot: programToSnapshot(program, oldProgram, testing), program: program, host: host, } if testing { incrementalProgram.testingData = &TestingData{} incrementalProgram.testingData.SemanticDiagnosticsPerFile = &incrementalProgram.snapshot.semanticDiagnosticsPerFile if oldProgram != nil { incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &oldProgram.snapshot.semanticDiagnosticsPerFile } else { incrementalProgram.testingData.OldProgramSemanticDiagnosticsPerFile = &collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName]{} } incrementalProgram.testingData.UpdatedSignatureKinds = make(map[tspath.Path]SignatureUpdateKind) } return incrementalProgram } type TestingData struct { SemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] OldProgramSemanticDiagnosticsPerFile *collections.SyncMap[tspath.Path, *DiagnosticsOrBuildInfoDiagnosticsWithFileName] UpdatedSignatureKinds map[tspath.Path]SignatureUpdateKind } func (p *Program) GetTestingData() *TestingData { return p.testingData } func (p *Program) panicIfNoProgram(method string) { if p.program == nil { panic(method + ": should not be called without program") } } func (p *Program) GetProgram() *compiler.Program { p.panicIfNoProgram("GetProgram") return p.program } func (p *Program) HasChangedDtsFile() bool { return p.snapshot.hasChangedDtsFile } // Options implements compiler.AnyProgram interface. func (p *Program) Options() *core.CompilerOptions { return p.snapshot.options } // CommonSourceDirectory implements compiler.AnyProgram interface. func (p *Program) CommonSourceDirectory() string { p.panicIfNoProgram("CommonSourceDirectory") return p.program.CommonSourceDirectory() } // Program implements compiler.AnyProgram interface. func (p *Program) Program() *compiler.Program { p.panicIfNoProgram("Program") return p.program } // IsSourceFileDefaultLibrary implements compiler.AnyProgram interface. func (p *Program) IsSourceFileDefaultLibrary(path tspath.Path) bool { p.panicIfNoProgram("IsSourceFileDefaultLibrary") return p.program.IsSourceFileDefaultLibrary(path) } // GetSourceFiles implements compiler.AnyProgram interface. func (p *Program) GetSourceFiles() []*ast.SourceFile { p.panicIfNoProgram("GetSourceFiles") return p.program.GetSourceFiles() } // GetSourceFile implements compiler.AnyProgram interface. func (p *Program) GetSourceFile(path string) *ast.SourceFile { p.panicIfNoProgram("GetSourceFile") return p.program.GetSourceFile(path) } // GetConfigFileParsingDiagnostics implements compiler.AnyProgram interface. func (p *Program) GetConfigFileParsingDiagnostics() []*ast.Diagnostic { p.panicIfNoProgram("GetConfigFileParsingDiagnostics") return p.program.GetConfigFileParsingDiagnostics() } // GetSyntacticDiagnostics implements compiler.AnyProgram interface. func (p *Program) GetSyntacticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetSyntacticDiagnostics") return p.program.GetSyntacticDiagnostics(ctx, file) } // GetBindDiagnostics implements compiler.AnyProgram interface. func (p *Program) GetBindDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetBindDiagnostics") return p.program.GetBindDiagnostics(ctx, file) } func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic { p.panicIfNoProgram("GetProgramDiagnostics") return p.program.GetProgramDiagnostics() } func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic { p.panicIfNoProgram("GetGlobalDiagnostics") return p.program.GetGlobalDiagnostics(ctx) } // GetSemanticDiagnostics implements compiler.AnyProgram interface. func (p *Program) GetSemanticDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetSemanticDiagnostics") if p.snapshot.options.NoCheck.IsTrue() { return nil } // Ensure all the diagnsotics are cached p.collectSemanticDiagnosticsOfAffectedFiles(ctx, file) if ctx.Err() != nil { return nil } // Return result from cache if file != nil { return p.getSemanticDiagnosticsOfFile(file) } var diagnostics []*ast.Diagnostic for _, file := range p.program.GetSourceFiles() { diagnostics = append(diagnostics, p.getSemanticDiagnosticsOfFile(file)...) } return diagnostics } func (p *Program) getSemanticDiagnosticsOfFile(file *ast.SourceFile) []*ast.Diagnostic { cachedDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) if !ok { panic("After handling all the affected files, there shouldnt be more changes") } return slices.Concat( compiler.FilterNoEmitSemanticDiagnostics(cachedDiagnostics.getDiagnostics(p.program, file), p.snapshot.options), p.program.GetIncludeProcessorDiagnostics(file), ) } // GetDeclarationDiagnostics implements compiler.AnyProgram interface. func (p *Program) GetDeclarationDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetDeclarationDiagnostics") result := emitFiles(ctx, p, compiler.EmitOptions{ TargetSourceFile: file, }, true) if result != nil { return result.Diagnostics } return nil } // GetSuggestionDiagnostics implements compiler.AnyProgram interface. func (p *Program) GetSuggestionDiagnostics(ctx context.Context, file *ast.SourceFile) []*ast.Diagnostic { p.panicIfNoProgram("GetSuggestionDiagnostics") return p.program.GetSuggestionDiagnostics(ctx, file) // TODO: incremental suggestion diagnostics (only relevant in editor incremental builder?) } // GetModeForUsageLocation implements compiler.AnyProgram interface. func (p *Program) Emit(ctx context.Context, options compiler.EmitOptions) *compiler.EmitResult { p.panicIfNoProgram("Emit") var result *compiler.EmitResult if p.snapshot.options.NoEmit.IsTrue() { result = &compiler.EmitResult{EmitSkipped: true} } else { result = compiler.HandleNoEmitOnError(ctx, p, options.TargetSourceFile) if ctx.Err() != nil { return nil } } if result != nil { if options.TargetSourceFile != nil { return result } // Emit buildInfo and combine result buildInfoResult := p.emitBuildInfo(ctx, options) if buildInfoResult != nil { result.Diagnostics = append(result.Diagnostics, buildInfoResult.Diagnostics...) result.EmittedFiles = append(result.EmittedFiles, buildInfoResult.EmittedFiles...) } return result } return emitFiles(ctx, p, options, false) } // Handle affected files and cache the semantic diagnostics for all of them or the file asked for func (p *Program) collectSemanticDiagnosticsOfAffectedFiles(ctx context.Context, file *ast.SourceFile) { if p.snapshot.canUseIncrementalState() { // Get all affected files collectAllAffectedFiles(ctx, p) if ctx.Err() != nil { return } if p.snapshot.semanticDiagnosticsPerFile.Size() == len(p.program.GetSourceFiles()) { // If we have all the files, return } } var affectedFiles []*ast.SourceFile if file != nil { _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) if ok { return } affectedFiles = []*ast.SourceFile{file} } else { for _, file := range p.program.GetSourceFiles() { if _, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()); !ok { affectedFiles = append(affectedFiles, file) } } } // Get their diagnostics and cache them diagnosticsPerFile := p.program.GetSemanticDiagnosticsWithoutNoEmitFiltering(ctx, affectedFiles) // commit changes if no err if ctx.Err() != nil { return } // Commit changes to snapshot for file, diagnostics := range diagnosticsPerFile { p.snapshot.semanticDiagnosticsPerFile.Store(file.Path(), &DiagnosticsOrBuildInfoDiagnosticsWithFileName{diagnostics: diagnostics}) } if p.snapshot.semanticDiagnosticsPerFile.Size() == len(p.program.GetSourceFiles()) && p.snapshot.checkPending && !p.snapshot.options.NoCheck.IsTrue() { p.snapshot.checkPending = false } p.snapshot.buildInfoEmitPending.Store(true) } func (p *Program) emitBuildInfo(ctx context.Context, options compiler.EmitOptions) *compiler.EmitResult { if tr := p.program.Tracing(); tr != nil { defer tr.Push(tracing.PhaseEmit, "emitBuildInfo", nil, true)() } buildInfoFileName := outputpaths.GetBuildInfoFileName(p.snapshot.options, tspath.ComparePathsOptions{ CurrentDirectory: p.program.GetCurrentDirectory(), UseCaseSensitiveFileNames: p.program.UseCaseSensitiveFileNames(), }) if buildInfoFileName == "" || p.program.IsEmitBlocked(buildInfoFileName) { return nil } if p.snapshot.hasErrors == core.TSUnknown { p.ensureHasErrorsForState(ctx, p.program) if p.snapshot.hasErrors != p.snapshot.hasErrorsFromOldState || p.snapshot.hasSemanticErrors != p.snapshot.hasSemanticErrorsFromOldState { p.snapshot.buildInfoEmitPending.Store(true) } } if p.snapshot.packageJsons == nil { p.ensurePackageJsonsForState() if !slices.Equal(p.snapshot.packageJsons, p.snapshot.packageJsonsFromOldState) || !slices.Equal(p.snapshot.missingPackageJsons, p.snapshot.missingPackageJsonsFromOldState) { p.snapshot.buildInfoEmitPending.Store(true) } } if !p.snapshot.buildInfoEmitPending.Load() { return nil } if ctx.Err() != nil { return nil } buildInfo := snapshotToBuildInfo(p.snapshot, p.program, buildInfoFileName) text, err := json.Marshal(buildInfo) if err != nil { panic(fmt.Sprintf("Failed to marshal build info: %v", err)) } if options.WriteFile != nil { err = options.WriteFile(buildInfoFileName, string(text), &compiler.WriteFileData{ BuildInfo: buildInfo, }) } else { err = p.program.Host().FS().WriteFile(buildInfoFileName, string(text)) } if err != nil { return &compiler.EmitResult{ EmitSkipped: true, Diagnostics: []*ast.Diagnostic{ ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, buildInfoFileName, err.Error()), }, } } p.snapshot.buildInfoEmitPending.Store(false) return &compiler.EmitResult{ EmitSkipped: false, EmittedFiles: []string{buildInfoFileName}, } } func (p *Program) ensureHasErrorsForState(ctx context.Context, program *compiler.Program) { var hasIncludeProcessingDiagnostics func() bool var hasEmitDiagnostics bool if p.snapshot.canUseIncrementalState() { if slices.ContainsFunc(program.GetSourceFiles(), func(file *ast.SourceFile) bool { if _, ok := p.snapshot.emitDiagnosticsPerFile.Load(file.Path()); ok { // emit diagnostics will be encoded in buildInfo; return true } if hasIncludeProcessingDiagnostics == nil && len(p.program.GetIncludeProcessorDiagnostics(file)) > 0 { hasIncludeProcessingDiagnostics = func() bool { return true } } return false }) { hasEmitDiagnostics = true } if hasIncludeProcessingDiagnostics == nil { hasIncludeProcessingDiagnostics = func() bool { return false } } } else { hasEmitDiagnostics = p.snapshot.hasEmitDiagnostics hasIncludeProcessingDiagnostics = func() bool { return slices.ContainsFunc(program.GetSourceFiles(), func(file *ast.SourceFile) bool { return len(p.program.GetIncludeProcessorDiagnostics(file)) > 0 }) } } if hasEmitDiagnostics { // Record this for only non incremental build info p.snapshot.hasErrors = core.IfElse(p.snapshot.options.IsIncremental(), core.TSFalse, core.TSTrue) // Dont need to encode semantic errors state since the emit diagnostics are encoded p.snapshot.hasSemanticErrors = false return } if hasIncludeProcessingDiagnostics() || len(program.GetConfigFileParsingDiagnostics()) > 0 || len(program.GetSyntacticDiagnostics(ctx, nil)) > 0 || len(program.GetProgramDiagnostics()) > 0 || len(program.GetGlobalDiagnostics(ctx)) > 0 { p.snapshot.hasErrors = core.TSTrue // Dont need to encode semantic errors state since the syntax and program diagnostics are encoded as present p.snapshot.hasSemanticErrors = false return } p.snapshot.hasErrors = core.TSFalse // Check semantic and emit diagnostics first as we dont need to ask program about it if slices.ContainsFunc(p.program.GetSourceFiles(), func(file *ast.SourceFile) bool { semanticDiagnostics, ok := p.snapshot.semanticDiagnosticsPerFile.Load(file.Path()) if !ok { // Missing semantic diagnostics in cache will be encoded in incremental buildInfo return p.snapshot.options.IsIncremental() } if len(semanticDiagnostics.diagnostics) > 0 || len(semanticDiagnostics.buildInfoDiagnostics) > 0 { // cached semantic diagnostics will be encoded in buildInfo return true } return false }) { // Because semantic diagnostics are recorded in buildInfo, we dont need to encode hasErrors in incremental buildInfo // But encode as errors in non incremental buildInfo p.snapshot.hasSemanticErrors = !p.snapshot.options.IsIncremental() } } func (p *Program) ensurePackageJsonsForState() { config := tspath.GetDirectoryPath(p.program.CommandLine().ConfigName()) if config != "" { p.program.PackageJsonCacheEntries(func(key tspath.Path, value *packagejson.InfoCacheEntry) bool { if value == nil { return true } packageJson := tspath.CombinePaths(value.PackageDirectory, "package.json") if value.Exists() || value.DirectoryExists { packageJson = p.program.Host().FS().Realpath(packageJson) } if value.Exists() { p.snapshot.packageJsons = append(p.snapshot.packageJsons, packageJson) } else if strings.Contains(packageJson, "/node_modules/") { p.snapshot.missingPackageJsons = append(p.snapshot.missingPackageJsons, packageJson) } return true }) } p.snapshot.packageJsons = normalizePackageJsons(p.snapshot.packageJsons) p.snapshot.missingPackageJsons = normalizePackageJsons(p.snapshot.missingPackageJsons) } func normalizePackageJsons(packageJsons []string) []string { if packageJsons == nil { return make([]string, 0) } slices.Sort(packageJsons) return core.Deduplicate(packageJsons) } func (p *Program) PackageJsonLookupPaths() []string { config := tspath.GetDirectoryPath(p.program.CommandLine().ConfigName()) if config == "" { return nil } var packageJsons []string p.program.PackageJsonCacheEntries(func(key tspath.Path, value *packagejson.InfoCacheEntry) bool { if value == nil { return true } packageJson := tspath.CombinePaths(value.PackageDirectory, "package.json") if value.Exists() || value.DirectoryExists { packageJson = p.program.Host().FS().Realpath(packageJson) } packageJsons = append(packageJsons, packageJson) return true }) slices.Sort(packageJsons) return core.Deduplicate(packageJsons) }