Files
2026-07-09 16:50:43 -04:00

454 lines
15 KiB
Go

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)
}