2025-10-15 10:12:44 +03:00

365 lines
12 KiB
Go

package incremental
import (
"context"
"fmt"
"slices"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"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/diagnostics"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/outputpaths"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"github.com/go-json-experiment/json"
)
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
}
// GetSourceFiles implements compiler.AnyProgram interface.
func (p *Program) GetSourceFiles() []*ast.SourceFile {
p.panicIfNoProgram("GetSourceFiles")
return p.program.GetSourceFiles()
}
// 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)
}
// GetOptionsDiagnostics implements compiler.AnyProgram interface.
func (p *Program) GetOptionsDiagnostics(ctx context.Context) []*ast.Diagnostic {
p.panicIfNoProgram("GetOptionsDiagnostics")
return p.program.GetOptionsDiagnostics(ctx)
}
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
}
// 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.GetSemanticDiagnosticsNoFilter(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 {
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.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), false, &compiler.WriteFileData{
BuildInfo: buildInfo,
})
} else {
err = p.program.Host().FS().WriteFile(buildInfoFileName, string(text), false)
}
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.GetOptionsDiagnostics(ctx)) > 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(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()
}
}