Skip to content

Add cleanup code for projects and script infos #1007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 5, 2025
2 changes: 1 addition & 1 deletion internal/project/ata.go
Original file line number Diff line number Diff line change
@@ -145,7 +145,7 @@ func (ti *TypingsInstaller) EnqueueInstallTypingsRequest(p *Project, typingsInfo
}

func (ti *TypingsInstaller) discoverAndInstallTypings(p *Project, typingsInfo *TypingsInfo, fileNames []string, projectRootPath string) {
ti.init((p))
ti.init(p)

cachedTypingPaths, newTypingNames, filesToWatch := DiscoverTypings(
p.FS(),
199 changes: 117 additions & 82 deletions internal/project/project.go
Original file line number Diff line number Diff line change
@@ -171,6 +171,7 @@ func NewConfiguredProject(configFileName string, configFilePath tspath.Path, hos
project.configFileName = configFileName
project.configFilePath = configFilePath
project.initialLoadPending = true
project.pendingReload = PendingReloadFull
client := host.Client()
if host.IsWatchEnabled() && client != nil {
project.rootFilesWatch = newWatchedFiles(project, lsproto.WatchKindChange|lsproto.WatchKindCreate|lsproto.WatchKindDelete, core.Identity, "root files")
@@ -193,6 +194,7 @@ func NewProject(name string, kind Kind, currentDirectory string, host ProjectHos
kind: kind,
currentDirectory: currentDirectory,
rootFileNames: &collections.OrderedMap[tspath.Path, string]{},
dirty: true,
}
project.comparePathsOptions = tspath.ComparePathsOptions{
CurrentDirectory: currentDirectory,
@@ -362,6 +364,16 @@ func (p *Project) updateWatchers(ctx context.Context) {
p.affectingLocationsWatch.update(ctx, affectingLocationGlobs)
}

func (p *Project) tryInvokeWildCardDirectories(fileName string, path tspath.Path) bool {
if p.kind == KindConfigured {
if p.rootFileNames.Has(path) || p.parsedCommandLine.MatchesFileName(fileName) {
p.SetPendingReload(PendingReloadFileNames)
return true
}
}
return false
}

// onWatchEventForNilScriptInfo is fired for watch events that are not the
// project tsconfig, and do not have a ScriptInfo for the associated file.
// This could be a case of one of the following:
@@ -371,14 +383,9 @@ func (p *Project) updateWatchers(ctx context.Context) {
// part of the project, e.g., a .js file in a project without --allowJs.
func (p *Project) onWatchEventForNilScriptInfo(fileName string) {
path := p.toPath(fileName)
if p.kind == KindConfigured {
if p.rootFileNames.Has(path) || p.parsedCommandLine.MatchesFileName(fileName) {
p.pendingReload = PendingReloadFileNames
p.markAsDirty()
return
}
if p.tryInvokeWildCardDirectories(fileName, path) {
return
}

if _, ok := p.failedLookupsWatch.data[path]; ok {
p.markAsDirty()
} else if _, ok := p.affectingLocationsWatch.data[path]; ok {
@@ -430,6 +437,15 @@ func (p *Project) MarkFileAsDirty(path tspath.Path) {
}
}

func (p *Project) SetPendingReload(level PendingReload) {
p.mu.Lock()
defer p.mu.Unlock()
if level > p.pendingReload {
p.pendingReload = level
p.markAsDirtyLocked()
}
}

func (p *Project) markAsDirty() {
p.mu.Lock()
defer p.mu.Unlock()
@@ -463,30 +479,30 @@ func (p *Project) updateGraph() bool {

start := time.Now()
p.Log("Starting updateGraph: Project: " + p.name)
var writeFileNames bool
oldProgram := p.program
p.initialLoadPending = false

if p.kind == KindConfigured && p.pendingReload != PendingReloadNone {
switch p.pendingReload {
case PendingReloadFileNames:
p.parsedCommandLine = tsoptions.ReloadFileNamesOfParsedCommandLine(p.parsedCommandLine, p.host.FS())
writeFileNames = p.setRootFiles(p.parsedCommandLine.FileNames())
p.setRootFiles(p.parsedCommandLine.FileNames())
p.programConfig = nil
p.pendingReload = PendingReloadNone
case PendingReloadFull:
if err := p.loadConfig(); err != nil {
err := p.LoadConfig()
if err != nil {
panic(fmt.Sprintf("failed to reload config: %v", err))
}
}
p.pendingReload = PendingReloadNone
}

oldProgramReused := p.updateProgram()
hasAddedOrRemovedFiles := p.hasAddedorRemovedFiles.Load()
p.hasAddedorRemovedFiles.Store(false)
p.dirty = false
p.dirtyFilePath = ""
if writeFileNames {
if hasAddedOrRemovedFiles {
p.Log(p.print(true /*writeFileNames*/, true /*writeFileExplanation*/, false /*writeFileVersionAndText*/, &strings.Builder{}))
} else if p.program != oldProgram {
p.Log("Different program with same set of root files")
@@ -496,6 +512,7 @@ func (p *Project) updateGraph() bool {
for _, oldSourceFile := range oldProgram.GetSourceFiles() {
if p.program.GetSourceFileByPath(oldSourceFile.Path()) == nil {
p.host.DocumentRegistry().ReleaseDocument(oldSourceFile, oldProgram.Options())
p.detachScriptInfoIfNotInferredRoot(oldSourceFile.Path())
}
}
}
@@ -707,7 +724,7 @@ func (p *Project) extractUnresolvedImportsFromSourceFile(file *ast.SourceFile, o
func (p *Project) UpdateTypingFiles(typingsInfo *TypingsInfo, typingFiles []string) {
p.mu.Lock()
defer p.mu.Unlock()
if p.typingsInfo != typingsInfo {
if p.isClosed() || p.typingsInfo != typingsInfo {
return
}

@@ -737,6 +754,12 @@ func (p *Project) UpdateTypingFiles(typingsInfo *TypingsInfo, typingFiles []stri
}

func (p *Project) WatchTypingLocations(files []string) {
p.mu.Lock()
defer p.mu.Unlock()
if p.isClosed() {
return
}

client := p.host.Client()
if !p.host.IsWatchEnabled() || client == nil {
return
@@ -804,23 +827,13 @@ func (p *Project) isRoot(info *ScriptInfo) bool {
return p.rootFileNames.Has(info.path)
}

func (p *Project) RemoveFile(info *ScriptInfo, fileExists bool, detachFromProject bool) {
func (p *Project) RemoveFile(info *ScriptInfo, fileExists bool) {
p.mu.Lock()
defer p.mu.Unlock()
p.removeFile(info, fileExists, detachFromProject)
p.markAsDirtyLocked()
}

func (p *Project) removeFile(info *ScriptInfo, fileExists bool, detachFromProject bool) {
if p.isRoot(info) {
switch p.kind {
case KindInferred:
p.rootFileNames.Delete(info.path)
p.typeAcquisition = nil
p.programConfig = nil
case KindConfigured:
p.pendingReload = PendingReloadFileNames
}
if p.isRoot(info) && p.kind == KindInferred {
p.rootFileNames.Delete(info.path)
p.typeAcquisition = nil
p.programConfig = nil
}
p.onFileAddedOrRemoved()

@@ -832,49 +845,34 @@ func (p *Project) removeFile(info *ScriptInfo, fileExists bool, detachFromProjec
// this.resolutionCache.invalidateResolutionOfFile(info.path);
// }
// this.cachedUnresolvedImportsPerFile.delete(info.path);
if detachFromProject {
info.detachFromProject(p)
}
p.markAsDirtyLocked()
}

func (p *Project) AddRoot(info *ScriptInfo) {
func (p *Project) AddInferredProjectRoot(info *ScriptInfo) {
p.mu.Lock()
defer p.mu.Unlock()
p.addRoot(info)
if p.isRoot(info) {
panic("script info is already a root")
Comment on lines +854 to +858
Copy link
Preview

Copilot AI Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The function panics if the script info is already a root; consider whether a graceful error handling strategy might be more appropriate for production usage.

Copilot uses AI. Check for mistakes.

}
p.rootFileNames.Set(info.path, info.fileName)
p.programConfig = nil
p.markAsDirtyLocked()
}

func (p *Project) addRoot(info *ScriptInfo) {
p.typeAcquisition = nil
// !!!
// if p.kind == KindInferred {
// p.host.startWatchingConfigFilesForInferredProjectRoot(info.path);
// // handle JS toggling
// }
if p.isRoot(info) {
panic("script info is already a root")
}
p.rootFileNames.Set(info.path, info.fileName)
if p.kind == KindInferred {
p.typeAcquisition = nil
}
info.attachToProject(p)
p.markAsDirtyLocked()
}

func (p *Project) LoadConfig() error {
if err := p.loadConfig(); err != nil {
return err
}
p.markAsDirty()
return nil
}

func (p *Project) loadConfig() error {
if p.kind != KindConfigured {
panic("loadConfig called on non-configured project")
}

p.programConfig = nil
p.pendingReload = PendingReloadNone
if configFileContent, ok := p.host.FS().ReadFile(p.configFileName); ok {
configDir := tspath.GetDirectoryPath(p.configFileName)
tsConfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(p.configFileName, p.configFilePath, configFileContent)
@@ -911,43 +909,28 @@ func (p *Project) loadConfig() error {
}

// setRootFiles returns true if the set of root files has changed.
func (p *Project) setRootFiles(rootFileNames []string) bool {
var hasChanged bool
func (p *Project) setRootFiles(rootFileNames []string) {
newRootScriptInfos := make(map[tspath.Path]struct{}, len(rootFileNames))
for _, file := range rootFileNames {
scriptKind := p.getScriptKind(file)
path := p.toPath(file)
// !!! updateNonInferredProjectFiles uses a fileExists check, which I guess
// could be needed if a watcher fails?
scriptInfo := p.host.GetOrCreateScriptInfoForFile(file, path, scriptKind)
newRootScriptInfos[path] = struct{}{}
isAlreadyRoot := p.rootFileNames.Has(path)
hasChanged = hasChanged || !isAlreadyRoot

if !isAlreadyRoot && scriptInfo != nil {
p.addRoot(scriptInfo)
if scriptInfo.isOpen {
// !!!
// s.removeRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo)
}
} else if !isAlreadyRoot {
p.rootFileNames.Set(path, file)
}
p.rootFileNames.Set(path, file)
// if !isAlreadyRoot {
// if scriptInfo.isOpen {
// !!!s.removeRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo)
// }
// }
}

if p.rootFileNames.Size() > len(rootFileNames) {
hasChanged = true
for root := range p.rootFileNames.Keys() {
if _, ok := newRootScriptInfos[root]; !ok {
if info := p.host.GetScriptInfoByPath(root); info != nil {
p.removeFile(info, true /*fileExists*/, true /*detachFromProject*/)
} else {
p.rootFileNames.Delete(root)
}
p.rootFileNames.Delete(root)
}
}
}
return hasChanged
}

func (p *Project) clearSourceMapperCache() {
@@ -994,21 +977,20 @@ func (p *Project) GetFileNames(excludeFilesFromExternalLibraries bool, excludeCo
}

func (p *Project) print(writeFileNames bool, writeFileExplanation bool, writeFileVersionAndText bool, builder *strings.Builder) string {
builder.WriteString(fmt.Sprintf("Project '%s' (%s)\n", p.name, p.kind.String()))
builder.WriteString(fmt.Sprintf("\nProject '%s' (%s)\n", p.name, p.kind.String()))
if p.initialLoadPending {
builder.WriteString("\tFiles (0) InitialLoadPending\n")
builder.WriteString("\n\tFiles (0) InitialLoadPending\n")
} else if p.program == nil {
builder.WriteString("\tFiles (0) NoProgram\n")
builder.WriteString("\n\tFiles (0) NoProgram\n")
} else {
sourceFiles := p.program.GetSourceFiles()
builder.WriteString(fmt.Sprintf("\tFiles (%d)\n", len(sourceFiles)))
builder.WriteString(fmt.Sprintf("\n\tFiles (%d)\n", len(sourceFiles)))
if writeFileNames {
for _, sourceFile := range sourceFiles {
builder.WriteString("\t\t" + sourceFile.FileName())
builder.WriteString("\n\t\t" + sourceFile.FileName())
if writeFileVersionAndText {
builder.WriteString(fmt.Sprintf(" %d %s", sourceFile.Version, sourceFile.Text()))
}
builder.WriteRune('\n')
}
// !!!
// if writeFileExplanation {}
@@ -1026,8 +1008,61 @@ func (p *Project) Logf(format string, args ...interface{}) {
p.Log(fmt.Sprintf(format, args...))
}

func (p *Project) detachScriptInfoIfNotInferredRoot(path tspath.Path) {
// We might not find the script info in case its not associated with the project any more
// and project graph was not updated (eg delayed update graph in case of files changed/deleted on the disk)
if scriptInfo := p.host.GetScriptInfoByPath(path); scriptInfo != nil &&
(p.kind != KindInferred || !p.isRoot(scriptInfo)) {
scriptInfo.detachFromProject(p)
}
}

func (p *Project) Close() {
// !!!
p.mu.Lock()
defer p.mu.Unlock()

if p.program != nil {
for _, sourceFile := range p.program.GetSourceFiles() {
p.host.DocumentRegistry().ReleaseDocument(sourceFile, p.program.Options())
// Detach script info if its not root or is root of non inferred project
p.detachScriptInfoIfNotInferredRoot(sourceFile.Path())
}
p.program = nil
}

if p.kind == KindInferred {
// Release root script infos for inferred projects.
for path := range p.rootFileNames.Keys() {
if info := p.host.GetScriptInfoByPath(path); info != nil {
info.detachFromProject(p)
}
}
}
p.rootFileNames = nil
p.parsedCommandLine = nil
p.programConfig = nil
p.checkerPool = nil
p.unresolvedImportsPerFile = nil
p.typingsInfo = nil
p.typingFiles = nil

// Clean up file watchers waiting for missing files
client := p.host.Client()
if p.host.IsWatchEnabled() && client != nil {
ctx := context.Background()
if p.rootFilesWatch != nil {
p.rootFilesWatch.update(ctx, nil)
}

p.failedLookupsWatch.update(ctx, nil)
p.affectingLocationsWatch.update(ctx, nil)
p.typingsFilesWatch.update(ctx, nil)
p.typingsDirectoryWatch.update(ctx, nil)
}
}

func (p *Project) isClosed() bool {
return p.rootFileNames == nil
}

func formatFileList(files []string, linePrefix string, groupSuffix string) string {
Loading
Oops, something went wrong.