vendor tsgo
This commit is contained in:
393
tools/tsgo/internal/execute/incremental/affectedfileshandler.go
Normal file
393
tools/tsgo/internal/execute/incremental/affectedfileshandler.go
Normal file
@@ -0,0 +1,393 @@
|
||||
package incremental
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"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/tspath"
|
||||
)
|
||||
|
||||
type dtsMayChange map[tspath.Path]FileEmitKind
|
||||
|
||||
func (c dtsMayChange) addFileToAffectedFilesPendingEmit(filePath tspath.Path, emitKind FileEmitKind) {
|
||||
c[filePath] = emitKind
|
||||
}
|
||||
|
||||
type updatedSignature struct {
|
||||
mu sync.Mutex
|
||||
signature string
|
||||
kind SignatureUpdateKind
|
||||
}
|
||||
|
||||
type affectedFilesHandler struct {
|
||||
ctx context.Context
|
||||
program *Program
|
||||
hasAllFilesExcludingDefaultLibraryFile atomic.Bool
|
||||
updatedSignatures collections.SyncMap[tspath.Path, *updatedSignature]
|
||||
dtsMayChange []dtsMayChange
|
||||
filesToRemoveDiagnostics collections.SyncSet[tspath.Path]
|
||||
cleanedDiagnosticsOfLibFiles sync.Once
|
||||
seenFileAndReferences collections.SyncMap[tspath.Path, bool]
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) getDtsMayChange(affectedFilePath tspath.Path, affectedFileEmitKind FileEmitKind) dtsMayChange {
|
||||
result := dtsMayChange(map[tspath.Path]FileEmitKind{affectedFilePath: affectedFileEmitKind})
|
||||
h.dtsMayChange = append(h.dtsMayChange, result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) isChangedSignature(path tspath.Path) bool {
|
||||
newSignature, _ := h.updatedSignatures.Load(path)
|
||||
// This method is called after updating signatures of that path, so signature is present in updatedSignatures
|
||||
// And is already calculated, so no need to lock and unlock mutex on the entry
|
||||
oldInfo, _ := h.program.snapshot.fileInfos.Load(path)
|
||||
return newSignature.signature != oldInfo.signature
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) removeSemanticDiagnosticsOf(path tspath.Path) {
|
||||
h.filesToRemoveDiagnostics.Add(path)
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) removeDiagnosticsOfLibraryFiles() {
|
||||
h.cleanedDiagnosticsOfLibFiles.Do(func() {
|
||||
for _, file := range h.program.GetSourceFiles() {
|
||||
if h.program.program.IsSourceFileDefaultLibrary(file.Path()) && !h.program.program.SkipTypeChecking(file, true) {
|
||||
h.removeSemanticDiagnosticsOf(file.Path())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) computeDtsSignature(file *ast.SourceFile) string {
|
||||
var signature string
|
||||
h.program.program.Emit(h.ctx, compiler.EmitOptions{
|
||||
TargetSourceFile: file,
|
||||
EmitOnly: compiler.EmitOnlyForcedDts,
|
||||
WriteFile: func(fileName string, text string, data *compiler.WriteFileData) error {
|
||||
if !tspath.IsDeclarationFileName(fileName) {
|
||||
panic("File extension for signature expected to be dts, got : " + fileName)
|
||||
}
|
||||
signature = h.program.snapshot.computeSignatureWithDiagnostics(file, text, data)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return signature
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) updateShapeSignature(file *ast.SourceFile, useFileVersionAsSignature bool) bool {
|
||||
update := &updatedSignature{}
|
||||
update.mu.Lock()
|
||||
defer update.mu.Unlock()
|
||||
// If we have cached the result for this file, that means hence forth we should assume file shape is uptodate
|
||||
if existing, ok := h.updatedSignatures.LoadOrStore(file.Path(), update); ok {
|
||||
// Ensure calculations for existing ones are complete before using the value
|
||||
existing.mu.Lock()
|
||||
defer existing.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
info, _ := h.program.snapshot.fileInfos.Load(file.Path())
|
||||
prevSignature := info.signature
|
||||
if !file.IsDeclarationFile && !useFileVersionAsSignature {
|
||||
update.signature = h.computeDtsSignature(file)
|
||||
}
|
||||
// Default is to use file version as signature
|
||||
if update.signature == "" {
|
||||
update.signature = info.version
|
||||
update.kind = SignatureUpdateKindUsedVersion
|
||||
}
|
||||
return update.signature != prevSignature
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.Path) []*ast.SourceFile {
|
||||
file := h.program.program.GetSourceFileByPath(path)
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !h.updateShapeSignature(file, false) {
|
||||
return []*ast.SourceFile{file}
|
||||
}
|
||||
|
||||
if info, _ := h.program.snapshot.fileInfos.Load(file.Path()); info.affectsGlobalScope {
|
||||
h.hasAllFilesExcludingDefaultLibraryFile.Store(true)
|
||||
h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, file)
|
||||
}
|
||||
|
||||
if h.program.snapshot.options.IsolatedModules.IsTrue() {
|
||||
return []*ast.SourceFile{file}
|
||||
}
|
||||
|
||||
// Now we need to if each file in the referencedBy list has a shape change as well.
|
||||
// Because if so, its own referencedBy files need to be saved as well to make the
|
||||
// emitting result consistent with files on disk.
|
||||
seenFileNamesMap := h.forEachFileReferencedBy(
|
||||
file,
|
||||
func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) {
|
||||
// If the current file is not nil and has a shape change, we need to queue it for processing
|
||||
if currentFile != nil && h.updateShapeSignature(currentFile, false) {
|
||||
return true, false
|
||||
}
|
||||
return false, false
|
||||
},
|
||||
)
|
||||
// Return array of values that needs emit
|
||||
return core.Filter(slices.Collect(maps.Values(seenFileNamesMap)), func(file *ast.SourceFile) bool {
|
||||
return file != nil
|
||||
})
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) forEachFileReferencedBy(file *ast.SourceFile, fn func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool)) map[tspath.Path]*ast.SourceFile {
|
||||
// Now we need to if each file in the referencedBy list has a shape change as well.
|
||||
// Because if so, its own referencedBy files need to be saved as well to make the
|
||||
// emitting result consistent with files on disk.
|
||||
seenFileNamesMap := map[tspath.Path]*ast.SourceFile{}
|
||||
// Start with the paths this file was referenced by
|
||||
seenFileNamesMap[file.Path()] = file
|
||||
queue := slices.Collect(h.program.snapshot.referencedMap.getReferencedBy(file.Path()))
|
||||
for len(queue) > 0 {
|
||||
currentPath := queue[len(queue)-1]
|
||||
queue = queue[:len(queue)-1]
|
||||
if _, ok := seenFileNamesMap[currentPath]; !ok {
|
||||
currentFile := h.program.program.GetSourceFileByPath(currentPath)
|
||||
seenFileNamesMap[currentPath] = currentFile
|
||||
queueForFile, fastReturn := fn(currentFile, currentPath)
|
||||
if fastReturn {
|
||||
return seenFileNamesMap
|
||||
}
|
||||
if queueForFile {
|
||||
for ref := range h.program.snapshot.referencedMap.getReferencedBy(currentFile.Path()) {
|
||||
queue = append(queue, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return seenFileNamesMap
|
||||
}
|
||||
|
||||
// Handles semantic diagnostics and dts emit for affectedFile and files, that are referencing modules that export entities from affected file
|
||||
// This is because even though js emit doesnt change, dts emit / type used can change resulting in need for dts emit and js change
|
||||
func (h *affectedFilesHandler) handleDtsMayChangeOfAffectedFile(dtsMayChange dtsMayChange, affectedFile *ast.SourceFile) {
|
||||
h.removeSemanticDiagnosticsOf(affectedFile.Path())
|
||||
|
||||
// If affected files is everything except default library, then nothing more to do
|
||||
if h.hasAllFilesExcludingDefaultLibraryFile.Load() {
|
||||
h.removeDiagnosticsOfLibraryFiles()
|
||||
// When a change affects the global scope, all files are considered to be affected without updating their signature
|
||||
// That means when affected file is handled, its signature can be out of date
|
||||
// To avoid this, ensure that we update the signature for any affected file in this scenario.
|
||||
h.updateShapeSignature(affectedFile, false)
|
||||
return
|
||||
}
|
||||
|
||||
if h.program.snapshot.options.AssumeChangesOnlyAffectDirectDependencies.IsTrue() {
|
||||
return
|
||||
}
|
||||
|
||||
// Iterate on referencing modules that export entities from affected file and delete diagnostics and add pending emit
|
||||
// If there was change in signature (dts output) for the changed file,
|
||||
// then only we need to handle pending file emit
|
||||
if !h.program.snapshot.changedFilesSet.Has(affectedFile.Path()) ||
|
||||
!h.isChangedSignature(affectedFile.Path()) {
|
||||
return
|
||||
}
|
||||
|
||||
// At this point affectedFile is actually one of the changed files
|
||||
// that has some change in its .d.ts signature.
|
||||
|
||||
// Since isolated modules dont change js files, files affected by change in signature is itself
|
||||
// But we need to cleanup semantic diagnostics and queue dts emit for affected files
|
||||
if h.program.snapshot.options.IsolatedModules.IsTrue() {
|
||||
h.forEachFileReferencedBy(
|
||||
affectedFile,
|
||||
func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) {
|
||||
if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, currentPath /*invalidateJsFiles*/, false) {
|
||||
return false, true
|
||||
}
|
||||
h.handleDtsMayChangeOf(dtsMayChange, currentPath /*invalidateJsFiles*/, false)
|
||||
if h.isChangedSignature(currentPath) {
|
||||
return true, false
|
||||
}
|
||||
return false, false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
invalidateJsFiles := false
|
||||
var typeChecker *checker.Checker
|
||||
var done func()
|
||||
// If exported const enum, we need to ensure that js files are emitted as well since the const enum value changed
|
||||
if affectedFile.Symbol != nil {
|
||||
for _, exported := range affectedFile.Symbol.Exports {
|
||||
if exported.Flags&ast.SymbolFlagsConstEnum != 0 {
|
||||
invalidateJsFiles = true
|
||||
break
|
||||
}
|
||||
if typeChecker == nil {
|
||||
typeChecker, done = h.program.program.GetTypeCheckerForFileExclusive(h.ctx, affectedFile)
|
||||
}
|
||||
aliased := checker.SkipAlias(exported, typeChecker)
|
||||
if aliased == exported {
|
||||
continue
|
||||
}
|
||||
if (aliased.Flags & ast.SymbolFlagsConstEnum) != 0 {
|
||||
if slices.ContainsFunc(aliased.Declarations, func(d *ast.Node) bool {
|
||||
return ast.GetSourceFileOfNode(d) == affectedFile
|
||||
}) {
|
||||
invalidateJsFiles = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if done != nil {
|
||||
done()
|
||||
}
|
||||
|
||||
// Go through files that reference affected file and handle dts emit and semantic diagnostics for them and their references
|
||||
for fileReferencingChangedFile := range h.program.snapshot.referencedMap.getReferencedBy(affectedFile.Path()) {
|
||||
if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, fileReferencingChangedFile, invalidateJsFiles) {
|
||||
return
|
||||
}
|
||||
// Since references of changed file = affected files - we would have already handled d.ts emit and semantic diagnostics
|
||||
// for those files. Now we need to handle files referencing those affected files to ensure correctness.
|
||||
for fileReferencingAffectedFile := range h.program.snapshot.referencedMap.getReferencedBy(fileReferencingChangedFile) {
|
||||
if h.handleDtsMayChangeOfFileAndReferences(dtsMayChange, fileReferencingAffectedFile, invalidateJsFiles) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) handleDtsMayChangeOfFileAndReferences(dtsMayChange dtsMayChange, filePath tspath.Path, invalidateJsFiles bool) bool {
|
||||
if existing, loaded := h.seenFileAndReferences.LoadOrStore(filePath, invalidateJsFiles); loaded && (existing || !invalidateJsFiles) {
|
||||
return false
|
||||
} else if loaded && invalidateJsFiles {
|
||||
h.seenFileAndReferences.Store(filePath, true)
|
||||
}
|
||||
|
||||
if h.handleDtsMayChangeOfGlobalScope(dtsMayChange, filePath, invalidateJsFiles) {
|
||||
return true
|
||||
}
|
||||
h.handleDtsMayChangeOf(dtsMayChange, filePath, invalidateJsFiles)
|
||||
|
||||
// Remove the diagnostics of files that import this file and
|
||||
// any files that are referenced by it (directly or indirectly)
|
||||
for referencingFilePath := range h.program.snapshot.referencedMap.getReferencedBy(filePath) {
|
||||
if h.handleDtsMayChangeOfFileAndReferences(dtsMayChange, referencingFilePath, invalidateJsFiles) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) handleDtsMayChangeOfGlobalScope(dtsMayChange dtsMayChange, filePath tspath.Path, invalidateJsFiles bool) bool {
|
||||
if info, ok := h.program.snapshot.fileInfos.Load(filePath); !ok || !info.affectsGlobalScope {
|
||||
return false
|
||||
}
|
||||
// Every file needs to be handled
|
||||
for _, file := range h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, nil) {
|
||||
h.handleDtsMayChangeOf(dtsMayChange, file.Path(), invalidateJsFiles)
|
||||
}
|
||||
h.removeDiagnosticsOfLibraryFiles()
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle the dts may change, so they need to be added to pending emit if dts emit is enabled,
|
||||
// Also we need to make sure signature is updated for these files
|
||||
func (h *affectedFilesHandler) handleDtsMayChangeOf(dtsMayChange dtsMayChange, path tspath.Path, invalidateJsFiles bool) {
|
||||
if h.program.snapshot.changedFilesSet.Has(path) {
|
||||
return
|
||||
}
|
||||
file := h.program.program.GetSourceFileByPath(path)
|
||||
if file == nil {
|
||||
return
|
||||
}
|
||||
h.removeSemanticDiagnosticsOf(path)
|
||||
// Even though the js emit doesnt change and we are already handling dts emit and semantic diagnostics
|
||||
// we need to update the signature to reflect correctness of the signature(which is output d.ts emit) of this file
|
||||
// This ensures that we dont later during incremental builds considering wrong signature.
|
||||
// Eg where this also is needed to ensure that .tsbuildinfo generated by incremental build should be same as if it was first fresh build
|
||||
// But we avoid expensive full shape computation, as using file version as shape is enough for correctness.
|
||||
h.updateShapeSignature(file, true)
|
||||
// If not dts emit, nothing more to do
|
||||
if invalidateJsFiles {
|
||||
dtsMayChange.addFileToAffectedFilesPendingEmit(path, GetFileEmitKind(h.program.snapshot.options))
|
||||
} else if h.program.snapshot.options.GetEmitDeclarations() {
|
||||
dtsMayChange.addFileToAffectedFilesPendingEmit(path, core.IfElse(h.program.snapshot.options.DeclarationMap.IsTrue(), FileEmitKindAllDts, FileEmitKindDts))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *affectedFilesHandler) updateSnapshot() {
|
||||
if h.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
h.updatedSignatures.Range(func(filePath tspath.Path, update *updatedSignature) bool {
|
||||
if info, ok := h.program.snapshot.fileInfos.Load(filePath); ok {
|
||||
info.signature = update.signature
|
||||
if h.program.testingData != nil {
|
||||
h.program.testingData.UpdatedSignatureKinds[filePath] = update.kind
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
h.filesToRemoveDiagnostics.Range(func(file tspath.Path) bool {
|
||||
h.program.snapshot.semanticDiagnosticsPerFile.Delete(file)
|
||||
return true
|
||||
})
|
||||
for _, change := range h.dtsMayChange {
|
||||
for filePath, emitKind := range change {
|
||||
h.program.snapshot.addFileToAffectedFilesPendingEmit(filePath, emitKind)
|
||||
}
|
||||
}
|
||||
h.program.snapshot.changedFilesSet = collections.SyncSet[tspath.Path]{}
|
||||
h.program.snapshot.buildInfoEmitPending.Store(true)
|
||||
}
|
||||
|
||||
func collectAllAffectedFiles(ctx context.Context, program *Program) {
|
||||
if program.snapshot.changedFilesSet.Size() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
handler := affectedFilesHandler{ctx: ctx, program: program}
|
||||
wg := core.NewWorkGroup(handler.program.program.SingleThreaded())
|
||||
var result collections.SyncSet[*ast.SourceFile]
|
||||
program.snapshot.changedFilesSet.Range(func(file tspath.Path) bool {
|
||||
wg.Queue(func() {
|
||||
for _, affectedFile := range handler.getFilesAffectedBy(file) {
|
||||
result.Add(affectedFile)
|
||||
}
|
||||
})
|
||||
return true
|
||||
})
|
||||
wg.RunAndWait()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// For all the affected files, get all the files that would need to change their dts or js files,
|
||||
// update their diagnostics
|
||||
wg = core.NewWorkGroup(program.program.SingleThreaded())
|
||||
emitKind := GetFileEmitKind(program.snapshot.options)
|
||||
result.Range(func(file *ast.SourceFile) bool {
|
||||
// remove the cached semantic diagnostics and handle dts emit and js emit if needed
|
||||
dtsMayChange := handler.getDtsMayChange(file.Path(), emitKind)
|
||||
wg.Queue(func() {
|
||||
handler.handleDtsMayChangeOfAffectedFile(dtsMayChange, file)
|
||||
})
|
||||
return true
|
||||
})
|
||||
wg.RunAndWait()
|
||||
|
||||
// Update the snapshot with the new state
|
||||
handler.updateSnapshot()
|
||||
}
|
||||
Reference in New Issue
Block a user