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

301 lines
12 KiB
Go

package incremental
import (
"context"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"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/tsoptions"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/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
} 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(), emitDiagnostics)
}
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(), diagnostics)
}
}
}
}
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 {
// 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.GetTypeCheckerForFile(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)
}