365 lines
12 KiB
Go
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()
|
|
}
|
|
}
|