Skip to content

Commit e93d364

Browse files
authored
Genericize ref counting caches; fix extended config cache bug (#2311)
1 parent 6d26654 commit e93d364

15 files changed

+270
-219
lines changed

internal/ast/ast.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/microsoft/typescript-go/internal/collections"
1111
"github.com/microsoft/typescript-go/internal/core"
1212
"github.com/microsoft/typescript-go/internal/tspath"
13+
"github.com/zeebo/xxh3"
1314
)
1415

1516
// Visitor
@@ -10779,6 +10780,7 @@ type SourceFile struct {
1077910780

1078010781
// Fields set by language service
1078110782

10783+
Hash xxh3.Uint128
1078210784
tokenCacheMu sync.Mutex
1078310785
tokenCache map[core.TextRange]*Node
1078410786
tokenFactory *NodeFactory

internal/execute/tsc/extendedconfigcache.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ type extendedConfigCacheEntry struct {
2424
var _ tsoptions.ExtendedConfigCache = (*ExtendedConfigCache)(nil)
2525

2626
// GetExtendedConfig implements tsoptions.ExtendedConfigCache.
27-
func (e *ExtendedConfigCache) GetExtendedConfig(fileName string, path tspath.Path, parse func() *tsoptions.ExtendedConfigCacheEntry) *tsoptions.ExtendedConfigCacheEntry {
27+
func (e *ExtendedConfigCache) GetExtendedConfig(fileName string, path tspath.Path, resolutionStack []string, host tsoptions.ParseConfigHost) *tsoptions.ExtendedConfigCacheEntry {
2828
entry, loaded := e.loadOrStoreNewLockedEntry(path)
2929
defer entry.mu.Unlock()
3030
if !loaded {
31-
entry.ExtendedConfigCacheEntry = parse()
31+
entry.ExtendedConfigCacheEntry = tsoptions.ParseExtendedConfig(fileName, path, resolutionStack, host, e)
3232
}
3333
return entry.ExtendedConfigCacheEntry
3434
}

internal/fourslash/fourslash.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,10 @@ func newLSPPipe() (*lspReader, *lspWriter) {
138138

139139
const rootDir = "/"
140140

141-
var parseCache = project.ParseCache{
142-
Options: project.ParseCacheOptions{
143-
DisableDeletion: true,
144-
},
145-
}
141+
var parseCache = project.NewParseCache(project.RefCountCacheOptions{
142+
DisableDeletion: true,
143+
},
144+
)
146145

147146
func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, content string) (*FourslashTest, func()) {
148147
repo.SkipIfNoTypeScriptSubmodule(t)
@@ -217,7 +216,7 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten
217216
FS: fs,
218217
DefaultLibraryPath: bundled.LibPath(),
219218

220-
ParseCache: &parseCache,
219+
ParseCache: parseCache,
221220
})
222221

223222
converters := lsconv.NewConverters(lsproto.PositionEncodingKindUTF8, func(fileName string) *lsconv.LSPLineMap {

internal/project/compilerhost.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func (c *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.Sourc
126126
c.ensureAlive()
127127
c.seenFiles.Add(opts.Path)
128128
if fh := c.fs.GetFileByPath(opts.FileName, opts.Path); fh != nil {
129-
return c.builder.parseCache.Acquire(fh, opts, fh.Kind())
129+
return c.builder.parseCache.Acquire(NewParseCacheKey(opts, fh.Hash(), fh.Kind()), fh)
130130
}
131131
return nil
132132
}

internal/project/configfilechanges_test.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ func TestConfigFileChanges(t *testing.T) {
1919
}
2020

2121
files := map[string]any{
22-
"/tsconfig.base.json": `{"compilerOptions": {"strict": true}}`,
23-
"/src/tsconfig.json": `{"extends": "../tsconfig.base.json", "compilerOptions": {"target": "es6"}, "references": [{"path": "../utils"}]}`,
24-
"/src/index.ts": `console.log("Hello, world!");`,
25-
"/src/subfolder/foo.ts": `export const foo = "bar";`,
22+
"/tsconfig.more-base.json": `{}`,
23+
"/tsconfig.base.json": `{"extends": "../tsconfig.more-base.json", "compilerOptions": {"strict": true}}`,
24+
"/src/tsconfig.json": `{"extends": "../tsconfig.base.json", "compilerOptions": {"target": "es6"}, "references": [{"path": "../utils"}]}`,
25+
"/src/index.ts": `console.log("Hello, world!");`,
26+
"/src/subfolder/foo.ts": `export const foo = "bar";`,
2627

2728
"/utils/tsconfig.json": `{"compilerOptions": {"composite": true}}`,
2829
"/utils/index.ts": `console.log("Hello, test!");`,
@@ -66,6 +67,25 @@ func TestConfigFileChanges(t *testing.T) {
6667
assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse)
6768
})
6869

70+
t.Run("should update project on doubly extended config file change", func(t *testing.T) {
71+
t.Parallel()
72+
session, utils := projecttestutil.Setup(files)
73+
session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript)
74+
75+
err := utils.FS().WriteFile("/tsconfig.more-base.json", `{"compilerOptions": {"verbatimModuleSyntax": true}}`, false /*writeByteOrderMark*/)
76+
assert.NilError(t, err)
77+
session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{
78+
{
79+
Uri: lsproto.DocumentUri("file:///tsconfig.more-base.json"),
80+
Type: lsproto.FileChangeTypeChanged,
81+
},
82+
})
83+
84+
ls, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts"))
85+
assert.NilError(t, err)
86+
assert.Equal(t, ls.GetProgram().Options().VerbatimModuleSyntax, core.TSTrue)
87+
})
88+
6989
t.Run("should update project on referenced config file change", func(t *testing.T) {
7090
t.Parallel()
7191
session, utils := projecttestutil.Setup(files)

internal/project/configfileregistrybuilder.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ func (c *configFileRegistryBuilder) reloadIfNeeded(entry *configFileEntry, fileN
106106
entry.commandLine = entry.commandLine.ReloadFileNamesOfParsedCommandLine(c.fs.fs)
107107
case PendingReloadFull:
108108
logger.Log("Loading config file: " + fileName)
109+
oldCommandLine := entry.commandLine
109110
entry.commandLine, _ = tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, c, c)
110-
c.updateExtendingConfigs(path, entry.commandLine, entry.commandLine)
111+
c.updateExtendingConfigs(path, entry.commandLine, oldCommandLine)
111112
c.updateRootFilesWatch(fileName, entry)
112113
logger.Log("Finished loading config file")
113114
default:
@@ -591,9 +592,21 @@ func (c *configFileRegistryBuilder) GetCurrentDirectory() string {
591592
}
592593

593594
// GetExtendedConfig implements tsoptions.ExtendedConfigCache.
594-
func (c *configFileRegistryBuilder) GetExtendedConfig(fileName string, path tspath.Path, parse func() *tsoptions.ExtendedConfigCacheEntry) *tsoptions.ExtendedConfigCacheEntry {
595+
func (c *configFileRegistryBuilder) GetExtendedConfig(fileName string, path tspath.Path, resolutionStack []string, host tsoptions.ParseConfigHost) *tsoptions.ExtendedConfigCacheEntry {
596+
var content string
595597
fh := c.fs.GetFileByPath(fileName, path)
596-
return c.extendedConfigCache.Acquire(fh, path, parse)
598+
if fh != nil {
599+
content = fh.Content()
600+
}
601+
602+
return c.extendedConfigCache.Acquire(path, ExtendedConfigParseArgs{
603+
FileName: fileName,
604+
Content: content,
605+
FS: c.fs,
606+
ResolutionStack: resolutionStack,
607+
Host: host,
608+
Cache: c,
609+
}).ExtendedConfigCacheEntry
597610
}
598611

599612
func (c *configFileRegistryBuilder) Cleanup() {
Lines changed: 33 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,52 @@
11
package project
22

33
import (
4-
"sync"
5-
6-
"github.com/microsoft/typescript-go/internal/collections"
74
"github.com/microsoft/typescript-go/internal/tsoptions"
85
"github.com/microsoft/typescript-go/internal/tspath"
96
"github.com/zeebo/xxh3"
107
)
118

12-
type ExtendedConfigCache struct {
13-
entries collections.SyncMap[tspath.Path, *extendedConfigCacheEntry]
9+
type ExtendedConfigParseArgs struct {
10+
FileName string
11+
Content string
12+
FS FileSource
13+
ResolutionStack []string
14+
Host tsoptions.ParseConfigHost
15+
Cache tsoptions.ExtendedConfigCache
1416
}
1517

16-
type extendedConfigCacheEntry struct {
17-
mu sync.Mutex
18-
entry *tsoptions.ExtendedConfigCacheEntry
19-
hash xxh3.Uint128
20-
refCount int
18+
type ExtendedConfigCacheEntry struct {
19+
*tsoptions.ExtendedConfigCacheEntry
20+
Hash xxh3.Uint128
2121
}
2222

23-
func (c *ExtendedConfigCache) Acquire(fh FileHandle, path tspath.Path, parse func() *tsoptions.ExtendedConfigCacheEntry) *tsoptions.ExtendedConfigCacheEntry {
24-
entry, loaded := c.loadOrStoreNewLockedEntry(path)
25-
defer entry.mu.Unlock()
26-
var hash xxh3.Uint128
27-
if fh != nil {
28-
hash = fh.Hash()
29-
}
30-
if !loaded || entry.hash != hash {
31-
// Reparse the config if the hash has changed, or parse for the first time.
32-
entry.entry = parse()
33-
entry.hash = hash
34-
}
35-
return entry.entry
36-
}
23+
type ExtendedConfigCache = RefCountCache[tspath.Path, *ExtendedConfigCacheEntry, ExtendedConfigParseArgs]
3724

38-
func (c *ExtendedConfigCache) Ref(path tspath.Path) {
39-
if entry, ok := c.entries.Load(path); ok {
40-
entry.mu.Lock()
41-
if entry.refCount <= 0 {
42-
// Entry was deleted while we were acquiring the lock
43-
newEntry, loaded := c.loadOrStoreNewLockedEntry(path)
44-
if !loaded {
45-
newEntry.entry = entry.entry
46-
newEntry.hash = entry.hash
25+
func NewExtendedConfigCache() *ExtendedConfigCache {
26+
return NewRefCountCache(
27+
RefCountCacheOptions{},
28+
func(path tspath.Path, args ExtendedConfigParseArgs) *ExtendedConfigCacheEntry {
29+
result := &ExtendedConfigCacheEntry{
30+
ExtendedConfigCacheEntry: tsoptions.ParseExtendedConfig(args.FileName, path, args.ResolutionStack, args.Host, args.Cache),
4731
}
48-
entry.mu.Unlock()
49-
newEntry.mu.Unlock()
50-
return
51-
}
52-
entry.refCount++
53-
entry.mu.Unlock()
54-
}
55-
}
56-
57-
func (c *ExtendedConfigCache) Deref(path tspath.Path) {
58-
if entry, ok := c.entries.Load(path); ok {
59-
entry.mu.Lock()
60-
entry.refCount--
61-
remove := entry.refCount <= 0
62-
if remove {
63-
c.entries.Delete(path)
64-
}
65-
entry.mu.Unlock()
66-
}
67-
}
68-
69-
func (c *ExtendedConfigCache) Has(path tspath.Path) bool {
70-
_, ok := c.entries.Load(path)
71-
return ok
32+
result.Hash = hash(result.ExtendedConfigCacheEntry, args)
33+
return result
34+
},
35+
func(path tspath.Path, entry *ExtendedConfigCacheEntry, args ExtendedConfigParseArgs) bool {
36+
return entry.Hash == xxh3.Uint128{} || entry.Hash != hash(entry.ExtendedConfigCacheEntry, args)
37+
},
38+
)
7239
}
7340

74-
// loadOrStoreNewLockedEntry loads an existing entry or creates a new one. The returned
75-
// entry's mutex is locked and its refCount is incremented (or initialized to 1
76-
// in the case of a new entry).
77-
func (c *ExtendedConfigCache) loadOrStoreNewLockedEntry(path tspath.Path) (*extendedConfigCacheEntry, bool) {
78-
entry := &extendedConfigCacheEntry{refCount: 1}
79-
entry.mu.Lock()
80-
if existing, loaded := c.entries.LoadOrStore(path, entry); loaded {
81-
existing.mu.Lock()
82-
if existing.refCount <= 0 {
83-
// Entry was deleted while we were acquiring the lock
84-
existing.mu.Unlock()
85-
return c.loadOrStoreNewLockedEntry(path)
41+
func hash(entry *tsoptions.ExtendedConfigCacheEntry, args ExtendedConfigParseArgs) xxh3.Uint128 {
42+
hasher := xxh3.New()
43+
_, _ = hasher.WriteString(args.Content)
44+
for _, fileName := range entry.ExtendedFileNames() {
45+
fh := args.FS.GetFile(fileName)
46+
if fh == nil {
47+
return xxh3.Uint128{}
8648
}
87-
existing.refCount++
88-
return existing, true
49+
_, _ = hasher.WriteString(fh.Content())
8950
}
90-
return entry, false
51+
return hasher.Sum128()
9152
}

0 commit comments

Comments
 (0)