Skip to content

Commit 5652e65

Browse files
authored
Add a new cached VFS wrapper (#750)
1 parent 933197b commit 5652e65

File tree

8 files changed

+846
-1
lines changed

8 files changed

+846
-1
lines changed

Diff for: go.mod

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ require (
1212
)
1313

1414
require (
15+
github.com/matryer/moq v0.5.3 // indirect
1516
golang.org/x/mod v0.23.0 // indirect
1617
golang.org/x/sync v0.11.0 // indirect
1718
golang.org/x/tools v0.30.0 // indirect
1819
)
1920

20-
tool golang.org/x/tools/cmd/stringer
21+
tool (
22+
github.com/matryer/moq
23+
golang.org/x/tools/cmd/stringer
24+
)

Diff for: go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6
44
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
55
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
66
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
7+
github.com/matryer/moq v0.5.3 h1:4femQCFmBUwFPYs8VfM5ID7AI67/DTEDRBbTtSWy7GU=
8+
github.com/matryer/moq v0.5.3/go.mod h1:8288Qkw7gMZhUP3cIN86GG7g5p9jRuZH8biXLW4RXvQ=
79
github.com/pkg/diff v0.0.0-20241224192749-4e6772a4315c h1:8TRxBMS/YsupXoOiGKHr9ZOXo+5DezGWPgBAhBHEHto=
810
github.com/pkg/diff v0.0.0-20241224192749-4e6772a4315c/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
11+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
913
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
1014
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
1115
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=

Diff for: internal/vfs/cachedvfs/cachedvfs.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package cachedvfs
2+
3+
import (
4+
"sync"
5+
6+
"github.com/microsoft/typescript-go/internal/vfs"
7+
)
8+
9+
type FS struct {
10+
fs vfs.FS
11+
12+
directoryExistsCache sync.Map // map[string]bool
13+
fileExistsCache sync.Map // map[string]bool
14+
getAccessibleEntriesCache sync.Map // map[string]vfs.Entries
15+
realpathCache sync.Map // map[string]string
16+
statCache sync.Map // map[string]vfs.FileInfo
17+
}
18+
19+
var _ vfs.FS = (*FS)(nil)
20+
21+
func From(fs vfs.FS) *FS {
22+
return &FS{fs: fs}
23+
}
24+
25+
func (fsys *FS) ClearCache() {
26+
fsys.directoryExistsCache.Clear()
27+
fsys.fileExistsCache.Clear()
28+
fsys.getAccessibleEntriesCache.Clear()
29+
fsys.realpathCache.Clear()
30+
fsys.statCache.Clear()
31+
}
32+
33+
func (fsys *FS) DirectoryExists(path string) bool {
34+
if ret, ok := fsys.directoryExistsCache.Load(path); ok {
35+
return ret.(bool)
36+
}
37+
ret := fsys.fs.DirectoryExists(path)
38+
fsys.directoryExistsCache.Store(path, ret)
39+
return ret
40+
}
41+
42+
func (fsys *FS) FileExists(path string) bool {
43+
if ret, ok := fsys.fileExistsCache.Load(path); ok {
44+
return ret.(bool)
45+
}
46+
ret := fsys.fs.FileExists(path)
47+
fsys.fileExistsCache.Store(path, ret)
48+
return ret
49+
}
50+
51+
func (fsys *FS) GetAccessibleEntries(path string) vfs.Entries {
52+
if ret, ok := fsys.getAccessibleEntriesCache.Load(path); ok {
53+
return ret.(vfs.Entries)
54+
}
55+
ret := fsys.fs.GetAccessibleEntries(path)
56+
fsys.getAccessibleEntriesCache.Store(path, ret)
57+
return ret
58+
}
59+
60+
func (fsys *FS) ReadFile(path string) (contents string, ok bool) {
61+
return fsys.fs.ReadFile(path)
62+
}
63+
64+
func (fsys *FS) Realpath(path string) string {
65+
if ret, ok := fsys.realpathCache.Load(path); ok {
66+
return ret.(string)
67+
}
68+
ret := fsys.fs.Realpath(path)
69+
fsys.realpathCache.Store(path, ret)
70+
return ret
71+
}
72+
73+
func (fsys *FS) Remove(path string) error {
74+
return fsys.fs.Remove(path)
75+
}
76+
77+
func (fsys *FS) Stat(path string) vfs.FileInfo {
78+
if ret, ok := fsys.statCache.Load(path); ok {
79+
return ret.(vfs.FileInfo)
80+
}
81+
ret := fsys.fs.Stat(path)
82+
fsys.statCache.Store(path, ret)
83+
return ret
84+
}
85+
86+
func (fsys *FS) UseCaseSensitiveFileNames() bool {
87+
return fsys.fs.UseCaseSensitiveFileNames()
88+
}
89+
90+
func (fsys *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
91+
return fsys.fs.WalkDir(root, walkFn)
92+
}
93+
94+
func (fsys *FS) WriteFile(path string, data string, writeByteOrderMark bool) error {
95+
return fsys.fs.WriteFile(path, data, writeByteOrderMark)
96+
}

Diff for: internal/vfs/cachedvfs/cachedvfs_test.go

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package cachedvfs_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/vfs"
7+
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
8+
"github.com/microsoft/typescript-go/internal/vfs/vfsmock"
9+
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
10+
"gotest.tools/v3/assert"
11+
)
12+
13+
func createMockFS() *vfsmock.FSMock {
14+
return vfsmock.Wrap(vfstest.FromMap(map[string]string{
15+
"/some/path/file.txt": "hello world",
16+
}, true))
17+
}
18+
19+
func TestDirectoryExists(t *testing.T) {
20+
t.Parallel()
21+
22+
underlying := createMockFS()
23+
cached := cachedvfs.From(underlying)
24+
25+
cached.DirectoryExists("/some/path")
26+
assert.Equal(t, 1, len(underlying.DirectoryExistsCalls()))
27+
28+
cached.DirectoryExists("/some/path")
29+
assert.Equal(t, 1, len(underlying.DirectoryExistsCalls()))
30+
31+
cached.ClearCache()
32+
cached.DirectoryExists("/some/path")
33+
assert.Equal(t, 2, len(underlying.DirectoryExistsCalls()))
34+
35+
cached.DirectoryExists("/other/path")
36+
assert.Equal(t, 3, len(underlying.DirectoryExistsCalls()))
37+
}
38+
39+
func TestFileExists(t *testing.T) {
40+
t.Parallel()
41+
42+
underlying := createMockFS()
43+
cached := cachedvfs.From(underlying)
44+
45+
cached.FileExists("/some/path/file.txt")
46+
assert.Equal(t, 1, len(underlying.FileExistsCalls()))
47+
48+
cached.FileExists("/some/path/file.txt")
49+
assert.Equal(t, 1, len(underlying.FileExistsCalls()))
50+
51+
cached.ClearCache()
52+
cached.FileExists("/some/path/file.txt")
53+
assert.Equal(t, 2, len(underlying.FileExistsCalls()))
54+
55+
cached.FileExists("/other/path/file.txt")
56+
assert.Equal(t, 3, len(underlying.FileExistsCalls()))
57+
}
58+
59+
func TestGetAccessibleEntries(t *testing.T) {
60+
t.Parallel()
61+
62+
underlying := createMockFS()
63+
cached := cachedvfs.From(underlying)
64+
65+
cached.GetAccessibleEntries("/some/path")
66+
assert.Equal(t, 1, len(underlying.GetAccessibleEntriesCalls()))
67+
68+
cached.GetAccessibleEntries("/some/path")
69+
assert.Equal(t, 1, len(underlying.GetAccessibleEntriesCalls()))
70+
71+
cached.ClearCache()
72+
cached.GetAccessibleEntries("/some/path")
73+
assert.Equal(t, 2, len(underlying.GetAccessibleEntriesCalls()))
74+
75+
cached.GetAccessibleEntries("/other/path")
76+
assert.Equal(t, 3, len(underlying.GetAccessibleEntriesCalls()))
77+
}
78+
79+
func TestRealpath(t *testing.T) {
80+
t.Parallel()
81+
82+
underlying := createMockFS()
83+
cached := cachedvfs.From(underlying)
84+
85+
cached.Realpath("/some/path")
86+
assert.Equal(t, 1, len(underlying.RealpathCalls()))
87+
88+
cached.Realpath("/some/path")
89+
assert.Equal(t, 1, len(underlying.RealpathCalls()))
90+
91+
cached.ClearCache()
92+
cached.Realpath("/some/path")
93+
assert.Equal(t, 2, len(underlying.RealpathCalls()))
94+
95+
cached.Realpath("/other/path")
96+
assert.Equal(t, 3, len(underlying.RealpathCalls()))
97+
}
98+
99+
func TestStat(t *testing.T) {
100+
t.Parallel()
101+
102+
underlying := createMockFS()
103+
cached := cachedvfs.From(underlying)
104+
105+
cached.Stat("/some/path")
106+
assert.Equal(t, 1, len(underlying.StatCalls()))
107+
108+
cached.Stat("/some/path")
109+
assert.Equal(t, 1, len(underlying.StatCalls()))
110+
111+
cached.ClearCache()
112+
cached.Stat("/some/path")
113+
assert.Equal(t, 2, len(underlying.StatCalls()))
114+
115+
cached.Stat("/other/path")
116+
assert.Equal(t, 3, len(underlying.StatCalls()))
117+
}
118+
119+
func TestReadFile(t *testing.T) {
120+
t.Parallel()
121+
122+
underlying := createMockFS()
123+
cached := cachedvfs.From(underlying)
124+
125+
cached.ReadFile("/some/path/file.txt")
126+
assert.Equal(t, 1, len(underlying.ReadFileCalls()))
127+
128+
cached.ReadFile("/some/path/file.txt")
129+
assert.Equal(t, 2, len(underlying.ReadFileCalls()))
130+
131+
cached.ClearCache()
132+
cached.ReadFile("/some/path/file.txt")
133+
assert.Equal(t, 3, len(underlying.ReadFileCalls()))
134+
}
135+
136+
func TestUseCaseSensitiveFileNames(t *testing.T) {
137+
t.Parallel()
138+
139+
underlying := createMockFS()
140+
cached := cachedvfs.From(underlying)
141+
142+
cached.UseCaseSensitiveFileNames()
143+
assert.Equal(t, 1, len(underlying.UseCaseSensitiveFileNamesCalls()))
144+
145+
cached.UseCaseSensitiveFileNames()
146+
assert.Equal(t, 2, len(underlying.UseCaseSensitiveFileNamesCalls()))
147+
148+
cached.ClearCache()
149+
cached.UseCaseSensitiveFileNames()
150+
assert.Equal(t, 3, len(underlying.UseCaseSensitiveFileNamesCalls()))
151+
}
152+
153+
func TestWalkDir(t *testing.T) {
154+
t.Parallel()
155+
156+
underlying := createMockFS()
157+
cached := cachedvfs.From(underlying)
158+
159+
walkFn := vfs.WalkDirFunc(func(path string, info vfs.DirEntry, err error) error {
160+
return nil
161+
})
162+
163+
_ = cached.WalkDir("/some/path", walkFn)
164+
assert.Equal(t, 1, len(underlying.WalkDirCalls()))
165+
166+
_ = cached.WalkDir("/some/path", walkFn)
167+
assert.Equal(t, 2, len(underlying.WalkDirCalls()))
168+
169+
cached.ClearCache()
170+
_ = cached.WalkDir("/some/path", walkFn)
171+
assert.Equal(t, 3, len(underlying.WalkDirCalls()))
172+
}
173+
174+
func TestRemove(t *testing.T) {
175+
t.Parallel()
176+
177+
underlying := createMockFS()
178+
cached := cachedvfs.From(underlying)
179+
180+
_ = cached.Remove("/some/path/file.txt")
181+
assert.Equal(t, 1, len(underlying.RemoveCalls()))
182+
183+
_ = cached.Remove("/some/path/file.txt")
184+
assert.Equal(t, 2, len(underlying.RemoveCalls()))
185+
186+
cached.ClearCache()
187+
_ = cached.Remove("/some/path/file.txt")
188+
assert.Equal(t, 3, len(underlying.RemoveCalls()))
189+
}
190+
191+
func TestWriteFile(t *testing.T) {
192+
t.Parallel()
193+
194+
underlying := createMockFS()
195+
cached := cachedvfs.From(underlying)
196+
197+
_ = cached.WriteFile("/some/path/file.txt", "new content", false)
198+
assert.Equal(t, 1, len(underlying.WriteFileCalls()))
199+
200+
_ = cached.WriteFile("/some/path/file.txt", "another content", true)
201+
assert.Equal(t, 2, len(underlying.WriteFileCalls()))
202+
203+
cached.ClearCache()
204+
_ = cached.WriteFile("/some/path/file.txt", "third content", false)
205+
assert.Equal(t, 3, len(underlying.WriteFileCalls()))
206+
207+
call := underlying.WriteFileCalls()[2]
208+
assert.Equal(t, "/some/path/file.txt", call.Path)
209+
assert.Equal(t, "third content", call.Data)
210+
assert.Equal(t, false, call.WriteByteOrderMark)
211+
}

Diff for: internal/vfs/vfs.go

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"io/fs"
55
)
66

7+
//go:generate go tool github.com/matryer/moq -fmt goimports -out vfsmock/mock_generated.go -pkg vfsmock . FS
8+
79
// FS is a file system abstraction.
810
type FS interface {
911
// UseCaseSensitiveFileNames returns true if the file system is case-sensitive.

0 commit comments

Comments
 (0)