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
126 changes: 126 additions & 0 deletions tsc/internal/api/session_apistate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"sync"
"testing"

"github.com/microsoft/TypeScript/tsc/internal/bundled"
Expand Down Expand Up @@ -535,3 +536,128 @@ func TestOpenProjectsWithCircularExtendsReportCircularity(t *testing.T) {
}
assert.DeepEqual(t, codes, []int32{18000})
}

func openProjectInNewSnapshot(t *testing.T, session *Session, configFileName string) SnapshotID {
t.Helper()
response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{
OpenProjects: []DocumentIdentifier{{FileName: configFileName}},
})
assert.NilError(t, err)
return response.Snapshot
}

func openProjectsInConcurrentSnapshots(t *testing.T, session *Session, configFileNames []string) []SnapshotID {
t.Helper()
responses := make([]*CreateSnapshotResponse, len(configFileNames))
errs := make([]error, len(configFileNames))
var wg sync.WaitGroup
for i, configFileName := range configFileNames {
wg.Go(func() {
responses[i], errs[i] = session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{
OpenProjects: []DocumentIdentifier{{FileName: configFileName}},
})
})
}
wg.Wait()
snapshots := make([]SnapshotID, len(configFileNames))
for i := range configFileNames {
assert.NilError(t, errs[i])
snapshots[i] = responses[i].Snapshot
}
return snapshots
}

func configFileParsingDiagnosticCodes(t *testing.T, session *Session, snapshot SnapshotID, configFileName string) []int32 {
t.Helper()
diagnostics, err := session.handleGetConfigFileParsingDiagnostics(context.Background(), &GetProjectDiagnosticsParams{
Snapshot: snapshot,
Project: configuredProjectID(configFileName),
})
assert.NilError(t, err)
codes := make([]int32, 0, len(diagnostics))
for _, diagnostic := range diagnostics {
codes = append(codes, diagnostic.Code)
}
return codes
}

func TestOpenProjectsExtendingIntoCircularExtendsReportCircularity(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}

const cycle = "/home/projects/cycle/tsconfig.json"
const outer = "/home/projects/outer/tsconfig.json"
projectSession, _ := projecttestutil.Setup(map[string]any{
cycle: `{ "extends": "./a.json" }`,
"/home/projects/cycle/a.json": `{ "extends": "./tsconfig.json" }`,
outer: `{ "extends": "../cycle/a.json" }`,
"/home/projects/cycle/main.ts": `export const a = 1;`,
"/home/projects/outer/main.ts": `export const b = 1;`,
})
defer projectSession.Close()
session := NewLSPSession(projectSession, nil)
defer session.Close()

openProjectInNewSnapshot(t, session, cycle)
snapshot := openProjectInNewSnapshot(t, session, outer)
assert.DeepEqual(t, configFileParsingDiagnosticCodes(t, session, snapshot, outer), []int32{18000})
}

func TestConcurrentlyOpenProjectsExtendingIntoCircularExtendsReportCircularity(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}

const cycle = "/home/projects/cycle/tsconfig.json"
outers := []string{"/home/projects/p1/tsconfig.json", "/home/projects/p2/tsconfig.json"}
projectSession, _ := projecttestutil.Setup(map[string]any{
cycle: `{ "extends": "./a.json" }`,
"/home/projects/cycle/a.json": `{ "extends": "./b.json" }`,
"/home/projects/cycle/b.json": `{ "extends": "./tsconfig.json" }`,
outers[0]: `{ "extends": "../cycle/a.json" }`,
outers[1]: `{ "extends": "../cycle/b.json" }`,
"/home/projects/cycle/main.ts": `export const a = 1;`,
"/home/projects/p1/main.ts": `export const b = 1;`,
"/home/projects/p2/main.ts": `export const c = 1;`,
})
defer projectSession.Close()
session := NewLSPSession(projectSession, nil)
defer session.Close()

for i, snapshot := range openProjectsInConcurrentSnapshots(t, session, outers) {
assert.DeepEqual(t, configFileParsingDiagnosticCodes(t, session, snapshot, outers[i]), []int32{18000})
}
openProjectInNewSnapshot(t, session, cycle)
for i, snapshot := range openProjectsInConcurrentSnapshots(t, session, outers) {
assert.DeepEqual(t, configFileParsingDiagnosticCodes(t, session, snapshot, outers[i]), []int32{18000})
}
}

func TestReopenProjectExtendingIntoCircularExtendsReportsCircularity(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}

const cycle = "/home/projects/cycle/tsconfig.json"
const outer = "/home/projects/outer/tsconfig.json"
projectSession, _ := projecttestutil.Setup(map[string]any{
cycle: `{ "extends": "./a.json" }`,
"/home/projects/cycle/a.json": `{ "extends": "./tsconfig.json" }`,
outer: `{ "extends": "../cycle/a.json" }`,
"/home/projects/cycle/main.ts": `export const a = 1;`,
"/home/projects/outer/main.ts": `export const b = 1;`,
})
defer projectSession.Close()
session := NewLSPSession(projectSession, nil)
defer session.Close()

openProjectInNewSnapshot(t, session, cycle)
_, err := session.handleRelease(context.Background(), &ReleaseParams{Snapshot: openProjectInNewSnapshot(t, session, outer)})
assert.NilError(t, err)
snapshot := openProjectInNewSnapshot(t, session, outer)
assert.DeepEqual(t, configFileParsingDiagnosticCodes(t, session, snapshot, outer), []int32{18000})
}
9 changes: 8 additions & 1 deletion tsc/internal/project/configfileregistrybuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,14 +796,21 @@ func (c *configFileRegistryBuilder) GetExtendedConfig(fileName tspath.RootedFile
content = fh.Content()
}

return c.extendedConfigCache.LoadAndAcquire(path, c.snapshotID, ExtendedConfigParseArgs{
entry := c.extendedConfigCache.LoadAndAcquire(path, c.snapshotID, ExtendedConfigParseArgs{
FileName: fileName,
Content: content,
FS: c.fs.source,
ResolutionStack: resolutionStack,
ConfigFS: fs,
Cache: c,
}).ExtendedConfigCacheEntry
for _, extendedFileName := range entry.ExtendedFileNames() {
extendedPath := fs.CaseSensitivity().PathKey(extendedFileName.AsPath())
if !slices.Contains(resolutionStack, extendedPath) && !c.extendedConfigCache.TryAddOwner(extendedPath, c.snapshotID) {
return tsoptions.ParseExtendedConfig(fileName, path, resolutionStack, fs, c)
}
}
return entry
}

func (c *configFileRegistryBuilder) Cleanup() {
Expand Down
5 changes: 5 additions & 0 deletions tsc/internal/project/extendedconfigcache.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package project

import (
"slices"

"github.com/microsoft/TypeScript/tsc/internal/tsoptions"
"github.com/microsoft/TypeScript/tsc/internal/tspath"
"github.com/microsoft/TypeScript/tsc/internal/vfs"
Expand Down Expand Up @@ -47,6 +49,9 @@ func hash(entry *tsoptions.ExtendedConfigCacheEntry, args ExtendedConfigParseArg
return xxh3.Uint128{}
}
_, _ = hasher.WriteString(fh.Content())
if slices.Contains(args.ResolutionStack, args.ConfigFS.CaseSensitivity().PathKey(fileName.AsPath())) {
_, _ = hasher.WriteString(fileName.AsString())
Comment thread
cplieger marked this conversation as resolved.
}
Comment thread
cplieger marked this conversation as resolved.
}
return hasher.Sum128()
}
33 changes: 28 additions & 5 deletions tsc/internal/project/ownercache.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,22 @@ func NewOwnerCache[K comparable, V any, LoadArgs any](
}

func (c *OwnerCache[K, V, LoadArgs]) LoadAndAcquire(identity K, owner uint64, loadArgs LoadArgs) V {
entry, loaded := c.loadOrStoreLockedEntry(identity)
defer entry.mu.Unlock()
if !loaded || c.isExpired != nil && c.isExpired(identity, entry.value, loadArgs) {
entry.value = c.parse(identity, loadArgs)
if entry, ok := c.entries.Load(identity); ok {
entry.mu.Lock()
if len(entry.owners) > 0 && (c.isExpired == nil || !c.isExpired(identity, entry.value, loadArgs)) {
entry.owners[owner] = struct{}{}
value := entry.value
entry.mu.Unlock()
return value
}
entry.mu.Unlock()
}
value := c.parse(identity, loadArgs)
entry, _ := c.loadOrStoreLockedEntry(identity)
defer entry.mu.Unlock()
entry.value = value
entry.owners[owner] = struct{}{}
return entry.value
return value
}

func (c *OwnerCache[K, V, LoadArgs]) Acquire(identity K, owner uint64, value V) {
Expand Down Expand Up @@ -70,6 +79,20 @@ func (c *OwnerCache[K, V, LoadArgs]) AddOwner(identity K, owner uint64) {
entry.owners[owner] = struct{}{}
}

func (c *OwnerCache[K, V, LoadArgs]) TryAddOwner(identity K, owner uint64) bool {
entry, ok := c.entries.Load(identity)
if !ok {
return false
}
entry.mu.Lock()
defer entry.mu.Unlock()
if len(entry.owners) == 0 {
return false
}
entry.owners[owner] = struct{}{}
return true
}

func (c *OwnerCache[K, V, LoadArgs]) Has(identity K) bool {
_, ok := c.entries.Load(identity)
return ok
Expand Down