Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion tsc/internal/compiler/fileloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,14 @@ func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName string) (
}

func (p *fileLoader) addProjectReferenceTasks(singleThreaded bool) {
// The mapper is shared with every program cloned from this one (via processedFiles),
// so don't retain the factory closures: they can capture the creator of this program
// (e.g. the LSP project), which would keep this program and its checkers alive.
opts := p.opts
opts.CreateCheckerPool = nil
opts.CreateModuleResolver = nil
p.projectReferenceFileMapper = &projectReferenceFileMapper{
opts: p.opts,
opts: opts,
host: p.opts.Host,
}
projectReferences := p.opts.Config.ResolvedProjectReferencePaths()
Expand Down
48 changes: 48 additions & 0 deletions tsc/internal/project/snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package project
import (
"context"
"fmt"
"runtime"
"sync/atomic"
"testing"
"time"

"github.com/microsoft/TypeScript/tsc/internal/bundled"
"github.com/microsoft/TypeScript/tsc/internal/collections"
Expand Down Expand Up @@ -209,6 +212,51 @@ func TestSnapshot(t *testing.T) {
assert.Equal(t, snapshotAfter.ProjectCollection.InferredProject().host.sourceFS.source, snapshotBefore.fs)
})

t.Run("cloned program does not retain the program it was cloned from", func(t *testing.T) {
t.Parallel()
files := map[string]any{
"/home/projects/TS/p1/tsconfig.json": "{}",
"/home/projects/TS/p1/index.ts": "console.log('Hello, world!');",
}
session := setup(files)
uri := lsproto.DocumentUri("file:///home/projects/TS/p1/index.ts")
session.DidOpenFile(context.Background(), uri, 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript)
_, err := session.GetLanguageService(context.Background(), uri)
assert.NilError(t, err)

var collected atomic.Bool
func() {
oldProgram := session.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")).GetProgram()
runtime.AddCleanup(oldProgram, func(b *atomic.Bool) { b.Store(true) }, &collected)
}()

session.DidChangeFile(context.Background(), uri, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{
{
Partial: &lsproto.TextDocumentContentChangePartial{
Text: "\n",
Range: lsproto.Range{
Start: lsproto.Position{Line: 0, Character: 24},
End: lsproto.Position{Line: 0, Character: 24},
},
},
},
})
_, err = session.GetLanguageService(context.Background(), uri)
assert.NilError(t, err)
assert.Equal(t, session.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")).ProgramUpdateKind, ProgramUpdateKindCloned)

// Cleanups run asynchronously after GC; background snapshot work may also hold a ref briefly.
for range 50 {
runtime.GC()
if collected.Load() {
break
}
time.Sleep(20 * time.Millisecond)
}
assert.Assert(t, collected.Load(), "program from before the edit should be garbage collected")
runtime.KeepAlive(session)
})

t.Run("cached disk files are cleaned up", func(t *testing.T) {
t.Parallel()
files := map[string]any{
Expand Down
Loading