vendor tsgo
This commit is contained in:
154
tools/tsgo/internal/vfs/cachedvfs/cachedvfs.go
Normal file
154
tools/tsgo/internal/vfs/cachedvfs/cachedvfs.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package cachedvfs
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
type FS struct {
|
||||
fs vfs.FS
|
||||
enabled atomic.Bool
|
||||
|
||||
directoryExistsCache collections.SyncMap[string, bool]
|
||||
fileExistsCache collections.SyncMap[string, bool]
|
||||
getAccessibleEntriesCache collections.SyncMap[string, vfs.Entries]
|
||||
realpathCache collections.SyncMap[string, string]
|
||||
statCache collections.SyncMap[string, vfs.FileInfo]
|
||||
}
|
||||
|
||||
var _ vfs.FS = (*FS)(nil)
|
||||
|
||||
func From(fs vfs.FS) *FS {
|
||||
fsys := &FS{fs: fs}
|
||||
fsys.enabled.Store(true)
|
||||
return fsys
|
||||
}
|
||||
|
||||
func (fsys *FS) DisableAndClearCache() {
|
||||
if fsys.enabled.CompareAndSwap(true, false) {
|
||||
fsys.ClearCache()
|
||||
}
|
||||
}
|
||||
|
||||
func (fsys *FS) Enable() {
|
||||
fsys.enabled.Store(true)
|
||||
}
|
||||
|
||||
func (fsys *FS) ClearCache() {
|
||||
fsys.directoryExistsCache.Clear()
|
||||
fsys.fileExistsCache.Clear()
|
||||
fsys.getAccessibleEntriesCache.Clear()
|
||||
fsys.realpathCache.Clear()
|
||||
fsys.statCache.Clear()
|
||||
}
|
||||
|
||||
func (fsys *FS) DirectoryExists(path string) bool {
|
||||
if fsys.enabled.Load() {
|
||||
if ret, ok := fsys.directoryExistsCache.Load(path); ok {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
ret := fsys.fs.DirectoryExists(path)
|
||||
|
||||
if fsys.enabled.Load() {
|
||||
fsys.directoryExistsCache.Store(path, ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (fsys *FS) FileExists(path string) bool {
|
||||
if fsys.enabled.Load() {
|
||||
if ret, ok := fsys.fileExistsCache.Load(path); ok {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
ret := fsys.fs.FileExists(path)
|
||||
|
||||
if fsys.enabled.Load() {
|
||||
fsys.fileExistsCache.Store(path, ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (fsys *FS) GetAccessibleEntries(path string) vfs.Entries {
|
||||
if fsys.enabled.Load() {
|
||||
if ret, ok := fsys.getAccessibleEntriesCache.Load(path); ok {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
ret := fsys.fs.GetAccessibleEntries(path)
|
||||
|
||||
if fsys.enabled.Load() {
|
||||
fsys.getAccessibleEntriesCache.Store(path, ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (fsys *FS) ReadFile(path string) (contents string, ok bool) {
|
||||
return fsys.fs.ReadFile(path)
|
||||
}
|
||||
|
||||
func (fsys *FS) Realpath(path string) string {
|
||||
if fsys.enabled.Load() {
|
||||
if ret, ok := fsys.realpathCache.Load(path); ok {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
ret := fsys.fs.Realpath(path)
|
||||
|
||||
if fsys.enabled.Load() {
|
||||
fsys.realpathCache.Store(path, ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (fsys *FS) Remove(path string) error {
|
||||
return fsys.fs.Remove(path)
|
||||
}
|
||||
|
||||
func (fsys *FS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
return fsys.fs.Chtimes(path, aTime, mTime)
|
||||
}
|
||||
|
||||
func (fsys *FS) Stat(path string) vfs.FileInfo {
|
||||
if fsys.enabled.Load() {
|
||||
if ret, ok := fsys.statCache.Load(path); ok {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
ret := fsys.fs.Stat(path)
|
||||
|
||||
if fsys.enabled.Load() {
|
||||
fsys.statCache.Store(path, ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (fsys *FS) UseCaseSensitiveFileNames() bool {
|
||||
return fsys.fs.UseCaseSensitiveFileNames()
|
||||
}
|
||||
|
||||
func (fsys *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
|
||||
return fsys.fs.WalkDir(root, walkFn)
|
||||
}
|
||||
|
||||
func (fsys *FS) WriteFile(path string, data string) error {
|
||||
return fsys.fs.WriteFile(path, data)
|
||||
}
|
||||
|
||||
func (fsys *FS) AppendFile(path string, data string) error {
|
||||
return fsys.fs.AppendFile(path, data)
|
||||
}
|
||||
350
tools/tsgo/internal/vfs/cachedvfs/cachedvfs_test.go
Normal file
350
tools/tsgo/internal/vfs/cachedvfs/cachedvfs_test.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package cachedvfs_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfsmock"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func createMockFS() *vfsmock.FSMock {
|
||||
return vfsmock.Wrap(vfstest.FromMap(map[string]string{
|
||||
"/some/path/file.txt": "hello world",
|
||||
}, true))
|
||||
}
|
||||
|
||||
func TestDirectoryExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
cached.DirectoryExists("/some/path")
|
||||
assert.Equal(t, 1, len(underlying.DirectoryExistsCalls()))
|
||||
|
||||
cached.DirectoryExists("/some/path")
|
||||
assert.Equal(t, 1, len(underlying.DirectoryExistsCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
cached.DirectoryExists("/some/path")
|
||||
assert.Equal(t, 2, len(underlying.DirectoryExistsCalls()))
|
||||
|
||||
cached.DirectoryExists("/other/path")
|
||||
assert.Equal(t, 3, len(underlying.DirectoryExistsCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
cached.DirectoryExists("/some/path")
|
||||
assert.Equal(t, 4, len(underlying.DirectoryExistsCalls()))
|
||||
|
||||
cached.DirectoryExists("/some/path")
|
||||
assert.Equal(t, 5, len(underlying.DirectoryExistsCalls()))
|
||||
|
||||
cached.Enable()
|
||||
cached.DirectoryExists("/some/path")
|
||||
assert.Equal(t, 6, len(underlying.DirectoryExistsCalls()))
|
||||
|
||||
cached.DirectoryExists("/some/path")
|
||||
assert.Equal(t, 6, len(underlying.DirectoryExistsCalls()))
|
||||
}
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
cached.FileExists("/some/path/file.txt")
|
||||
assert.Equal(t, 1, len(underlying.FileExistsCalls()))
|
||||
|
||||
cached.FileExists("/some/path/file.txt")
|
||||
assert.Equal(t, 1, len(underlying.FileExistsCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
cached.FileExists("/some/path/file.txt")
|
||||
assert.Equal(t, 2, len(underlying.FileExistsCalls()))
|
||||
|
||||
cached.FileExists("/other/path/file.txt")
|
||||
assert.Equal(t, 3, len(underlying.FileExistsCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
cached.FileExists("/some/path/file.txt")
|
||||
assert.Equal(t, 4, len(underlying.FileExistsCalls()))
|
||||
|
||||
cached.FileExists("/some/path/file.txt")
|
||||
assert.Equal(t, 5, len(underlying.FileExistsCalls()))
|
||||
|
||||
cached.Enable()
|
||||
cached.FileExists("/some/path/file.txt")
|
||||
assert.Equal(t, 6, len(underlying.FileExistsCalls()))
|
||||
|
||||
cached.FileExists("/some/path/file.txt")
|
||||
assert.Equal(t, 6, len(underlying.FileExistsCalls()))
|
||||
}
|
||||
|
||||
func TestGetAccessibleEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
cached.GetAccessibleEntries("/some/path")
|
||||
assert.Equal(t, 1, len(underlying.GetAccessibleEntriesCalls()))
|
||||
|
||||
cached.GetAccessibleEntries("/some/path")
|
||||
assert.Equal(t, 1, len(underlying.GetAccessibleEntriesCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
cached.GetAccessibleEntries("/some/path")
|
||||
assert.Equal(t, 2, len(underlying.GetAccessibleEntriesCalls()))
|
||||
|
||||
cached.GetAccessibleEntries("/other/path")
|
||||
assert.Equal(t, 3, len(underlying.GetAccessibleEntriesCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
cached.GetAccessibleEntries("/some/path")
|
||||
assert.Equal(t, 4, len(underlying.GetAccessibleEntriesCalls()))
|
||||
|
||||
cached.GetAccessibleEntries("/some/path")
|
||||
assert.Equal(t, 5, len(underlying.GetAccessibleEntriesCalls()))
|
||||
|
||||
cached.Enable()
|
||||
cached.GetAccessibleEntries("/some/path")
|
||||
assert.Equal(t, 6, len(underlying.GetAccessibleEntriesCalls()))
|
||||
|
||||
cached.GetAccessibleEntries("/some/path")
|
||||
assert.Equal(t, 6, len(underlying.GetAccessibleEntriesCalls()))
|
||||
}
|
||||
|
||||
func TestRealpath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
cached.Realpath("/some/path")
|
||||
assert.Equal(t, 1, len(underlying.RealpathCalls()))
|
||||
|
||||
cached.Realpath("/some/path")
|
||||
assert.Equal(t, 1, len(underlying.RealpathCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
cached.Realpath("/some/path")
|
||||
assert.Equal(t, 2, len(underlying.RealpathCalls()))
|
||||
|
||||
cached.Realpath("/other/path")
|
||||
assert.Equal(t, 3, len(underlying.RealpathCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
cached.Realpath("/some/path")
|
||||
assert.Equal(t, 4, len(underlying.RealpathCalls()))
|
||||
|
||||
cached.Realpath("/some/path")
|
||||
assert.Equal(t, 5, len(underlying.RealpathCalls()))
|
||||
|
||||
cached.Enable()
|
||||
cached.Realpath("/some/path")
|
||||
assert.Equal(t, 6, len(underlying.RealpathCalls()))
|
||||
|
||||
cached.Realpath("/some/path")
|
||||
assert.Equal(t, 6, len(underlying.RealpathCalls()))
|
||||
}
|
||||
|
||||
func TestStat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
cached.Stat("/some/path")
|
||||
assert.Equal(t, 1, len(underlying.StatCalls()))
|
||||
|
||||
cached.Stat("/some/path")
|
||||
assert.Equal(t, 1, len(underlying.StatCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
cached.Stat("/some/path")
|
||||
assert.Equal(t, 2, len(underlying.StatCalls()))
|
||||
|
||||
cached.Stat("/other/path")
|
||||
assert.Equal(t, 3, len(underlying.StatCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
cached.Stat("/some/path")
|
||||
assert.Equal(t, 4, len(underlying.StatCalls()))
|
||||
|
||||
cached.Stat("/some/path")
|
||||
assert.Equal(t, 5, len(underlying.StatCalls()))
|
||||
|
||||
cached.Enable()
|
||||
cached.Stat("/some/path")
|
||||
assert.Equal(t, 6, len(underlying.StatCalls()))
|
||||
|
||||
cached.Stat("/some/path")
|
||||
assert.Equal(t, 6, len(underlying.StatCalls()))
|
||||
}
|
||||
|
||||
func TestReadFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
cached.ReadFile("/some/path/file.txt")
|
||||
assert.Equal(t, 1, len(underlying.ReadFileCalls()))
|
||||
|
||||
cached.ReadFile("/some/path/file.txt")
|
||||
assert.Equal(t, 2, len(underlying.ReadFileCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
cached.ReadFile("/some/path/file.txt")
|
||||
assert.Equal(t, 3, len(underlying.ReadFileCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
cached.ReadFile("/some/path/file.txt")
|
||||
assert.Equal(t, 4, len(underlying.ReadFileCalls()))
|
||||
|
||||
cached.ReadFile("/some/path/file.txt")
|
||||
assert.Equal(t, 5, len(underlying.ReadFileCalls()))
|
||||
|
||||
cached.Enable()
|
||||
cached.ReadFile("/some/path/file.txt")
|
||||
assert.Equal(t, 6, len(underlying.ReadFileCalls()))
|
||||
|
||||
cached.ReadFile("/some/path/file.txt")
|
||||
assert.Equal(t, 7, len(underlying.ReadFileCalls()))
|
||||
}
|
||||
|
||||
func TestUseCaseSensitiveFileNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
cached.UseCaseSensitiveFileNames()
|
||||
assert.Equal(t, 1, len(underlying.UseCaseSensitiveFileNamesCalls()))
|
||||
|
||||
cached.UseCaseSensitiveFileNames()
|
||||
assert.Equal(t, 2, len(underlying.UseCaseSensitiveFileNamesCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
cached.UseCaseSensitiveFileNames()
|
||||
assert.Equal(t, 3, len(underlying.UseCaseSensitiveFileNamesCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
cached.UseCaseSensitiveFileNames()
|
||||
assert.Equal(t, 4, len(underlying.UseCaseSensitiveFileNamesCalls()))
|
||||
|
||||
cached.UseCaseSensitiveFileNames()
|
||||
assert.Equal(t, 5, len(underlying.UseCaseSensitiveFileNamesCalls()))
|
||||
|
||||
cached.Enable()
|
||||
cached.UseCaseSensitiveFileNames()
|
||||
assert.Equal(t, 6, len(underlying.UseCaseSensitiveFileNamesCalls()))
|
||||
|
||||
cached.UseCaseSensitiveFileNames()
|
||||
assert.Equal(t, 7, len(underlying.UseCaseSensitiveFileNamesCalls()))
|
||||
}
|
||||
|
||||
func TestWalkDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
walkFn := vfs.WalkDirFunc(func(path string, info vfs.DirEntry, err error) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = cached.WalkDir("/some/path", walkFn)
|
||||
assert.Equal(t, 1, len(underlying.WalkDirCalls()))
|
||||
|
||||
_ = cached.WalkDir("/some/path", walkFn)
|
||||
assert.Equal(t, 2, len(underlying.WalkDirCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
_ = cached.WalkDir("/some/path", walkFn)
|
||||
assert.Equal(t, 3, len(underlying.WalkDirCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
_ = cached.WalkDir("/some/path", walkFn)
|
||||
assert.Equal(t, 4, len(underlying.WalkDirCalls()))
|
||||
|
||||
_ = cached.WalkDir("/some/path", walkFn)
|
||||
assert.Equal(t, 5, len(underlying.WalkDirCalls()))
|
||||
|
||||
cached.Enable()
|
||||
_ = cached.WalkDir("/some/path", walkFn)
|
||||
assert.Equal(t, 6, len(underlying.WalkDirCalls()))
|
||||
|
||||
_ = cached.WalkDir("/some/path", walkFn)
|
||||
assert.Equal(t, 7, len(underlying.WalkDirCalls()))
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
_ = cached.Remove("/some/path/file.txt")
|
||||
assert.Equal(t, 1, len(underlying.RemoveCalls()))
|
||||
|
||||
_ = cached.Remove("/some/path/file.txt")
|
||||
assert.Equal(t, 2, len(underlying.RemoveCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
_ = cached.Remove("/some/path/file.txt")
|
||||
assert.Equal(t, 3, len(underlying.RemoveCalls()))
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
_ = cached.Remove("/some/path/file.txt")
|
||||
assert.Equal(t, 4, len(underlying.RemoveCalls()))
|
||||
|
||||
_ = cached.Remove("/some/path/file.txt")
|
||||
assert.Equal(t, 5, len(underlying.RemoveCalls()))
|
||||
|
||||
cached.Enable()
|
||||
_ = cached.Remove("/some/path/file.txt")
|
||||
assert.Equal(t, 6, len(underlying.RemoveCalls()))
|
||||
|
||||
_ = cached.Remove("/some/path/file.txt")
|
||||
assert.Equal(t, 7, len(underlying.RemoveCalls()))
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
underlying := createMockFS()
|
||||
cached := cachedvfs.From(underlying)
|
||||
|
||||
_ = cached.WriteFile("/some/path/file.txt", "new content")
|
||||
assert.Equal(t, 1, len(underlying.WriteFileCalls()))
|
||||
|
||||
_ = cached.WriteFile("/some/path/file.txt", "another content")
|
||||
assert.Equal(t, 2, len(underlying.WriteFileCalls()))
|
||||
|
||||
cached.ClearCache()
|
||||
_ = cached.WriteFile("/some/path/file.txt", "third content")
|
||||
assert.Equal(t, 3, len(underlying.WriteFileCalls()))
|
||||
|
||||
call := underlying.WriteFileCalls()[2]
|
||||
assert.Equal(t, "/some/path/file.txt", call.Path)
|
||||
assert.Equal(t, "third content", call.Data)
|
||||
|
||||
cached.DisableAndClearCache()
|
||||
_ = cached.WriteFile("/some/path/file.txt", "fourth content")
|
||||
assert.Equal(t, 4, len(underlying.WriteFileCalls()))
|
||||
|
||||
_ = cached.WriteFile("/some/path/file.txt", "fifth content")
|
||||
assert.Equal(t, 5, len(underlying.WriteFileCalls()))
|
||||
|
||||
cached.Enable()
|
||||
_ = cached.WriteFile("/some/path/file.txt", "sixth content")
|
||||
assert.Equal(t, 6, len(underlying.WriteFileCalls()))
|
||||
|
||||
_ = cached.WriteFile("/some/path/file.txt", "seventh content")
|
||||
assert.Equal(t, 7, len(underlying.WriteFileCalls()))
|
||||
}
|
||||
194
tools/tsgo/internal/vfs/internal/internal.go
Normal file
194
tools/tsgo/internal/vfs/internal/internal.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
type Common struct {
|
||||
RootFor func(root string) fs.FS
|
||||
IsReparsePoint func(path string) bool
|
||||
}
|
||||
|
||||
func RootLength(p string) int {
|
||||
l := tspath.GetEncodedRootLength(p)
|
||||
if l == 0 {
|
||||
panic(fmt.Sprintf("vfs: path %q is not absolute", p))
|
||||
} else if l < 0 {
|
||||
return ^l
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func SplitPath(p string) (rootName, rest string) {
|
||||
p = tspath.NormalizePath(p)
|
||||
l := RootLength(p)
|
||||
rootName, rest = p[:l], p[l:]
|
||||
rest = tspath.RemoveTrailingDirectorySeparator(rest)
|
||||
return rootName, rest
|
||||
}
|
||||
|
||||
func (vfs *Common) RootAndPath(path string) (fsys fs.FS, rootName string, rest string) {
|
||||
rootName, rest = SplitPath(path)
|
||||
if rest == "" {
|
||||
rest = "."
|
||||
}
|
||||
return vfs.RootFor(rootName), rootName, rest
|
||||
}
|
||||
|
||||
func (vfs *Common) Stat(path string) vfs.FileInfo {
|
||||
fsys, _, rest := vfs.RootAndPath(path)
|
||||
if fsys == nil {
|
||||
return nil
|
||||
}
|
||||
stat, err := fs.Stat(fsys, rest)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return stat
|
||||
}
|
||||
|
||||
func (vfs *Common) FileExists(path string) bool {
|
||||
stat := vfs.Stat(path)
|
||||
return stat != nil && !stat.IsDir()
|
||||
}
|
||||
|
||||
func (vfs *Common) DirectoryExists(path string) bool {
|
||||
stat := vfs.Stat(path)
|
||||
return stat != nil && stat.IsDir()
|
||||
}
|
||||
|
||||
func (vfs *Common) GetAccessibleEntries(path string) (result vfs.Entries) {
|
||||
result.Symlinks = map[string]struct{}{}
|
||||
|
||||
addToResult := func(name string, mode fs.FileMode, isLink bool) (added bool) {
|
||||
if mode.IsDir() {
|
||||
result.Directories = append(result.Directories, name)
|
||||
} else if mode.IsRegular() {
|
||||
result.Files = append(result.Files, name)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
if isLink {
|
||||
result.Symlinks[name] = struct{}{}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
for _, entry := range vfs.getEntries(path) {
|
||||
entryType := entry.Type()
|
||||
|
||||
if addToResult(entry.Name(), entryType, false) {
|
||||
continue
|
||||
}
|
||||
|
||||
if entryType&fs.ModeSymlink != 0 {
|
||||
// Easy case; UNIX-like system will clearly mark symlinks.
|
||||
if stat := vfs.Stat(path + "/" + entry.Name()); stat != nil {
|
||||
addToResult(entry.Name(), stat.Mode(), true)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if entryType&fs.ModeIrregular != 0 && vfs.IsReparsePoint != nil {
|
||||
// Could be a Windows junction or other reparse point.
|
||||
// Check using the OS-specific helper.
|
||||
fullPath := path + "/" + entry.Name()
|
||||
if vfs.IsReparsePoint(fullPath) {
|
||||
if stat := vfs.Stat(fullPath); stat != nil {
|
||||
addToResult(entry.Name(), stat.Mode(), true)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (vfs *Common) getEntries(path string) []vfs.DirEntry {
|
||||
fsys, _, rest := vfs.RootAndPath(path)
|
||||
if fsys == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries, err := fs.ReadDir(fsys, rest)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func (vfs *Common) WalkDir(root string, walkFn fs.WalkDirFunc) error {
|
||||
fsys, rootName, rest := vfs.RootAndPath(root)
|
||||
if fsys == nil {
|
||||
return nil
|
||||
}
|
||||
return fs.WalkDir(fsys, rest, func(path string, d fs.DirEntry, err error) error {
|
||||
if path == "." {
|
||||
path = ""
|
||||
}
|
||||
return walkFn(rootName+path, d, err)
|
||||
})
|
||||
}
|
||||
|
||||
func (vfs *Common) ReadFile(path string) (contents string, ok bool) {
|
||||
fsys, _, rest := vfs.RootAndPath(path)
|
||||
if fsys == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
b, err := fs.ReadFile(fsys, rest)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// An invariant of any underlying filesystem is that the bytes returned
|
||||
// are immutable, otherwise anyone using the filesystem would end up
|
||||
// with data races.
|
||||
//
|
||||
// This means that we can safely convert the bytes to a string directly,
|
||||
// saving a copy.
|
||||
if len(b) == 0 {
|
||||
return "", true
|
||||
}
|
||||
|
||||
s := unsafe.String(&b[0], len(b))
|
||||
|
||||
return decodeBytes(s)
|
||||
}
|
||||
|
||||
func decodeBytes(s string) (contents string, ok bool) {
|
||||
var bom [2]byte
|
||||
if len(s) >= 2 {
|
||||
bom = [2]byte{s[0], s[1]}
|
||||
switch bom {
|
||||
case [2]byte{0xFF, 0xFE}:
|
||||
return decodeUtf16(s[2:], binary.LittleEndian), true
|
||||
case [2]byte{0xFE, 0xFF}:
|
||||
return decodeUtf16(s[2:], binary.BigEndian), true
|
||||
}
|
||||
}
|
||||
if len(s) >= 3 && s[0] == 0xEF && s[1] == 0xBB && s[2] == 0xBF {
|
||||
s = s[3:]
|
||||
}
|
||||
|
||||
return s, true
|
||||
}
|
||||
|
||||
func decodeUtf16(s string, order binary.ByteOrder) string {
|
||||
ints := make([]uint16, len(s)/2)
|
||||
if err := binary.Read(strings.NewReader(s), order, &ints); err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(utf16.Decode(ints))
|
||||
}
|
||||
222
tools/tsgo/internal/vfs/iovfs/iofs.go
Normal file
222
tools/tsgo/internal/vfs/iovfs/iofs.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package iovfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/internal"
|
||||
)
|
||||
|
||||
type RealpathFS interface {
|
||||
fs.FS
|
||||
Realpath(path string) (string, error)
|
||||
}
|
||||
|
||||
type WritableFS interface {
|
||||
fs.FS
|
||||
WriteFile(path string, data string, perm fs.FileMode) error
|
||||
AppendFile(path string, data string, perm fs.FileMode) error
|
||||
MkdirAll(path string, perm fs.FileMode) error
|
||||
// Removes `path` and all its contents. Will return the first error it encounters.
|
||||
Remove(path string) error
|
||||
Chtimes(path string, aTime time.Time, mTime time.Time) error
|
||||
}
|
||||
|
||||
type FsWithSys interface {
|
||||
vfs.FS
|
||||
FSys() fs.FS
|
||||
}
|
||||
|
||||
// From creates a new FS from an [fs.FS].
|
||||
//
|
||||
// For paths like `c:/foo/bar`, fsys will be used as though it's rooted at `/` and the path is `/c:/foo/bar`.
|
||||
//
|
||||
// If the provided [fs.FS] implements [RealpathFS], it will be used to implement the Realpath method.
|
||||
// If the provided [fs.FS] implements [WritableFS], it will be used to implement the WriteFile method.
|
||||
//
|
||||
// From does not actually handle case-insensitivity; ensure the passed in [fs.FS]
|
||||
// respects case-insensitive file names if needed. Consider using [vfstest.FromMap] for testing.
|
||||
func From(fsys fs.FS, useCaseSensitiveFileNames bool) FsWithSys {
|
||||
var realpath func(path string) (string, error)
|
||||
if fsys, ok := fsys.(RealpathFS); ok {
|
||||
realpath = func(path string) (string, error) {
|
||||
rest, hadSlash := strings.CutPrefix(path, "/")
|
||||
rp, err := fsys.Realpath(rest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if hadSlash {
|
||||
return "/" + rp, nil
|
||||
}
|
||||
return rp, nil
|
||||
}
|
||||
} else {
|
||||
realpath = func(path string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
var writeFile func(path string, content string) error
|
||||
var appendFile func(path string, content string) error
|
||||
var mkdirAll func(path string) error
|
||||
var remove func(path string) error
|
||||
var chtimes func(path string, aTime time.Time, mTime time.Time) error
|
||||
if fsys, ok := fsys.(WritableFS); ok {
|
||||
writeFile = func(path string, content string) error {
|
||||
rest, _ := strings.CutPrefix(path, "/")
|
||||
return fsys.WriteFile(rest, content, 0o666)
|
||||
}
|
||||
appendFile = func(path string, content string) error {
|
||||
rest, _ := strings.CutPrefix(path, "/")
|
||||
return fsys.AppendFile(rest, content, 0o666)
|
||||
}
|
||||
mkdirAll = func(path string) error {
|
||||
rest, _ := strings.CutPrefix(path, "/")
|
||||
return fsys.MkdirAll(rest, 0o777)
|
||||
}
|
||||
remove = func(path string) error {
|
||||
rest, _ := strings.CutPrefix(path, "/")
|
||||
return fsys.Remove(rest)
|
||||
}
|
||||
chtimes = func(path string, aTime time.Time, mTime time.Time) error {
|
||||
rest, _ := strings.CutPrefix(path, "/")
|
||||
return fsys.Chtimes(rest, aTime, mTime)
|
||||
}
|
||||
} else {
|
||||
writeFile = func(string, string) error {
|
||||
panic("writeFile not supported")
|
||||
}
|
||||
appendFile = func(string, string) error {
|
||||
panic("appendFile not supported")
|
||||
}
|
||||
mkdirAll = func(string) error {
|
||||
panic("mkdirAll not supported")
|
||||
}
|
||||
remove = func(string) error {
|
||||
panic("remove not supported")
|
||||
}
|
||||
chtimes = func(string, time.Time, time.Time) error {
|
||||
panic("chtimes not supported")
|
||||
}
|
||||
}
|
||||
|
||||
return &ioFS{
|
||||
common: internal.Common{
|
||||
RootFor: func(root string) fs.FS {
|
||||
if root == "/" {
|
||||
return fsys
|
||||
}
|
||||
|
||||
p := tspath.RemoveTrailingDirectorySeparator(root)
|
||||
sub, err := fs.Sub(fsys, p)
|
||||
if err != nil {
|
||||
if tspath.IsUrl(root) {
|
||||
return nil
|
||||
}
|
||||
panic(fmt.Sprintf("vfs: failed to create sub file system for %q: %v", p, err))
|
||||
}
|
||||
return sub
|
||||
},
|
||||
},
|
||||
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
|
||||
realpath: realpath,
|
||||
writeFile: writeFile,
|
||||
appendFile: appendFile,
|
||||
mkdirAll: mkdirAll,
|
||||
remove: remove,
|
||||
chtimes: chtimes,
|
||||
fsys: fsys,
|
||||
}
|
||||
}
|
||||
|
||||
type ioFS struct {
|
||||
common internal.Common
|
||||
|
||||
useCaseSensitiveFileNames bool
|
||||
realpath func(path string) (string, error)
|
||||
writeFile func(path string, content string) error
|
||||
appendFile func(path string, content string) error
|
||||
mkdirAll func(path string) error
|
||||
remove func(path string) error
|
||||
chtimes func(path string, aTime time.Time, mTime time.Time) error
|
||||
fsys fs.FS
|
||||
}
|
||||
|
||||
var _ FsWithSys = (*ioFS)(nil)
|
||||
|
||||
func (vfs *ioFS) UseCaseSensitiveFileNames() bool {
|
||||
return vfs.useCaseSensitiveFileNames
|
||||
}
|
||||
|
||||
func (vfs *ioFS) DirectoryExists(path string) bool {
|
||||
return vfs.common.DirectoryExists(path)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) FileExists(path string) bool {
|
||||
return vfs.common.FileExists(path)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) GetAccessibleEntries(path string) vfs.Entries {
|
||||
return vfs.common.GetAccessibleEntries(path)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) Stat(path string) vfs.FileInfo {
|
||||
_ = internal.RootLength(path) // Assert path is rooted
|
||||
return vfs.common.Stat(path)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) ReadFile(path string) (contents string, ok bool) {
|
||||
return vfs.common.ReadFile(path)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
|
||||
return vfs.common.WalkDir(root, walkFn)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) Remove(path string) error {
|
||||
_ = internal.RootLength(path) // Assert path is rooted
|
||||
return vfs.remove(path)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
_ = internal.RootLength(path) // Assert path is rooted
|
||||
return vfs.chtimes(path, aTime, mTime)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) Realpath(path string) string {
|
||||
root, rest := internal.SplitPath(path)
|
||||
// splitPath normalizes the path into parts (e.g. "c:/foo/bar" -> "c:/", "foo/bar")
|
||||
// Put them back together to call realpath.
|
||||
realpath, err := vfs.realpath(root + rest)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
return realpath
|
||||
}
|
||||
|
||||
func (vfs *ioFS) writeFileEnsuringDir(path string, content string, write func(path, content string) error) error {
|
||||
_ = internal.RootLength(path) // Assert path is rooted
|
||||
if err := write(path, content); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := vfs.mkdirAll(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil {
|
||||
return err
|
||||
}
|
||||
return write(path, content)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) WriteFile(path string, content string) error {
|
||||
return vfs.writeFileEnsuringDir(path, content, vfs.writeFile)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) AppendFile(path string, content string) error {
|
||||
return vfs.writeFileEnsuringDir(path, content, vfs.appendFile)
|
||||
}
|
||||
|
||||
func (vfs *ioFS) FSys() fs.FS {
|
||||
return vfs.fsys
|
||||
}
|
||||
134
tools/tsgo/internal/vfs/iovfs/iofs_test.go
Normal file
134
tools/tsgo/internal/vfs/iovfs/iofs_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package iovfs_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/testutil"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/iovfs"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestIOFS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testfs := fstest.MapFS{
|
||||
"foo.ts": &fstest.MapFile{
|
||||
Data: []byte("hello, world"),
|
||||
},
|
||||
"dir1/file1.ts": &fstest.MapFile{
|
||||
Data: []byte("export const foo = 42;"),
|
||||
},
|
||||
"dir1/file2.ts": &fstest.MapFile{
|
||||
Data: []byte("export const foo = 42;"),
|
||||
},
|
||||
"dir2/file1.ts": &fstest.MapFile{
|
||||
Data: []byte("export const foo = 42;"),
|
||||
},
|
||||
}
|
||||
|
||||
fs := iovfs.From(testfs, true)
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, ok := fs.ReadFile("/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/does/not/exist.ts")
|
||||
assert.Assert(t, !ok)
|
||||
assert.Equal(t, content, "")
|
||||
})
|
||||
|
||||
t.Run("ReadFileUnrooted", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() { fs.ReadFile("bar") }, `vfs: path "bar" is not absolute`)
|
||||
})
|
||||
|
||||
t.Run("FileExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, fs.FileExists("/foo.ts"))
|
||||
assert.Assert(t, !fs.FileExists("/bar"))
|
||||
})
|
||||
|
||||
t.Run("DirectoryExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, fs.DirectoryExists("/"))
|
||||
assert.Assert(t, fs.DirectoryExists("/dir1"))
|
||||
assert.Assert(t, fs.DirectoryExists("/dir1/"))
|
||||
assert.Assert(t, fs.DirectoryExists("/dir1/./"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/bar"))
|
||||
})
|
||||
|
||||
t.Run("GetAccessibleEntries", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
entries := fs.GetAccessibleEntries("/")
|
||||
assert.DeepEqual(t, entries.Directories, []string{"dir1", "dir2"})
|
||||
assert.DeepEqual(t, entries.Files, []string{"foo.ts"})
|
||||
})
|
||||
|
||||
t.Run("WalkDir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var files []string
|
||||
err := fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
files = append(files, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
slices.Sort(files)
|
||||
|
||||
assert.DeepEqual(t, files, []string{"/dir1/file1.ts", "/dir1/file2.ts", "/dir2/file1.ts", "/foo.ts"})
|
||||
})
|
||||
|
||||
t.Run("WalkDirSkip", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var files []string
|
||||
err := fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
files = append(files, path)
|
||||
}
|
||||
|
||||
if path == "/" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return vfs.SkipDir
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
slices.Sort(files)
|
||||
|
||||
assert.DeepEqual(t, files, []string{"/foo.ts"})
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
realpath := fs.Realpath("/foo.ts")
|
||||
assert.Equal(t, realpath, "/foo.ts")
|
||||
})
|
||||
|
||||
t.Run("UseCaseSensitiveFileNames", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, fs.UseCaseSensitiveFileNames())
|
||||
})
|
||||
}
|
||||
27
tools/tsgo/internal/vfs/osvfs/helpers_test.go
Normal file
27
tools/tsgo/internal/vfs/osvfs/helpers_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package osvfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func mklink(tb testing.TB, target, link string, isDir bool) {
|
||||
tb.Helper()
|
||||
|
||||
if runtime.GOOS == "windows" && isDir {
|
||||
// Don't use os.Symlink on Windows, as it creates a "real" symlink, not a junction.
|
||||
assert.NilError(tb, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run())
|
||||
} else {
|
||||
err := os.Symlink(target, link)
|
||||
if err != nil && !isDir && runtime.GOOS == "windows" && strings.Contains(err.Error(), "A required privilege is not held by the client") {
|
||||
tb.Log(err)
|
||||
tb.Skip("file symlink support is not enabled without elevation or developer mode")
|
||||
}
|
||||
assert.NilError(tb, err)
|
||||
}
|
||||
}
|
||||
242
tools/tsgo/internal/vfs/osvfs/os.go
Normal file
242
tools/tsgo/internal/vfs/osvfs/os.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package osvfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/nativepath"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
// Semaphore for operations that are effectively blocking syscalls.
|
||||
blockingOpSema = core.NewLimitedSemaphore(128)
|
||||
// Semaphore for file reads.
|
||||
readSema = core.NewLimitedSemaphore(128)
|
||||
// Semaphore for file writes.
|
||||
writeSema = core.NewLimitedSemaphore(32)
|
||||
)
|
||||
|
||||
// FS creates a new FS from the OS file system.
|
||||
func FS() vfs.FS {
|
||||
return osVFS
|
||||
}
|
||||
|
||||
var osVFS vfs.FS = &osFS{
|
||||
common: internal.Common{
|
||||
RootFor: os.DirFS,
|
||||
IsReparsePoint: isReparsePoint,
|
||||
},
|
||||
}
|
||||
|
||||
type osFS struct {
|
||||
common internal.Common
|
||||
}
|
||||
|
||||
// We do this right at startup to minimize the chance that executable gets moved or deleted.
|
||||
var isFileSystemCaseSensitive = func() bool {
|
||||
// win32/win64 are case insensitive platforms
|
||||
if runtime.GOOS == "windows" {
|
||||
return false
|
||||
}
|
||||
|
||||
if runtime.GOARCH == "wasm" {
|
||||
// !!! Who knows; this depends on the host implementation.
|
||||
return true
|
||||
}
|
||||
|
||||
// As a proxy for case-insensitivity, we check if the current executable exists under a different case.
|
||||
// This is not entirely correct, since different OSs can have differing case sensitivity in different paths,
|
||||
// but this is largely good enough for our purposes (and what sys.ts used to do with __filename).
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("vfs: failed to get executable path: %v", err))
|
||||
}
|
||||
|
||||
// If the current executable exists under a different case, we must be case-insensitive.
|
||||
swapped := swapCase(exe)
|
||||
if _, err := os.Stat(swapped); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
panic(fmt.Sprintf("vfs: failed to stat %q: %v", swapped, err))
|
||||
}
|
||||
return false
|
||||
}()
|
||||
|
||||
// Convert all lowercase chars to uppercase, and vice-versa
|
||||
func swapCase(str string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
upper := unicode.ToUpper(r)
|
||||
if upper == r {
|
||||
return unicode.ToLower(r)
|
||||
} else {
|
||||
return upper
|
||||
}
|
||||
}, str)
|
||||
}
|
||||
|
||||
func (vfs *osFS) UseCaseSensitiveFileNames() bool {
|
||||
return isFileSystemCaseSensitive
|
||||
}
|
||||
|
||||
func (vfs *osFS) ReadFile(path string) (contents string, ok bool) {
|
||||
defer readSema.Acquire()()
|
||||
return vfs.common.ReadFile(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) DirectoryExists(path string) bool {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return vfs.common.DirectoryExists(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) FileExists(path string) bool {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return vfs.common.FileExists(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) GetAccessibleEntries(path string) vfs.Entries {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return vfs.common.GetAccessibleEntries(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) Stat(path string) vfs.FileInfo {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return vfs.common.Stat(path)
|
||||
}
|
||||
|
||||
var limitedWalkDirFuncPool = sync.Pool{
|
||||
New: func() any {
|
||||
w := &limitedWalkDirFunc{}
|
||||
w.walk = w.walker
|
||||
return w
|
||||
},
|
||||
}
|
||||
|
||||
func getLimitedWalkDirFunc(walkFn vfs.WalkDirFunc) *limitedWalkDirFunc {
|
||||
w := limitedWalkDirFuncPool.Get().(*limitedWalkDirFunc)
|
||||
w.inner = walkFn
|
||||
return w
|
||||
}
|
||||
|
||||
func putLimitedWalkDirFunc(w *limitedWalkDirFunc) {
|
||||
w.inner = nil
|
||||
limitedWalkDirFuncPool.Put(w)
|
||||
}
|
||||
|
||||
type limitedWalkDirFunc struct {
|
||||
inner vfs.WalkDirFunc
|
||||
walk vfs.WalkDirFunc
|
||||
}
|
||||
|
||||
func (w *limitedWalkDirFunc) walker(path string, d fs.DirEntry, err error) error {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return w.inner(path, d, err)
|
||||
}
|
||||
|
||||
func (vfs *osFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
|
||||
walker := getLimitedWalkDirFunc(walkFn)
|
||||
defer putLimitedWalkDirFunc(walker)
|
||||
return vfs.common.WalkDir(root, walker.walk)
|
||||
}
|
||||
|
||||
func (vfs *osFS) Realpath(path string) string {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return osFSRealpath(path)
|
||||
}
|
||||
|
||||
func osFSRealpath(path string) string {
|
||||
_ = internal.RootLength(path) // Assert path is rooted
|
||||
|
||||
orig := path
|
||||
path = filepath.FromSlash(path)
|
||||
path, err := nativepath.Realpath(path)
|
||||
if err != nil {
|
||||
return orig
|
||||
}
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return orig
|
||||
}
|
||||
return tspath.NormalizeSlashes(path)
|
||||
}
|
||||
|
||||
func isReparsePoint(path string) bool {
|
||||
return nativepath.IsSymlinkOrReparsePoint(filepath.FromSlash(path))
|
||||
}
|
||||
|
||||
func (vfs *osFS) writeFileWithFlag(path string, content string, flag int) error {
|
||||
defer writeSema.Acquire()()
|
||||
|
||||
file, err := os.OpenFile(path, flag, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vfs *osFS) ensureDirectoryExists(directoryPath string) error {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return os.MkdirAll(directoryPath, 0o777)
|
||||
}
|
||||
|
||||
func (vfs *osFS) writeFileEnsuringDir(path string, content string, flag int) error {
|
||||
_ = internal.RootLength(path) // Assert path is rooted
|
||||
if err := vfs.writeFileWithFlag(path, content, flag); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := vfs.ensureDirectoryExists(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil {
|
||||
return err
|
||||
}
|
||||
return vfs.writeFileWithFlag(path, content, flag)
|
||||
}
|
||||
|
||||
func (vfs *osFS) WriteFile(path string, content string) error {
|
||||
return vfs.writeFileEnsuringDir(path, content, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
|
||||
}
|
||||
|
||||
func (vfs *osFS) AppendFile(path string, content string) error {
|
||||
return vfs.writeFileEnsuringDir(path, content, os.O_WRONLY|os.O_CREATE|os.O_APPEND)
|
||||
}
|
||||
|
||||
func (vfs *osFS) Remove(path string) error {
|
||||
defer blockingOpSema.Acquire()()
|
||||
// todo: #701 add retry mechanism?
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
||||
func (vfs *osFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
defer blockingOpSema.Acquire()()
|
||||
return os.Chtimes(path, aTime, mTime)
|
||||
}
|
||||
|
||||
func GetGlobalTypingsCacheLocation() string {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
cacheDir = os.TempDir()
|
||||
}
|
||||
|
||||
var subdir string
|
||||
if runtime.GOOS == "windows" {
|
||||
subdir = "Microsoft/TypeScript"
|
||||
} else {
|
||||
subdir = "typescript"
|
||||
}
|
||||
return tspath.CombinePaths(cacheDir, subdir, core.VersionMajorMinor())
|
||||
}
|
||||
67
tools/tsgo/internal/vfs/osvfs/os_test.go
Normal file
67
tools/tsgo/internal/vfs/osvfs/os_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package osvfs_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/repo"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestOS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := osvfs.FS()
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
goMod := filepath.Join(repo.RootPath(), "go.mod")
|
||||
goModPath := tspath.NormalizePath(goMod)
|
||||
|
||||
expectedRaw, err := os.ReadFile(goMod)
|
||||
assert.NilError(t, err)
|
||||
expected := string(expectedRaw)
|
||||
|
||||
contents, ok := fs.ReadFile(goModPath)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, contents, expected)
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
home = tspath.NormalizePath(home)
|
||||
|
||||
expected := home
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows drive letters can be lowercase, but realpath will always return uppercase.
|
||||
expected = strings.ToUpper(expected[:1]) + expected[1:]
|
||||
}
|
||||
realpath := fs.Realpath(home)
|
||||
assert.Equal(t, realpath, expected)
|
||||
})
|
||||
|
||||
t.Run("UseCaseSensitiveFileNames", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Just check that it works.
|
||||
fs.UseCaseSensitiveFileNames()
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
assert.Assert(t, !fs.UseCaseSensitiveFileNames())
|
||||
case "linux":
|
||||
assert.Assert(t, fs.UseCaseSensitiveFileNames())
|
||||
}
|
||||
})
|
||||
}
|
||||
152
tools/tsgo/internal/vfs/osvfs/realpath_test.go
Normal file
152
tools/tsgo/internal/vfs/osvfs/realpath_test.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package osvfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestSymlinkRealpath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
targetFile, linkFile := setupSymlinks(t)
|
||||
|
||||
gotContents, err := os.ReadFile(linkFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(gotContents), "hello")
|
||||
|
||||
fs := FS()
|
||||
|
||||
targetRealpath := fs.Realpath(tspath.NormalizePath(targetFile))
|
||||
linkRealpath := fs.Realpath(tspath.NormalizePath(linkFile))
|
||||
|
||||
if targetRealpath != linkRealpath {
|
||||
t.Errorf("expected realpath of target and link to be equal, got %q and %q", targetRealpath, linkRealpath)
|
||||
cmd := exec.Command("node", "-e", `console.log({ native: fs.realpathSync.native(process.argv[1]), node: fs.realpathSync(process.argv[1]) })`, linkFile)
|
||||
out, err := cmd.CombinedOutput()
|
||||
assert.NilError(t, err)
|
||||
t.Logf("node: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func setupSymlinks(tb testing.TB) (targetFile, linkFile string) {
|
||||
tb.Helper()
|
||||
|
||||
tmp := tb.TempDir()
|
||||
|
||||
target := filepath.Join(tmp, "target")
|
||||
targetFile = filepath.Join(target, "file")
|
||||
|
||||
link := filepath.Join(tmp, "link")
|
||||
linkFile = filepath.Join(link, "file")
|
||||
|
||||
assert.NilError(tb, os.MkdirAll(target, 0o777))
|
||||
assert.NilError(tb, os.WriteFile(targetFile, []byte("hello"), 0o666))
|
||||
|
||||
mklink(tb, target, link, true)
|
||||
|
||||
return targetFile, linkFile
|
||||
}
|
||||
|
||||
func BenchmarkRealpath(b *testing.B) {
|
||||
targetFile, linkFile := setupSymlinks(b)
|
||||
|
||||
fs := FS()
|
||||
normalizedTargetFile := tspath.NormalizePath(targetFile)
|
||||
normalizedLinkFile := tspath.NormalizePath(linkFile)
|
||||
|
||||
b.Run("target", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
fs.Realpath(normalizedTargetFile)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("link", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
fs.Realpath(normalizedLinkFile)
|
||||
}
|
||||
})
|
||||
|
||||
// Simulate a deep node_modules path to show scaling with depth.
|
||||
deepDir := b.TempDir()
|
||||
for _, seg := range []string{"project", "node_modules", "@scope", "package", "node_modules", "dep", "lib", "dist", "esm", "internal", "utils"} {
|
||||
deepDir = filepath.Join(deepDir, seg)
|
||||
}
|
||||
assert.NilError(b, os.MkdirAll(deepDir, 0o777))
|
||||
deepFile := filepath.Join(deepDir, "index.js")
|
||||
assert.NilError(b, os.WriteFile(deepFile, []byte("module.exports = {}"), 0o666))
|
||||
normalizedDeepFile := tspath.NormalizePath(deepFile)
|
||||
|
||||
b.Run("deep", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
fs.Realpath(normalizedDeepFile)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("deep_evalSymlinks", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
deepNative := filepath.FromSlash(normalizedDeepFile)
|
||||
|
||||
for b.Loop() {
|
||||
filepath.EvalSymlinks(deepNative) //nolint:errcheck
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAccessibleEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
target := filepath.Join(tmp, "target")
|
||||
link := filepath.Join(tmp, "link")
|
||||
|
||||
assert.NilError(t, os.MkdirAll(target, 0o777))
|
||||
assert.NilError(t, os.MkdirAll(link, 0o777))
|
||||
|
||||
targetFile1 := filepath.Join(target, "file1")
|
||||
targetFile2 := filepath.Join(target, "file2")
|
||||
|
||||
assert.NilError(t, os.WriteFile(targetFile1, []byte("hello"), 0o666))
|
||||
assert.NilError(t, os.WriteFile(targetFile2, []byte("world"), 0o666))
|
||||
|
||||
targetDir1 := filepath.Join(target, "dir1")
|
||||
targetDir2 := filepath.Join(target, "dir2")
|
||||
|
||||
assert.NilError(t, os.MkdirAll(targetDir1, 0o777))
|
||||
assert.NilError(t, os.MkdirAll(targetDir2, 0o777))
|
||||
|
||||
mklink(t, targetFile1, filepath.Join(link, "file1"), false)
|
||||
mklink(t, targetFile2, filepath.Join(link, "file2"), false)
|
||||
mklink(t, targetDir1, filepath.Join(link, "dir1"), true)
|
||||
mklink(t, targetDir2, filepath.Join(link, "dir2"), true)
|
||||
|
||||
fs := FS()
|
||||
|
||||
entries := fs.GetAccessibleEntries(tspath.NormalizePath(link))
|
||||
|
||||
assert.DeepEqual(t, entries.Directories, []string{"dir1", "dir2"})
|
||||
assert.DeepEqual(t, entries.Files, []string{"file1", "file2"})
|
||||
assert.Check(t, entries.Symlinks != nil, "expected Symlinks to be set for directory with symlinks")
|
||||
assert.Equal(t, len(entries.Symlinks), 4)
|
||||
for _, name := range []string{"file1", "file2", "dir1", "dir2"} {
|
||||
_, ok := entries.Symlinks[name]
|
||||
assert.Check(t, ok, "expected %q to be in Symlinks", name)
|
||||
}
|
||||
|
||||
// Non-symlink directory should have empty Symlinks.
|
||||
entries = fs.GetAccessibleEntries(tspath.NormalizePath(target))
|
||||
assert.DeepEqual(t, entries.Directories, []string{"dir1", "dir2"})
|
||||
assert.DeepEqual(t, entries.Files, []string{"file1", "file2"})
|
||||
assert.Check(t, entries.Symlinks != nil, "expected Symlinks to be non-nil for directory without symlinks")
|
||||
assert.Equal(t, len(entries.Symlinks), 0)
|
||||
}
|
||||
76
tools/tsgo/internal/vfs/trackingvfs/trackingvfs.go
Normal file
76
tools/tsgo/internal/vfs/trackingvfs/trackingvfs.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Package trackingvfs provides a VFS wrapper that records every file path
|
||||
// accessed during compilation. This allows watch mode to know exactly which
|
||||
// files and directories the compiler depended on, including non-existent
|
||||
// paths from failed module resolution.
|
||||
package trackingvfs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
// FS wraps a vfs.FS and records every path accessed via read-like operations.
|
||||
// Write operations (WriteFile, Remove, Chtimes) are not tracked since they
|
||||
// represent outputs, not dependencies.
|
||||
type FS struct {
|
||||
Inner vfs.FS
|
||||
SeenFiles collections.SyncSet[string]
|
||||
}
|
||||
|
||||
var _ vfs.FS = (*FS)(nil)
|
||||
|
||||
func (fs *FS) ReadFile(path string) (string, bool) {
|
||||
fs.SeenFiles.Add(path)
|
||||
return fs.Inner.ReadFile(path)
|
||||
}
|
||||
|
||||
func (fs *FS) FileExists(path string) bool {
|
||||
fs.SeenFiles.Add(path)
|
||||
return fs.Inner.FileExists(path)
|
||||
}
|
||||
|
||||
func (fs *FS) UseCaseSensitiveFileNames() bool { return fs.Inner.UseCaseSensitiveFileNames() }
|
||||
|
||||
func (fs *FS) WriteFile(path string, data string) error {
|
||||
return fs.Inner.WriteFile(path, data)
|
||||
}
|
||||
|
||||
func (fs *FS) AppendFile(path string, data string) error {
|
||||
return fs.Inner.AppendFile(path, data)
|
||||
}
|
||||
|
||||
func (fs *FS) Remove(path string) error { return fs.Inner.Remove(path) }
|
||||
|
||||
func (fs *FS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
return fs.Inner.Chtimes(path, aTime, mTime)
|
||||
}
|
||||
|
||||
func (fs *FS) DirectoryExists(path string) bool {
|
||||
fs.SeenFiles.Add(path)
|
||||
return fs.Inner.DirectoryExists(path)
|
||||
}
|
||||
|
||||
func (fs *FS) GetAccessibleEntries(path string) vfs.Entries {
|
||||
fs.SeenFiles.Add(path)
|
||||
return fs.Inner.GetAccessibleEntries(path)
|
||||
}
|
||||
|
||||
func (fs *FS) Stat(path string) vfs.FileInfo {
|
||||
fs.SeenFiles.Add(path)
|
||||
return fs.Inner.Stat(path)
|
||||
}
|
||||
|
||||
func (fs *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
|
||||
fs.SeenFiles.Add(root)
|
||||
return fs.Inner.WalkDir(root, func(path string, d vfs.DirEntry, err error) error {
|
||||
fs.SeenFiles.Add(path)
|
||||
return walkFn(path, d, err)
|
||||
})
|
||||
}
|
||||
|
||||
func (fs *FS) Realpath(path string) string {
|
||||
fs.SeenFiles.Add(path)
|
||||
return fs.Inner.Realpath(path)
|
||||
}
|
||||
88
tools/tsgo/internal/vfs/vfs.go
Normal file
88
tools/tsgo/internal/vfs/vfs.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:generate go tool github.com/matryer/moq -fmt goimports -out vfsmock/mock_generated.go -pkg vfsmock . FS
|
||||
//go:generate npx dprint fmt vfsmock/mock_generated.go
|
||||
|
||||
// FS is a file system abstraction.
|
||||
type FS interface {
|
||||
// UseCaseSensitiveFileNames returns true if the file system is case-sensitive.
|
||||
UseCaseSensitiveFileNames() bool
|
||||
|
||||
// FileExists returns true if the file exists.
|
||||
FileExists(path string) bool
|
||||
|
||||
// ReadFile reads the file specified by path and returns the content.
|
||||
// If the file fails to be read, ok will be false.
|
||||
ReadFile(path string) (contents string, ok bool)
|
||||
|
||||
WriteFile(path string, data string) error
|
||||
|
||||
// AppendFile appends data to the file at path, creating it if it does not exist.
|
||||
AppendFile(path string, data string) error
|
||||
|
||||
// Removes `path` and all its contents. Will return the first error it encounters.
|
||||
Remove(path string) error
|
||||
|
||||
// Chtimes changes the access and modification times of the named
|
||||
Chtimes(path string, aTime time.Time, mTime time.Time) error
|
||||
|
||||
// DirectoryExists returns true if the path is a directory.
|
||||
DirectoryExists(path string) bool
|
||||
|
||||
// GetAccessibleEntries returns the files/directories in the specified directory.
|
||||
// If any entry is a symlink, it will be followed.
|
||||
GetAccessibleEntries(path string) Entries
|
||||
|
||||
Stat(path string) FileInfo
|
||||
|
||||
// WalkDir walks the file tree rooted at root, calling walkFn for each file or directory in the tree.
|
||||
// It is has the same behavior as [fs.WalkDir], but with paths as [string].
|
||||
WalkDir(root string, walkFn WalkDirFunc) error
|
||||
|
||||
// Realpath returns the "real path" of the specified path,
|
||||
// following symlinks and correcting filename casing.
|
||||
Realpath(path string) string
|
||||
}
|
||||
|
||||
type Entries struct {
|
||||
Files []string
|
||||
Directories []string
|
||||
// Symlinks contains the names of entries in Files or Directories that were
|
||||
// originally symbolic links (or reparse points) on disk. The names are the
|
||||
// same as those in Files/Directories (i.e., the link name, not the target).
|
||||
// nil means symlink information is not available and the entries may need
|
||||
// to be re-checked for symlinks.
|
||||
Symlinks map[string]struct{}
|
||||
}
|
||||
|
||||
type (
|
||||
// DirEntry is [fs.DirEntry].
|
||||
DirEntry = fs.DirEntry
|
||||
|
||||
// FileInfo is [fs.FileInfo].
|
||||
FileInfo = fs.FileInfo
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalid = fs.ErrInvalid // "invalid argument"
|
||||
ErrPermission = fs.ErrPermission // "permission denied"
|
||||
ErrExist = fs.ErrExist // "file already exists"
|
||||
ErrNotExist = fs.ErrNotExist // "file does not exist"
|
||||
ErrClosed = fs.ErrClosed // "file already closed"
|
||||
)
|
||||
|
||||
// WalkDirFunc is [fs.WalkDirFunc].
|
||||
type WalkDirFunc = fs.WalkDirFunc
|
||||
|
||||
var (
|
||||
// SkipAll is [fs.SkipAll].
|
||||
SkipAll = fs.SkipAll //nolint:errname
|
||||
|
||||
// SkipDir is [fs.SkipDir].
|
||||
SkipDir = fs.SkipDir //nolint:errname
|
||||
)
|
||||
65
tools/tsgo/internal/vfs/vfs_test.go
Normal file
65
tools/tsgo/internal/vfs/vfs_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package vfs_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/repo"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func BenchmarkReadFile(b *testing.B) {
|
||||
type bench struct {
|
||||
name string
|
||||
fs vfs.FS
|
||||
path string
|
||||
}
|
||||
|
||||
osFS := osvfs.FS()
|
||||
|
||||
const smallData = "hello, world"
|
||||
tmpdir := tspath.NormalizeSlashes(b.TempDir())
|
||||
osSmallDataPath := tspath.CombinePaths(tmpdir, "foo.ts")
|
||||
err := osFS.WriteFile(osSmallDataPath, smallData)
|
||||
assert.NilError(b, err)
|
||||
|
||||
tests := []bench{
|
||||
{"MapFS small", vfstest.FromMap(fstest.MapFS{
|
||||
"/foo.ts": &fstest.MapFile{
|
||||
Data: []byte(smallData),
|
||||
},
|
||||
}, true), "/foo.ts"},
|
||||
{"OS small", osFS, osSmallDataPath},
|
||||
}
|
||||
|
||||
if repo.TypeScriptSubmoduleExists() {
|
||||
checkerPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath()), "src", "compiler", "checker.ts")
|
||||
|
||||
checkerContents, ok := osFS.ReadFile(checkerPath)
|
||||
assert.Assert(b, ok)
|
||||
|
||||
tests = append(tests, bench{
|
||||
"MapFS checker.ts",
|
||||
vfstest.FromMap(fstest.MapFS{
|
||||
"/checker.ts": &fstest.MapFile{
|
||||
Data: []byte(checkerContents),
|
||||
},
|
||||
}, true),
|
||||
"/checker.ts",
|
||||
})
|
||||
tests = append(tests, bench{"OS checker.ts", osFS, checkerPath})
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for range b.N {
|
||||
_, _ = tt.fs.ReadFile(tt.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
425
tools/tsgo/internal/vfs/vfsmatch/MATCHING_ALGORITHM.md
Normal file
425
tools/tsgo/internal/vfs/vfsmatch/MATCHING_ALGORITHM.md
Normal file
@@ -0,0 +1,425 @@
|
||||
# Glob Matching Algorithm Specification
|
||||
|
||||
This document is a formal algorithmic specification of the file-path glob matching
|
||||
logic. An implementation conforming to this specification must produce identical
|
||||
results for all inputs. All subroutine errors are propagated as errors of the
|
||||
calling routine unless stated otherwise.
|
||||
|
||||
---
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
**Path** — A normalized, `/`-separated absolute file path (e.g., `/project/src/index.ts`).
|
||||
|
||||
**Path component** — A single segment between `/` delimiters (e.g., `src`, `index.ts`). The leading `/` produces the root component, which is the empty string `""`.
|
||||
|
||||
**Spec** — A user-provided glob string (e.g., `src/**/*.ts`), before compilation.
|
||||
|
||||
**Base path** — The absolute directory against which relative specs are resolved.
|
||||
|
||||
**Usage mode** — One of three modes that alter matching semantics:
|
||||
- **Files** — Matches complete file paths.
|
||||
- **Directories** — Matches directory prefixes for traversal pruning.
|
||||
- **Exclude** — Matches paths to be excluded.
|
||||
|
||||
**Component kind** — One of:
|
||||
- **Literal** — Contains no `*` or `?` characters.
|
||||
- **Wildcard** — Contains at least one `*` or `?` character.
|
||||
- **DoubleAsterisk** — The exact string `**`.
|
||||
|
||||
**Character** — A single Unicode scalar value (codepoint). See Section 9 for the precise character-boundary requirements that apply during segment matching.
|
||||
|
||||
**Segment kind** — One of:
|
||||
- **SegLiteral** — An exact literal substring.
|
||||
- **SegStar** — Matches zero or more characters excluding `/`.
|
||||
- **SegQuestion** — Matches exactly one character excluding `/`.
|
||||
|
||||
**Pattern** — A compiled spec consisting of a component list, a usage mode, and a case-sensitivity flag.
|
||||
|
||||
---
|
||||
|
||||
## 2. Helper Predicates
|
||||
|
||||
**IS_HIDDEN_PATH**(_component_)
|
||||
|
||||
> 1. If the length of _component_ is 0, return **false**.
|
||||
> 2. If the first character of _component_ is `"."`, return **true**.
|
||||
> 3. Return **false**.
|
||||
|
||||
**IS_PACKAGE_FOLDER**(_component_)
|
||||
|
||||
> 1. If _component_ equals `"node_modules"` (case-insensitive), return **true**.
|
||||
> 2. If _component_ equals `"bower_components"` (case-insensitive), return **true**.
|
||||
> 3. If _component_ equals `"jspm_packages"` (case-insensitive), return **true**.
|
||||
> 4. Return **false**.
|
||||
|
||||
**ENSURE_TRAILING_SLASH**(_s_)
|
||||
|
||||
> 1. If the length of _s_ is 0, return _s_.
|
||||
> 2. If the last character of _s_ is `"/"`, return _s_.
|
||||
> 3. Return _s_ concatenated with `"/"`.
|
||||
|
||||
**STRINGS_EQUAL**(_a_, _b_, _caseSensitive_)
|
||||
|
||||
> 1. If _caseSensitive_ is **true**, return whether _a_ and _b_ are byte-for-byte identical.
|
||||
> 2. Return whether _a_ and _b_ are equal under Unicode case folding.
|
||||
|
||||
**IS_IMPLICIT_GLOB**(_component_)
|
||||
|
||||
> 1. If _component_ contains any of the characters `"."`, `"*"`, or `"?"`, return **false**.
|
||||
> 2. Return **true**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Spec Normalization
|
||||
|
||||
**NORMALIZE_SPEC**(_spec_, _basePath_)
|
||||
|
||||
> 1. Let _components_ be the result of resolving _spec_ against _basePath_ into an ordered list of normalized path components. The first element is the absolute root prefix (e.g., `"/home"`). The resolution uses `/` as the path separator, resolves `.` and `..` segments, and collapses consecutive separators.
|
||||
> 2. If the last character of _components_\[0\] is `"/"`, remove it.
|
||||
> 3. Return _components_.
|
||||
|
||||
---
|
||||
|
||||
## 4. Segment Parsing
|
||||
|
||||
**PARSE_SEGMENTS**(_string_)
|
||||
|
||||
> 1. Let _segments_ be an empty list.
|
||||
> 2. Let _start_ be 0.
|
||||
> 3. For each index _i_ from 0 to the length of _string_ − 1:
|
||||
> 1. If _string_\[_i_\] is `"*"` or `"?"`, then:
|
||||
> 1. If _i_ > _start_, append a **SegLiteral** segment with value _string_\[_start_.._i_\] to _segments_.
|
||||
> 2. If _string_\[_i_\] is `"*"`, append a **SegStar** segment to _segments_.
|
||||
> 3. Otherwise, append a **SegQuestion** segment to _segments_.
|
||||
> 4. Set _start_ to _i_ + 1.
|
||||
> 4. If _start_ < length of _string_, append a **SegLiteral** segment with value _string_\[_start_..\] to _segments_.
|
||||
> 5. Return _segments_.
|
||||
|
||||
---
|
||||
|
||||
## 5. Pattern Compilation
|
||||
|
||||
**COMPILE_PATTERN**(_spec_, _basePath_, _usage_, _caseSensitive_)
|
||||
|
||||
> 1. Let _components_ be the result of NORMALIZE_SPEC(_spec_, _basePath_).
|
||||
> 2. If the last element of _components_ is `"**"` and _usage_ is not **Exclude**, return **failure**. (The pattern compiles to nothing.)
|
||||
> 3. If IS_IMPLICIT_GLOB(last element of _components_) is **true** (note: this check is applied to the _normalized_ component, not the raw spec string), then:
|
||||
> 1. Append `"**"` to _components_.
|
||||
> 2. Append `"*"` to _components_.
|
||||
> 4. Let _compiledComponents_ be an empty list.
|
||||
> 5. For each _part_ in _components_:
|
||||
> 1. If _part_ is `"**"`, append a **DoubleAsterisk** component to _compiledComponents_.
|
||||
> 2. Otherwise, if _part_ contains no `"*"` or `"?"` characters, append a **Literal** component with value _part_ to _compiledComponents_.
|
||||
> 3. Otherwise, append a **Wildcard** component with segments PARSE_SEGMENTS(_part_) to _compiledComponents_.
|
||||
> 6. Return a pattern with component list _compiledComponents_, usage mode _usage_, and case-sensitivity flag _caseSensitive_.
|
||||
|
||||
---
|
||||
|
||||
## 6. Path Component Extraction
|
||||
|
||||
**NEXT_PATH_COMPONENT**(_path_, _offset_)
|
||||
|
||||
> 1. If _offset_ ≥ length of _path_, return (**none**, _offset_, **false**).
|
||||
> 2. If _offset_ is 0 and _path_\[0\] is `"/"`, return (`""`, 1, **true**).
|
||||
> 3. While _offset_ < length of _path_ and _path_\[_offset_\] is `"/"`, increment _offset_.
|
||||
> 4. If _offset_ ≥ length of _path_, return (**none**, _offset_, **false**).
|
||||
> 5. Let _start_ be _offset_.
|
||||
> 6. While _offset_ < length of _path_ and _path_\[_offset_\] is not `"/"`, increment _offset_.
|
||||
> 7. Return (_path_\[_start_.._offset_\], _offset_, **true**).
|
||||
|
||||
---
|
||||
|
||||
## 7. Full-Path Matching
|
||||
|
||||
**MATCH_PATH**(_pattern_, _path_)
|
||||
|
||||
> 1. Return the result of MATCH_PATH_INNER(_pattern_, _path_, 0, 0, **false**).
|
||||
|
||||
**MATCH_PATH_PREFIX**(_pattern_, _path_)
|
||||
|
||||
> 1. Return the result of MATCH_PATH_INNER(_pattern_, _path_, 0, 0, **true**).
|
||||
|
||||
**MATCH_PATH_INNER**(_pattern_, _path_, _pathOffset_, _compIdx_, _prefixOnly_)
|
||||
|
||||
> 1. Let _components_ be the component list of _pattern_.
|
||||
> 2. Let _usage_ be the usage mode of _pattern_.
|
||||
> 3. Let _caseSensitive_ be the case-sensitivity flag of _pattern_.
|
||||
> 4. Loop:
|
||||
> 1. Let (_part_, _nextOffset_, _ok_) be the result of NEXT_PATH_COMPONENT(_path_, _pathOffset_).
|
||||
> 2. If _ok_ is **false**, then:
|
||||
> 1. If _prefixOnly_ is **true**, return **true**.
|
||||
> 2. Return the result of PATTERN_SATISFIED(_components_, _compIdx_).
|
||||
> 3. If _compIdx_ ≥ length of _components_, then:
|
||||
> 1. If _usage_ is **Exclude** and _prefixOnly_ is **false**, return **true**.
|
||||
> 2. Return **false**.
|
||||
> 4. Let _comp_ be _components_\[_compIdx_\].
|
||||
> 5. If the kind of _comp_ is **DoubleAsterisk**, then:
|
||||
> 1. Let _skipResult_ be the result of MATCH_PATH_INNER(_pattern_, _path_, _pathOffset_, _compIdx_ + 1, _prefixOnly_).
|
||||
> 2. If _skipResult_ is **true**, return **true**.
|
||||
> 3. If _usage_ is not **Exclude**, then:
|
||||
> 1. If IS_HIDDEN_PATH(_part_) is **true**, return **false**.
|
||||
> 2. If IS_PACKAGE_FOLDER(_part_) is **true**, return **false**.
|
||||
> 4. Set _pathOffset_ to _nextOffset_.
|
||||
> 5. Continue the loop.
|
||||
> 6. If the kind of _comp_ is **Literal**, then:
|
||||
> 1. If STRINGS_EQUAL(_comp_.value, _part_, _caseSensitive_) is **false**, return **false**.
|
||||
> 7. If the kind of _comp_ is **Wildcard**, then:
|
||||
> 1. If _usage_ is not **Exclude** and IS_PACKAGE_FOLDER(_part_) is **true**, return **false**.
|
||||
> 2. If the result of MATCH_WILDCARD(_pattern_, _comp_.segments, _part_) is **false**, return **false**.
|
||||
> 8. Set _pathOffset_ to _nextOffset_.
|
||||
> 9. Increment _compIdx_.
|
||||
|
||||
**PATTERN_SATISFIED**(_components_, _compIdx_)
|
||||
|
||||
> 1. For each index _i_ from _compIdx_ to length of _components_ − 1:
|
||||
> 1. If the kind of _components_\[_i_\] is not **DoubleAsterisk**, return **false**.
|
||||
> 2. Return **true**.
|
||||
|
||||
---
|
||||
|
||||
## 8. Wildcard Component Matching
|
||||
|
||||
**MATCH_WILDCARD**(_pattern_, _segments_, _string_)
|
||||
|
||||
> 1. Let _usage_ be the usage mode of _pattern_.
|
||||
> 2. Let _caseSensitive_ be the case-sensitivity flag of _pattern_.
|
||||
> 3. If _usage_ is not **Exclude**, then:
|
||||
> 1. If the length of _segments_ > 0, then:
|
||||
> 1. Let _firstKind_ be the kind of _segments_\[0\].
|
||||
> 2. If (_firstKind_ is **SegStar** or _firstKind_ is **SegQuestion**) and IS_HIDDEN_PATH(_string_) is **true**, return **false**.
|
||||
> 4. Let _matched_ be the result of MATCH_SEGMENTS(_segments_, _string_, _caseSensitive_).
|
||||
> 5. If _matched_ is **false**, return **false**.
|
||||
> 6. Let _accepted_ be the result of SHOULD_ACCEPT_MIN_JS(_pattern_, _segments_, _string_).
|
||||
> 7. Return _accepted_.
|
||||
|
||||
---
|
||||
|
||||
## 9. Segment Matching
|
||||
|
||||
In this section, all string positions refer to **character** (codepoint) boundaries.
|
||||
Implementations must advance by full codepoints, not by encoding units (e.g., not
|
||||
by individual bytes in UTF-8, nor by individual code units in UTF-16). "Increment
|
||||
_sIdx_" means advance _sIdx_ past the next character (one codepoint). Likewise,
|
||||
"length of _s_" is the number of characters, and _s_\[_sIdx_\] is the character
|
||||
at position _sIdx_.
|
||||
|
||||
The original TypeScript implementation uses ECMAScript regexes without the `u` flag,
|
||||
which operate on UTF-16 code units; a conforming implementation may match on
|
||||
codepoints instead, as the difference is only observable for supplementary-plane
|
||||
characters (U+10000 and above) in filenames.
|
||||
|
||||
**MATCH_SEGMENTS**(_segments_, _s_, _caseSensitive_)
|
||||
|
||||
> 1. Let _segIdx_ be 0.
|
||||
> 2. Let _sIdx_ be 0.
|
||||
> 3. Let _starSegIdx_ be −1.
|
||||
> 4. Let _starSIdx_ be 0.
|
||||
> 5. While _sIdx_ < length of _s_:
|
||||
> 1. If _segIdx_ < length of _segments_, then:
|
||||
> 1. Let _seg_ be _segments_\[_segIdx_\].
|
||||
> 2. If the kind of _seg_ is **SegLiteral**, then:
|
||||
> 1. Let _lit_ be the value of _seg_.
|
||||
> 2. If _sIdx_ + length of _lit_ ≤ length of _s_ and STRINGS_EQUAL(_lit_, _s_\[_sIdx_.._sIdx_+len(_lit_)\], _caseSensitive_) is **true**, then:
|
||||
> 1. Set _sIdx_ to _sIdx_ + length of _lit_.
|
||||
> 2. Increment _segIdx_.
|
||||
> 3. Continue the loop.
|
||||
> 3. If the kind of _seg_ is **SegQuestion**, then:
|
||||
> 1. If _s_\[_sIdx_\] is not `"/"`, then:
|
||||
> 1. Increment _sIdx_.
|
||||
> 2. Increment _segIdx_.
|
||||
> 3. Continue the loop.
|
||||
> 4. If the kind of _seg_ is **SegStar**, then:
|
||||
> 1. Set _starSegIdx_ to _segIdx_.
|
||||
> 2. Set _starSIdx_ to _sIdx_.
|
||||
> 3. Increment _segIdx_.
|
||||
> 4. Continue the loop.
|
||||
> 2. If _starSegIdx_ ≥ 0 and _starSIdx_ < length of _s_ and _s_\[_starSIdx_\] is not `"/"`, then:
|
||||
> 1. Increment _starSIdx_.
|
||||
> 2. Set _sIdx_ to _starSIdx_.
|
||||
> 3. Set _segIdx_ to _starSegIdx_ + 1.
|
||||
> 4. Continue the loop.
|
||||
> 3. Return **false**.
|
||||
> 6. While _segIdx_ < length of _segments_ and the kind of _segments_\[_segIdx_\] is **SegStar**:
|
||||
> 1. Increment _segIdx_.
|
||||
> 7. Return _segIdx_ ≥ length of _segments_.
|
||||
|
||||
---
|
||||
|
||||
## 10. `.min.js` Default Exclusion
|
||||
|
||||
**SHOULD_ACCEPT_MIN_JS**(_pattern_, _segments_, _filename_)
|
||||
|
||||
> 1. Let _usage_ be the usage mode of _pattern_.
|
||||
> 2. If _usage_ is not **Files**, return **true**.
|
||||
> 3. If the result of HAS_MIN_JS_SUFFIX(_filename_, _pattern_.caseSensitive) is **false**, return **true**.
|
||||
> 4. If the result of PATTERN_MENTIONS_MIN_SUFFIX(_segments_, _pattern_.caseSensitive) is **true**, return **true**.
|
||||
> 5. Return **false**.
|
||||
|
||||
**HAS_MIN_JS_SUFFIX**(_filename_, _caseSensitive_)
|
||||
|
||||
> 1. Let _suffix_ be `".min.js"`.
|
||||
> 2. If length of _filename_ < length of _suffix_, return **false**.
|
||||
> 3. Let _tail_ be the last 7 characters of _filename_.
|
||||
> 4. If _caseSensitive_ is **true**, return whether _tail_ equals `".min.js"`.
|
||||
> 5. Return whether _tail_ equals `".min.js"` under Unicode case folding.
|
||||
|
||||
**PATTERN_MENTIONS_MIN_SUFFIX**(_segments_, _caseSensitive_)
|
||||
|
||||
> 1. For each _seg_ in _segments_:
|
||||
> 1. If the kind of _seg_ is not **SegLiteral**, continue.
|
||||
> 2. Let _lit_ be the value of _seg_.
|
||||
> 3. If _caseSensitive_ is **false**, let _lit_ be the lowercase form of _lit_.
|
||||
> 4. If _lit_ contains the substring `".min.js"` or `".min."`, return **true**.
|
||||
> 2. Return **false**.
|
||||
|
||||
---
|
||||
|
||||
## 11. Composite Matchers
|
||||
|
||||
**MATCH_FILE**(_path_, _includePatterns_, _excludePatterns_, _hadIncludes_)
|
||||
|
||||
> 1. For each _pattern_ in _excludePatterns_:
|
||||
> 1. If the result of MATCH_PATH(_pattern_, _path_) is **true**, return (0, **false**).
|
||||
> 2. If length of _includePatterns_ is 0, then:
|
||||
> 1. If _hadIncludes_ is **true**, return (0, **false**).
|
||||
> 2. Return (0, **true**).
|
||||
> 3. For each index _i_ from 0 to length of _includePatterns_ − 1:
|
||||
> 1. If the result of MATCH_PATH(_includePatterns_\[_i_\], _path_) is **true**, return (_i_, **true**).
|
||||
> 4. Return (0, **false**).
|
||||
|
||||
**MATCH_DIRECTORY**(_path_, _includePatterns_, _excludePatterns_, _hadIncludes_)
|
||||
|
||||
> 1. For each _pattern_ in _excludePatterns_:
|
||||
> 1. If the result of MATCH_PATH(_pattern_, _path_) is **true**, return **false**.
|
||||
> 2. If length of _includePatterns_ is 0, then:
|
||||
> 1. If _hadIncludes_ is **true**, return **false**.
|
||||
> 2. Return **true**.
|
||||
> 3. For each _pattern_ in _includePatterns_:
|
||||
> 1. If the result of MATCH_PATH_PREFIX(_pattern_, _path_) is **true**, return **true**.
|
||||
> 4. Return **false**.
|
||||
|
||||
**MATCH_SPEC**(_patterns_, _path_)
|
||||
|
||||
> 1. For each _pattern_ in _patterns_:
|
||||
> 1. If the result of MATCH_PATH(_pattern_, _path_) is **true**, return **true**.
|
||||
> 2. Return **false**.
|
||||
|
||||
**MATCH_SPEC_INDEX**(_patterns_, _path_)
|
||||
|
||||
> 1. For each index _i_ from 0 to length of _patterns_ − 1:
|
||||
> 1. If the result of MATCH_PATH(_patterns_\[_i_\], _path_) is **true**, return _i_.
|
||||
> 2. Return −1.
|
||||
|
||||
---
|
||||
|
||||
## 12. Pattern Set Compilation
|
||||
|
||||
**COMPILE_PATTERNS**(_specs_, _basePath_, _usage_, _caseSensitive_)
|
||||
|
||||
> 1. Let _patterns_ be an empty list.
|
||||
> 2. For each _spec_ in _specs_:
|
||||
> 1. Let _result_ be the result of COMPILE_PATTERN(_spec_, _basePath_, _usage_, _caseSensitive_).
|
||||
> 2. If _result_ is not **failure**, append _result_ to _patterns_.
|
||||
> 3. Return _patterns_.
|
||||
|
||||
**COMPILE_FILE_MATCHER**(_includeSpecs_, _excludeSpecs_, _basePath_, _caseSensitive_)
|
||||
|
||||
> 1. Let _includePatterns_ be the result of COMPILE_PATTERNS(_includeSpecs_, _basePath_, **Files**, _caseSensitive_).
|
||||
> 2. Let _excludePatterns_ be the result of COMPILE_PATTERNS(_excludeSpecs_, _basePath_, **Exclude**, _caseSensitive_).
|
||||
> 3. Let _hadIncludes_ be whether length of _includeSpecs_ > 0.
|
||||
> 4. Return (_includePatterns_, _excludePatterns_, _hadIncludes_).
|
||||
|
||||
**COMPILE_DIRECTORY_MATCHER**(_includeSpecs_, _excludeSpecs_, _basePath_, _caseSensitive_)
|
||||
|
||||
> 1. Let _includePatterns_ be the result of COMPILE_PATTERNS(_includeSpecs_, _basePath_, **Directories**, _caseSensitive_).
|
||||
> 2. Let _excludePatterns_ be the result of COMPILE_PATTERNS(_excludeSpecs_, _basePath_, **Exclude**, _caseSensitive_).
|
||||
> 3. Let _hadIncludes_ be whether length of _includeSpecs_ > 0.
|
||||
> 4. Return (_includePatterns_, _excludePatterns_, _hadIncludes_).
|
||||
|
||||
---
|
||||
|
||||
## 13. Base Path Computation
|
||||
|
||||
**GET_BASE_PATHS**(_rootPath_, _includeSpecs_, _caseSensitive_)
|
||||
|
||||
> 1. Let _basePaths_ be a list containing _rootPath_.
|
||||
> 2. If _includeSpecs_ is empty, return _basePaths_.
|
||||
> 3. Let _includeBasePaths_ be an empty list.
|
||||
> 4. For each _spec_ in _includeSpecs_:
|
||||
> 1. Let _absolute_ be the result of resolving _spec_ to an absolute normalized path against _rootPath_.
|
||||
> 2. Let _basePath_ be GET_INCLUDE_BASE_PATH(_absolute_).
|
||||
> 3. Append _basePath_ to _includeBasePaths_.
|
||||
> 5. Sort _includeBasePaths_ using a string comparator that is case-insensitive if _caseSensitive_ is **false**.
|
||||
> 6. For each _candidate_ in _includeBasePaths_:
|
||||
> 1. If no element of _basePaths_ is a path-prefix of _candidate_ (respecting _caseSensitive_), append _candidate_ to _basePaths_.
|
||||
> 7. Return _basePaths_.
|
||||
|
||||
**GET_INCLUDE_BASE_PATH**(_absoluteSpec_)
|
||||
|
||||
> 1. Let _wildcardOffset_ be the index of the first `"*"` or `"?"` character in _absoluteSpec_.
|
||||
> 2. If _wildcardOffset_ < 0, then:
|
||||
> 1. If _absoluteSpec_ has a file extension (contains `"."`), return the parent directory of _absoluteSpec_.
|
||||
> 2. Return _absoluteSpec_.
|
||||
> 3. Return the substring of _absoluteSpec_ up to and including the last `"/"` before _wildcardOffset_.
|
||||
|
||||
---
|
||||
|
||||
## 14. Directory Traversal
|
||||
|
||||
**READ_DIRECTORY**(_host_, _currentDir_, _path_, _extensions_, _excludeSpecs_, _includeSpecs_, _caseSensitive_, _depth_)
|
||||
|
||||
The _host_ must provide the following operations:
|
||||
- **Realpath**(_path_) — Resolves symlinks and returns the canonical absolute path.
|
||||
- **GetAccessibleEntries**(_path_) — Returns the sorted lists of files and subdirectories in the directory at _path_.
|
||||
|
||||
> 1. Let _path_ be the normalized form of _path_.
|
||||
> 2. Let _currentDir_ be the normalized form of _currentDir_.
|
||||
> 3. Let _absolutePath_ be the concatenation of _currentDir_, `"/"`, and _path_ (normalized).
|
||||
> 4. Let (_fileIncludes_, _fileExcludes_, _fileHadIncludes_) be the result of COMPILE_FILE_MATCHER(_includeSpecs_, _excludeSpecs_, _absolutePath_, _caseSensitive_).
|
||||
> 5. Let (_dirIncludes_, _dirExcludes_, _dirHadIncludes_) be the result of COMPILE_DIRECTORY_MATCHER(_includeSpecs_, _excludeSpecs_, _absolutePath_, _caseSensitive_).
|
||||
> 6. Let _resultBuckets_ be a list of empty lists, with length equal to max(length of _fileIncludes_, 1).
|
||||
> 7. Let _visited_ be an empty set of strings.
|
||||
> 8. Let _basePaths_ be the result of GET_BASE_PATHS(_path_, _includeSpecs_, _caseSensitive_).
|
||||
> 9. For each _basePath_ in _basePaths_:
|
||||
> 1. Let _baseAbsolute_ be the concatenation of _currentDir_, `"/"`, and _basePath_ (normalized).
|
||||
> 2. Perform VISIT(_host_, _basePath_, _baseAbsolute_, _depth_, _extensions_, _fileIncludes_, _fileExcludes_, _fileHadIncludes_, _dirIncludes_, _dirExcludes_, _dirHadIncludes_, _caseSensitive_, _visited_, _resultBuckets_).
|
||||
> 10. Return the concatenation of all lists in _resultBuckets_, in order.
|
||||
|
||||
**VISIT**(_host_, _path_, _absolutePath_, _depth_, _extensions_, _fileIncludes_, _fileExcludes_, _fileHadIncludes_, _dirIncludes_, _dirExcludes_, _dirHadIncludes_, _caseSensitive_, _visited_, _resultBuckets_)
|
||||
|
||||
> 1. Let _realPath_ be the result of _host_.Realpath(_absolutePath_).
|
||||
> 2. Let _canonicalPath_ be the canonical form of _realPath_ under the file system's case-sensitivity rules.
|
||||
> 3. If _visited_ contains _canonicalPath_, return.
|
||||
> 4. Add _canonicalPath_ to _visited_.
|
||||
> 5. Let _entries_ be the result of _host_.GetAccessibleEntries(_absolutePath_).
|
||||
> 6. Let _absPrefix_ be ENSURE_TRAILING_SLASH(_absolutePath_).
|
||||
> 7. Let _pathPrefix_ be ENSURE_TRAILING_SLASH(_path_).
|
||||
> 8. For each _file_ in _entries_.files:
|
||||
> 1. If _extensions_ is non-empty and the file extension of _file_ is not in _extensions_, continue.
|
||||
> 2. Let _absFile_ be _absPrefix_ concatenated with _file_.
|
||||
> 3. Let (_index_, _matched_) be the result of MATCH_FILE(_absFile_, _fileIncludes_, _fileExcludes_, _fileHadIncludes_).
|
||||
> 4. If _matched_ is **true**, append _pathPrefix_ concatenated with _file_ to _resultBuckets_\[_index_\].
|
||||
> 9. If _depth_ is finite (i.e., not the sentinel value representing unlimited depth), then:
|
||||
> 1. Decrement _depth_.
|
||||
> 2. If _depth_ is 0, return.
|
||||
> 10. For each _dir_ in _entries_.directories:
|
||||
> 1. Let _absDir_ be _absPrefix_ concatenated with _dir_.
|
||||
> 2. If the result of MATCH_DIRECTORY(_absDir_, _dirIncludes_, _dirExcludes_, _dirHadIncludes_) is **false**, continue.
|
||||
> 3. Perform VISIT(_host_, _pathPrefix_ concatenated with _dir_, _absDir_, _depth_, _extensions_, _fileIncludes_, _fileExcludes_, _fileHadIncludes_, _dirIncludes_, _dirExcludes_, _dirHadIncludes_, _caseSensitive_, _visited_, _resultBuckets_).
|
||||
|
||||
---
|
||||
|
||||
## 15. Invariants
|
||||
|
||||
The following properties hold for all conforming implementations:
|
||||
|
||||
1. COMPILE_PATTERN returns **failure** for any include or directory spec whose last component is `"**"`.
|
||||
2. When the pattern is exhausted but path components remain, exclude patterns return **true** and all other patterns return **false**.
|
||||
3. MATCH_SEGMENTS is guaranteed **O(n·m)** where _n_ is the string length and _m_ is the segment count.
|
||||
4. The `.min.js` default exclusion applies only under **Files** usage mode and only to wildcard components.
|
||||
5. Symlink cycles are detected through real-path canonicalization in VISIT and cause the directory to be skipped.
|
||||
6. Excludes are always evaluated before includes in MATCH_FILE and MATCH_DIRECTORY.
|
||||
7. For include patterns, wildcard components reject package folders; literal components do not.
|
||||
8. For include patterns, `**` does not descend into hidden paths or package folders.
|
||||
9. For include patterns, a wildcard component whose first segment is **SegStar** or **SegQuestion** does not match hidden path components.
|
||||
233
tools/tsgo/internal/vfs/vfsmatch/bench_test.go
Normal file
233
tools/tsgo/internal/vfs/vfsmatch/bench_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package vfsmatch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
)
|
||||
|
||||
// Benchmark test cases using the same hosts as the unit tests
|
||||
|
||||
func BenchmarkReadDirectory(b *testing.B) {
|
||||
benchCases := []struct {
|
||||
name string
|
||||
host func() vfs.FS
|
||||
path string
|
||||
extensions []string
|
||||
excludes []string
|
||||
includes []string
|
||||
}{
|
||||
{
|
||||
name: "LiteralIncludes",
|
||||
host: caseInsensitiveHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
includes: []string{"a.ts", "b.ts"},
|
||||
},
|
||||
{
|
||||
name: "WildcardIncludes",
|
||||
host: caseInsensitiveHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
includes: []string{"z/*.ts", "x/*.ts"},
|
||||
},
|
||||
{
|
||||
name: "RecursiveWildcard",
|
||||
host: caseInsensitiveHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
includes: []string{"**/a.ts"},
|
||||
},
|
||||
{
|
||||
name: "RecursiveWithExcludes",
|
||||
host: caseInsensitiveHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
excludes: []string{"**/b.ts"},
|
||||
includes: []string{"**/*.ts"},
|
||||
},
|
||||
{
|
||||
name: "ComplexPattern",
|
||||
host: caseInsensitiveHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
excludes: []string{"*.ts", "z/??z.ts", "*/b.ts"},
|
||||
includes: []string{"a.ts", "b.ts", "z/a.ts", "z/abz.ts", "z/aba.ts", "x/b.ts"},
|
||||
},
|
||||
{
|
||||
name: "DottedFolders",
|
||||
host: dottedFoldersHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
includes: []string{"**/.*/*"},
|
||||
},
|
||||
{
|
||||
name: "CommonPackageFolders",
|
||||
host: commonFoldersHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
includes: []string{"**/a.ts"},
|
||||
},
|
||||
{
|
||||
name: "NoIncludes",
|
||||
host: caseInsensitiveHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
},
|
||||
{
|
||||
name: "MultipleRecursive",
|
||||
host: caseInsensitiveHost,
|
||||
path: "/dev",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
includes: []string{"**/x/**/*"},
|
||||
},
|
||||
{
|
||||
name: "LargeFileSystem",
|
||||
host: largeFileSystemHost,
|
||||
path: "/project",
|
||||
extensions: []string{".ts", ".tsx", ".d.ts"},
|
||||
includes: []string{"src/**/*.ts"},
|
||||
excludes: []string{"**/node_modules/**", "**/*.test.ts"},
|
||||
},
|
||||
{
|
||||
name: "LargeAllFiles",
|
||||
host: largeFileSystemHost,
|
||||
path: "/project",
|
||||
extensions: []string{".ts", ".tsx", ".js"},
|
||||
excludes: []string{"**/node_modules/**"},
|
||||
includes: []string{"**/*"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, bc := range benchCases {
|
||||
b.Run(bc.name, func(b *testing.B) {
|
||||
host := cachedvfs.From(bc.host())
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
matchFiles(bc.path, bc.extensions, bc.excludes, bc.includes, host.UseCaseSensitiveFileNames(), "/", UnlimitedDepth, host)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// largeFileSystemHost creates a more realistic file system with many files
|
||||
func largeFileSystemHost() vfs.FS {
|
||||
files := make(map[string]string)
|
||||
|
||||
// Create a realistic project structure
|
||||
dirs := []string{
|
||||
"/project/src",
|
||||
"/project/src/components",
|
||||
"/project/src/utils",
|
||||
"/project/src/services",
|
||||
"/project/src/models",
|
||||
"/project/src/hooks",
|
||||
"/project/test",
|
||||
"/project/node_modules/react",
|
||||
"/project/node_modules/typescript",
|
||||
"/project/node_modules/@types/node",
|
||||
}
|
||||
|
||||
// Add files to each directory
|
||||
for _, dir := range dirs {
|
||||
for j := range 20 {
|
||||
files[dir+"/file"+string(rune('a'+j))+".ts"] = ""
|
||||
files[dir+"/file"+string(rune('a'+j))+".test.ts"] = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Add some dotted directories
|
||||
files["/project/src/.hidden/secret.ts"] = ""
|
||||
files["/project/.config/settings.ts"] = ""
|
||||
|
||||
return vfstest.FromMap(files, false)
|
||||
}
|
||||
|
||||
// BenchmarkPatternCompilation benchmarks the pattern compilation step
|
||||
func BenchmarkPatternCompilation(b *testing.B) {
|
||||
patterns := []struct {
|
||||
name string
|
||||
spec string
|
||||
}{
|
||||
{"Literal", "src/file.ts"},
|
||||
{"SingleWildcard", "src/*.ts"},
|
||||
{"QuestionMark", "src/?.ts"},
|
||||
{"DoubleAsterisk", "**/file.ts"},
|
||||
{"Complex", "src/**/components/*.tsx"},
|
||||
{"DottedPattern", "**/.*/*"},
|
||||
}
|
||||
|
||||
for _, p := range patterns {
|
||||
b.Run(p.name, func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_, _ = compileGlobPattern(p.spec, "/project", UsageFiles, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPatternMatching benchmarks pattern matching against paths
|
||||
func BenchmarkPatternMatching(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
spec string
|
||||
paths []string
|
||||
}{
|
||||
{
|
||||
name: "LiteralMatch",
|
||||
spec: "src/file.ts",
|
||||
paths: []string{
|
||||
"/project/src/file.ts",
|
||||
"/project/src/other.ts",
|
||||
"/project/lib/file.ts",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "WildcardMatch",
|
||||
spec: "src/*.ts",
|
||||
paths: []string{
|
||||
"/project/src/file.ts",
|
||||
"/project/src/component.ts",
|
||||
"/project/src/deep/file.ts",
|
||||
"/project/lib/file.ts",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "RecursiveMatch",
|
||||
spec: "**/file.ts",
|
||||
paths: []string{
|
||||
"/project/file.ts",
|
||||
"/project/src/file.ts",
|
||||
"/project/src/deep/nested/file.ts",
|
||||
"/project/src/other.ts",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ComplexMatch",
|
||||
spec: "src/**/components/*.tsx",
|
||||
paths: []string{
|
||||
"/project/src/components/Button.tsx",
|
||||
"/project/src/features/auth/components/Login.tsx",
|
||||
"/project/src/components/Button.ts",
|
||||
"/project/lib/components/Button.tsx",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
pattern, ok := compileGlobPattern(tc.spec, "/project", UsageFiles, true)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
for _, path := range tc.paths {
|
||||
pattern.matches(path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
26
tools/tsgo/internal/vfs/vfsmatch/stringer_generated.go
Normal file
26
tools/tsgo/internal/vfs/vfsmatch/stringer_generated.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Code generated by "stringer -type=Usage -trimprefix=Usage -output=stringer_generated.go"; DO NOT EDIT.
|
||||
|
||||
package vfsmatch
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[UsageFiles-0]
|
||||
_ = x[UsageDirectories-1]
|
||||
_ = x[UsageExclude-2]
|
||||
}
|
||||
|
||||
const _Usage_name = "FilesDirectoriesExclude"
|
||||
|
||||
var _Usage_index = [...]uint8{0, 5, 16, 23}
|
||||
|
||||
func (i Usage) String() string {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_Usage_index)-1 {
|
||||
return "Usage(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Usage_name[_Usage_index[idx]:_Usage_index[idx+1]]
|
||||
}
|
||||
717
tools/tsgo/internal/vfs/vfsmatch/vfsmatch.go
Normal file
717
tools/tsgo/internal/vfs/vfsmatch/vfsmatch.go
Normal file
@@ -0,0 +1,717 @@
|
||||
package vfsmatch
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
//go:generate go tool golang.org/x/tools/cmd/stringer -type=Usage -trimprefix=Usage -output=stringer_generated.go
|
||||
//go:generate npx dprint fmt stringer_generated.go
|
||||
|
||||
// This file implements the glob matching algorithm specified in MATCHING_ALGORITHM.md.
|
||||
|
||||
type Usage int8
|
||||
|
||||
const (
|
||||
UsageFiles Usage = iota
|
||||
UsageDirectories
|
||||
UsageExclude
|
||||
)
|
||||
|
||||
// UnlimitedDepth can be passed as the depth argument to indicate there is no depth limit.
|
||||
const UnlimitedDepth = math.MaxInt
|
||||
|
||||
func ReadDirectory(host vfs.FS, currentDir string, path string, extensions []string, excludes []string, includes []string, depth int) []string {
|
||||
return matchFiles(path, extensions, excludes, includes, host.UseCaseSensitiveFileNames(), currentDir, depth, host)
|
||||
}
|
||||
|
||||
// IsImplicitGlob checks if a path component is implicitly a glob.
|
||||
// An "includes" path "foo" is implicitly a glob "foo/** /*" (without the space) if its last component has no extension,
|
||||
// and does not contain any glob characters itself.
|
||||
func IsImplicitGlob(lastPathComponent string) bool {
|
||||
return !strings.ContainsAny(lastPathComponent, ".*?")
|
||||
}
|
||||
|
||||
var wildcardCharCodes = []rune{'*', '?'}
|
||||
|
||||
func getIncludeBasePath(absolute string) string {
|
||||
wildcardOffset := strings.IndexAny(absolute, string(wildcardCharCodes))
|
||||
if wildcardOffset < 0 {
|
||||
// No "*" or "?" in the path
|
||||
if !tspath.HasExtension(absolute) {
|
||||
return absolute
|
||||
} else {
|
||||
return tspath.RemoveTrailingDirectorySeparator(tspath.GetDirectoryPath(absolute))
|
||||
}
|
||||
}
|
||||
return absolute[:max(strings.LastIndex(absolute[:wildcardOffset], string(tspath.DirectorySeparator)), 0)]
|
||||
}
|
||||
|
||||
// getBasePaths computes the unique non-wildcard base paths amongst the provided include patterns.
|
||||
func getBasePaths(path string, includes []string, useCaseSensitiveFileNames bool) []string {
|
||||
// Storage for our results in the form of literal paths (e.g. the paths as written by the user).
|
||||
basePaths := []string{path}
|
||||
|
||||
if len(includes) > 0 {
|
||||
comparePathsOptions := tspath.ComparePathsOptions{CurrentDirectory: path, UseCaseSensitiveFileNames: useCaseSensitiveFileNames}
|
||||
stringComparer := comparePathsOptions.GetComparer()
|
||||
|
||||
// Storage for literal base paths amongst the include patterns.
|
||||
includeBasePaths := []string{}
|
||||
for _, include := range includes {
|
||||
// We also need to check the relative paths by converting them to absolute and normalizing
|
||||
// in case they escape the base path (e.g "..\somedirectory")
|
||||
var absolute string
|
||||
if tspath.IsRootedDiskPath(include) {
|
||||
absolute = include
|
||||
} else {
|
||||
absolute = tspath.NormalizePath(tspath.CombinePaths(path, include))
|
||||
}
|
||||
// Append the literal and canonical candidate base paths.
|
||||
includeBasePaths = append(includeBasePaths, getIncludeBasePath(absolute))
|
||||
}
|
||||
|
||||
// Sort the offsets array using either the literal or canonical path representations.
|
||||
slices.SortStableFunc(includeBasePaths, stringComparer)
|
||||
|
||||
// Iterate over each include base path and include unique base paths that are not a
|
||||
// subpath of an existing base path
|
||||
for _, includeBasePath := range includeBasePaths {
|
||||
if core.Every(basePaths, func(basepath string) bool {
|
||||
return !tspath.ContainsPath(basepath, includeBasePath, comparePathsOptions)
|
||||
}) {
|
||||
basePaths = append(basePaths, includeBasePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return basePaths
|
||||
}
|
||||
|
||||
// globPattern is a compiled glob pattern for matching file paths without regex.
|
||||
type globPattern struct {
|
||||
components []component // path segments to match (e.g., ["src", "**", "*.ts"])
|
||||
isExclude bool // exclude patterns have different matching rules
|
||||
caseSensitive bool
|
||||
excludeMinJs bool // for "files" patterns, exclude .min.js by default
|
||||
}
|
||||
|
||||
// component is a single path segment in a glob pattern.
|
||||
// Examples: "src" (literal), "*" (wildcard), "*.ts" (wildcard), "**" (recursive)
|
||||
type component struct {
|
||||
kind componentKind
|
||||
literal string // for kindLiteral: the exact string to match
|
||||
segments []segment // for kindWildcard: parsed wildcard pattern
|
||||
// Include patterns with wildcards skip common package folders (node_modules, etc.)
|
||||
skipPackageFolders bool
|
||||
}
|
||||
|
||||
type componentKind int
|
||||
|
||||
const (
|
||||
kindLiteral componentKind = iota // exact match (e.g., "src")
|
||||
kindWildcard // contains * or ? (e.g., "*.ts")
|
||||
kindDoubleAsterisk // ** matches zero or more directories
|
||||
)
|
||||
|
||||
// segment is a piece of a wildcard component.
|
||||
// Example: "*.ts" becomes [segStar, segLiteral(".ts")]
|
||||
type segment struct {
|
||||
kind segmentKind
|
||||
literal string // only for segLiteral
|
||||
}
|
||||
|
||||
type segmentKind int
|
||||
|
||||
const (
|
||||
segLiteral segmentKind = iota // exact text
|
||||
segStar // * matches any chars except /
|
||||
segQuestion // ? matches single char except /
|
||||
)
|
||||
|
||||
// compileGlobPattern compiles a glob spec (e.g., "src/**/*.ts") into a pattern.
|
||||
// Returns (pattern, false) if the pattern would match nothing.
|
||||
func compileGlobPattern(spec string, basePath string, usage Usage, caseSensitive bool) (globPattern, bool) {
|
||||
parts := tspath.GetNormalizedPathComponents(spec, basePath)
|
||||
|
||||
// "src/**" without a filename matches nothing (for include patterns)
|
||||
if usage != UsageExclude && core.LastOrNil(parts) == "**" {
|
||||
return globPattern{}, false
|
||||
}
|
||||
|
||||
// Normalize root: "/home/" -> "/home"
|
||||
parts[0] = tspath.RemoveTrailingDirectorySeparator(parts[0])
|
||||
|
||||
// Directories implicitly match all files: "src" -> "src/**/*"
|
||||
if IsImplicitGlob(core.LastOrNil(parts)) {
|
||||
parts = append(parts, "**", "*")
|
||||
}
|
||||
|
||||
p := globPattern{
|
||||
isExclude: usage == UsageExclude,
|
||||
caseSensitive: caseSensitive,
|
||||
excludeMinJs: usage == UsageFiles,
|
||||
// Avoid slice growth during compilation.
|
||||
components: make([]component, 0, len(parts)),
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
p.components = append(p.components, parseComponent(part, usage != UsageExclude))
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
// parseComponent converts a path segment string into a component.
|
||||
func parseComponent(s string, isInclude bool) component {
|
||||
if s == "**" {
|
||||
return component{kind: kindDoubleAsterisk}
|
||||
}
|
||||
if !strings.ContainsAny(s, "*?") {
|
||||
return component{kind: kindLiteral, literal: s}
|
||||
}
|
||||
return component{
|
||||
kind: kindWildcard,
|
||||
segments: parseSegments(s),
|
||||
skipPackageFolders: isInclude,
|
||||
}
|
||||
}
|
||||
|
||||
// parseSegments breaks "*.ts" into [segStar, segLiteral(".ts")]
|
||||
func parseSegments(s string) []segment {
|
||||
// Preallocate based on wildcard count: each wildcard contributes 1 segment,
|
||||
// and each wildcard can split literals into at most one extra literal segment.
|
||||
wildcards := 0
|
||||
for i := range len(s) {
|
||||
if s[i] == '*' || s[i] == '?' {
|
||||
wildcards++
|
||||
}
|
||||
}
|
||||
result := make([]segment, 0, 2*wildcards+1)
|
||||
start := 0
|
||||
for i := range len(s) {
|
||||
switch s[i] {
|
||||
case '*', '?':
|
||||
if i > start {
|
||||
result = append(result, segment{kind: segLiteral, literal: s[start:i]})
|
||||
}
|
||||
if s[i] == '*' {
|
||||
result = append(result, segment{kind: segStar})
|
||||
} else {
|
||||
result = append(result, segment{kind: segQuestion})
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(s) {
|
||||
result = append(result, segment{kind: segLiteral, literal: s[start:]})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// matches returns true if path matches this pattern.
|
||||
func (p *globPattern) matches(path string) bool {
|
||||
return p.matchPathParts(path, "", 0, 0, false)
|
||||
}
|
||||
|
||||
// matchesParts returns true if prefix+suffix matches this pattern.
|
||||
// This avoids allocating a combined string for common call sites where prefix ends with '/'.
|
||||
func (p *globPattern) matchesParts(prefix, suffix string) bool {
|
||||
return p.matchPathParts(prefix, suffix, 0, 0, false)
|
||||
}
|
||||
|
||||
// matchesPrefixParts returns true if files under prefix+suffix could match.
|
||||
func (p *globPattern) matchesPrefixParts(prefix, suffix string) bool {
|
||||
return p.matchPathParts(prefix, suffix, 0, 0, true)
|
||||
}
|
||||
|
||||
// matchPathParts is like matchPath, but operates on a virtual path formed by prefix+suffix.
|
||||
// Offsets are in the combined string.
|
||||
func (p *globPattern) matchPathParts(prefix, suffix string, pathOffset, compIdx int, prefixOnly bool) bool {
|
||||
for {
|
||||
pathPart, nextOffset, ok := nextPathPartParts(prefix, suffix, pathOffset)
|
||||
if !ok {
|
||||
if prefixOnly {
|
||||
return true
|
||||
}
|
||||
return p.patternSatisfied(compIdx)
|
||||
}
|
||||
|
||||
if compIdx >= len(p.components) {
|
||||
return p.isExclude && !prefixOnly
|
||||
}
|
||||
|
||||
comp := p.components[compIdx]
|
||||
switch comp.kind {
|
||||
case kindDoubleAsterisk:
|
||||
if p.matchPathParts(prefix, suffix, pathOffset, compIdx+1, prefixOnly) {
|
||||
return true
|
||||
}
|
||||
if !p.isExclude && (isHiddenPath(pathPart) || isPackageFolder(pathPart)) {
|
||||
return false
|
||||
}
|
||||
pathOffset = nextOffset
|
||||
continue
|
||||
case kindLiteral:
|
||||
if comp.skipPackageFolders && isPackageFolder(pathPart) {
|
||||
panic("unreachable: literal components never have skipPackageFolders")
|
||||
}
|
||||
if !p.stringsEqual(comp.literal, pathPart) {
|
||||
return false
|
||||
}
|
||||
case kindWildcard:
|
||||
if comp.skipPackageFolders && isPackageFolder(pathPart) {
|
||||
return false
|
||||
}
|
||||
if !p.matchWildcard(comp.segments, pathPart) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
pathOffset = nextOffset
|
||||
compIdx++
|
||||
}
|
||||
}
|
||||
|
||||
// patternSatisfied checks if remaining pattern components can match empty input.
|
||||
func (p *globPattern) patternSatisfied(compIdx int) bool {
|
||||
// A pattern is satisfied when remaining components can match empty input.
|
||||
// For both include and exclude patterns, only trailing "**" components may match nothing.
|
||||
for _, c := range p.components[compIdx:] {
|
||||
if c.kind != kindDoubleAsterisk {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// nextPathPart extracts the next path component from path starting at offset.
|
||||
func nextPathPartSingle(s string, offset int) (part string, nextOffset int, ok bool) {
|
||||
if offset >= len(s) {
|
||||
return "", offset, false
|
||||
}
|
||||
if offset == 0 && len(s) > 0 && s[0] == '/' {
|
||||
return "", 1, true
|
||||
}
|
||||
for offset < len(s) && s[offset] == '/' {
|
||||
offset++
|
||||
}
|
||||
if offset >= len(s) {
|
||||
return "", offset, false
|
||||
}
|
||||
rest := s[offset:]
|
||||
if idx := strings.IndexByte(rest, '/'); idx >= 0 {
|
||||
return rest[:idx], offset + idx, true
|
||||
}
|
||||
return rest, len(s), true
|
||||
}
|
||||
|
||||
func nextPathPartParts(prefix, suffix string, offset int) (part string, nextOffset int, ok bool) {
|
||||
// Fast paths: keep the hot single-string scan tight.
|
||||
if len(suffix) == 0 {
|
||||
return nextPathPartSingle(prefix, offset)
|
||||
}
|
||||
if len(prefix) == 0 {
|
||||
return nextPathPartSingle(suffix, offset)
|
||||
}
|
||||
|
||||
// For matchFilesNoRegex call sites, prefix is a directory path ending in '/',
|
||||
// and suffix is a single entry name (no '/'). That makes this significantly
|
||||
// simpler than a general-purpose "virtual concatenation" scanner.
|
||||
|
||||
totalLen := len(prefix) + len(suffix)
|
||||
if offset >= totalLen {
|
||||
return "", offset, false
|
||||
}
|
||||
|
||||
// Handle leading slash (root of absolute path)
|
||||
if offset == 0 && prefix[0] == '/' {
|
||||
return "", 1, true
|
||||
}
|
||||
|
||||
// Scan within prefix.
|
||||
if offset < len(prefix) {
|
||||
for offset < len(prefix) && prefix[offset] == '/' {
|
||||
offset++
|
||||
}
|
||||
if offset < len(prefix) {
|
||||
rest := prefix[offset:]
|
||||
idx := strings.IndexByte(rest, '/')
|
||||
// idx is guaranteed >= 0 for the call sites we care about because prefix ends in '/'.
|
||||
return rest[:idx], offset + idx, true
|
||||
}
|
||||
// Fall through into suffix region.
|
||||
}
|
||||
|
||||
// Scan suffix: it's a single component.
|
||||
sOff := offset - len(prefix)
|
||||
if sOff >= len(suffix) {
|
||||
return "", offset, false
|
||||
}
|
||||
return suffix[sOff:], totalLen, true
|
||||
}
|
||||
|
||||
// matchWildcard matches a path component against wildcard segments.
|
||||
func (p *globPattern) matchWildcard(segs []segment, s string) bool {
|
||||
// Include patterns: wildcards at start cannot match hidden files
|
||||
if !p.isExclude && len(segs) > 0 && isHiddenPath(s) && (segs[0].kind == segStar || segs[0].kind == segQuestion) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Fast path: single * followed by literal suffix (e.g., "*.ts")
|
||||
if len(segs) == 2 && segs[0].kind == segStar && segs[1].kind == segLiteral {
|
||||
suffix := segs[1].literal
|
||||
if len(s) < len(suffix) || !p.stringsEqual(suffix, s[len(s)-len(suffix):]) {
|
||||
return false
|
||||
}
|
||||
return p.shouldIncludeMinJs(s, segs)
|
||||
}
|
||||
|
||||
return p.matchSegments(segs, s) && p.shouldIncludeMinJs(s, segs)
|
||||
}
|
||||
|
||||
// matchSegments matches segments against string s using an iterative algorithm.
|
||||
// This avoids exponential backtracking by tracking only the last star position.
|
||||
// The algorithm is O(n*m) where n is the string length and m is pattern length.
|
||||
func (p *globPattern) matchSegments(segs []segment, s string) bool {
|
||||
segIdx, sIdx := 0, 0
|
||||
starSegIdx, starSIdx := -1, 0
|
||||
|
||||
for sIdx < len(s) {
|
||||
if segIdx < len(segs) {
|
||||
seg := segs[segIdx]
|
||||
switch seg.kind {
|
||||
case segLiteral:
|
||||
end := sIdx + len(seg.literal)
|
||||
if end <= len(s) && p.stringsEqual(seg.literal, s[sIdx:end]) {
|
||||
sIdx = end
|
||||
segIdx++
|
||||
continue
|
||||
}
|
||||
case segQuestion:
|
||||
if s[sIdx] != '/' {
|
||||
_, size := utf8.DecodeRuneInString(s[sIdx:])
|
||||
sIdx += size
|
||||
segIdx++
|
||||
continue
|
||||
}
|
||||
case segStar:
|
||||
// Record star position for backtracking, then try matching zero chars.
|
||||
starSegIdx = segIdx
|
||||
starSIdx = sIdx
|
||||
segIdx++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Current segment didn't match. Backtrack to last star if possible.
|
||||
if starSegIdx >= 0 && starSIdx < len(s) && s[starSIdx] != '/' {
|
||||
// Star consumes one more character (rune), retry from segment after star.
|
||||
_, size := utf8.DecodeRuneInString(s[starSIdx:])
|
||||
starSIdx += size
|
||||
sIdx = starSIdx
|
||||
segIdx = starSegIdx + 1
|
||||
continue
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Consume any trailing stars.
|
||||
for segIdx < len(segs) && segs[segIdx].kind == segStar {
|
||||
segIdx++
|
||||
}
|
||||
return segIdx >= len(segs)
|
||||
}
|
||||
|
||||
func (p *globPattern) shouldIncludeMinJs(filename string, segs []segment) bool {
|
||||
if !p.excludeMinJs {
|
||||
return true
|
||||
}
|
||||
|
||||
// Preserve legacy behavior:
|
||||
// - When matching is case-sensitive, only the exact ".min.js" suffix is excluded by default.
|
||||
// - When matching is case-insensitive, any casing variant is excluded by default.
|
||||
if !p.hasMinJsSuffix(filename) {
|
||||
return true
|
||||
}
|
||||
// Allow when the user's pattern explicitly references the .min. suffix.
|
||||
if p.patternMentionsMinSuffix(segs) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *globPattern) hasMinJsSuffix(filename string) bool {
|
||||
if p.caseSensitive {
|
||||
return strings.HasSuffix(filename, ".min.js")
|
||||
}
|
||||
const minJs = ".min.js"
|
||||
if len(filename) < len(minJs) {
|
||||
return false
|
||||
}
|
||||
// Avoid allocating via strings.ToLower; compare suffix case-insensitively.
|
||||
return strings.EqualFold(filename[len(filename)-len(minJs):], minJs)
|
||||
}
|
||||
|
||||
func (p *globPattern) patternMentionsMinSuffix(segs []segment) bool {
|
||||
for _, seg := range segs {
|
||||
if seg.kind != segLiteral {
|
||||
continue
|
||||
}
|
||||
lit := seg.literal
|
||||
if !p.caseSensitive {
|
||||
lit = strings.ToLower(lit)
|
||||
}
|
||||
if strings.Contains(lit, ".min.js") || strings.Contains(lit, ".min.") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// stringsEqual compares strings with appropriate case sensitivity.
|
||||
func (p *globPattern) stringsEqual(a, b string) bool {
|
||||
if p.caseSensitive {
|
||||
return a == b
|
||||
}
|
||||
return strings.EqualFold(a, b)
|
||||
}
|
||||
|
||||
// isHiddenPath checks if a path component is hidden (starts with dot).
|
||||
func isHiddenPath(name string) bool {
|
||||
return len(name) > 0 && name[0] == '.'
|
||||
}
|
||||
|
||||
// isPackageFolder checks if name is a common package folder (node_modules, etc.)
|
||||
func isPackageFolder(name string) bool {
|
||||
switch len(name) {
|
||||
case len("node_modules"):
|
||||
return strings.EqualFold(name, "node_modules")
|
||||
case len("jspm_packages"):
|
||||
return strings.EqualFold(name, "jspm_packages")
|
||||
case len("bower_components"):
|
||||
return strings.EqualFold(name, "bower_components")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ensureTrailingSlash(s string) string {
|
||||
if len(s) > 0 && s[len(s)-1] != '/' {
|
||||
return s + "/"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// globMatcher combines include and exclude patterns for file matching.
|
||||
type globMatcher struct {
|
||||
includes []globPattern
|
||||
excludes []globPattern
|
||||
hadIncludes bool // true if include specs were provided (even if none compiled)
|
||||
}
|
||||
|
||||
func newGlobMatcher(includeSpecs, excludeSpecs []string, basePath string, caseSensitive bool, usage Usage) *globMatcher {
|
||||
m := &globMatcher{
|
||||
hadIncludes: len(includeSpecs) > 0,
|
||||
includes: make([]globPattern, 0, len(includeSpecs)),
|
||||
excludes: make([]globPattern, 0, len(excludeSpecs)),
|
||||
}
|
||||
|
||||
for _, spec := range includeSpecs {
|
||||
if p, ok := compileGlobPattern(spec, basePath, usage, caseSensitive); ok {
|
||||
m.includes = append(m.includes, p)
|
||||
}
|
||||
}
|
||||
for _, spec := range excludeSpecs {
|
||||
if p, ok := compileGlobPattern(spec, basePath, UsageExclude, caseSensitive); ok {
|
||||
m.excludes = append(m.excludes, p)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// matchesFileParts checks if prefix+suffix matches against the glob patterns.
|
||||
// Returns the index of the matching include pattern and true if matched, or (0, false) if not.
|
||||
func (m *globMatcher) matchesFileParts(prefix, suffix string) (int, bool) {
|
||||
for i := range m.excludes {
|
||||
if m.excludes[i].matchesParts(prefix, suffix) {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
if len(m.includes) == 0 {
|
||||
if m.hadIncludes {
|
||||
return 0, false
|
||||
}
|
||||
return 0, true
|
||||
}
|
||||
for i := range m.includes {
|
||||
if m.includes[i].matchesParts(prefix, suffix) {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// matchesDirectoryParts checks if files under the directory prefix+suffix could match any pattern.
|
||||
func (m *globMatcher) matchesDirectoryParts(prefix, suffix string) bool {
|
||||
for i := range m.excludes {
|
||||
if m.excludes[i].matchesParts(prefix, suffix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(m.includes) == 0 {
|
||||
return !m.hadIncludes
|
||||
}
|
||||
for i := range m.includes {
|
||||
if m.includes[i].matchesPrefixParts(prefix, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// globVisitor traverses directories matching files against glob patterns.
|
||||
type globVisitor struct {
|
||||
host vfs.FS
|
||||
fileMatcher *globMatcher
|
||||
directoryMatcher *globMatcher
|
||||
extensions []string
|
||||
useCaseSensitiveFileNames bool
|
||||
visited collections.Set[string]
|
||||
results [][]string
|
||||
}
|
||||
|
||||
// visit walks a directory tree, collecting files that match the glob patterns.
|
||||
// resolvedRealPath, when non-empty, is the already-resolved real path for this
|
||||
// directory (computed incrementally from the parent). When empty, Realpath is
|
||||
// called to resolve symlinks.
|
||||
func (v *globVisitor) visit(path, absolutePath string, depth int, resolvedRealPath string) {
|
||||
// Detect symlink cycles
|
||||
var realPath string
|
||||
if resolvedRealPath != "" {
|
||||
realPath = resolvedRealPath
|
||||
} else {
|
||||
realPath = v.host.Realpath(absolutePath)
|
||||
}
|
||||
canonicalPath := tspath.GetCanonicalFileName(realPath, v.useCaseSensitiveFileNames)
|
||||
if v.visited.Has(canonicalPath) {
|
||||
return
|
||||
}
|
||||
v.visited.Add(canonicalPath)
|
||||
|
||||
entries := v.host.GetAccessibleEntries(absolutePath)
|
||||
|
||||
pathPrefix := ensureTrailingSlash(path)
|
||||
absPrefix := ensureTrailingSlash(absolutePath)
|
||||
|
||||
for _, file := range entries.Files {
|
||||
if len(v.extensions) > 0 && !tspath.FileExtensionIsOneOf(file, v.extensions) {
|
||||
continue
|
||||
}
|
||||
if idx, ok := v.fileMatcher.matchesFileParts(absPrefix, file); ok {
|
||||
v.results[idx] = append(v.results[idx], pathPrefix+file)
|
||||
}
|
||||
}
|
||||
|
||||
if depth != UnlimitedDepth {
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, dir := range entries.Directories {
|
||||
if !v.directoryMatcher.matchesDirectoryParts(absPrefix, dir) {
|
||||
continue
|
||||
}
|
||||
absDir := absPrefix + dir
|
||||
var childRealPath string
|
||||
if entries.Symlinks != nil {
|
||||
if _, isSymlink := entries.Symlinks[dir]; !isSymlink {
|
||||
// Non-symlink directory: compute realpath incrementally.
|
||||
childRealPath = tspath.CombinePaths(realPath, dir)
|
||||
}
|
||||
// else: symlink directory; leave childRealPath empty to force Realpath call.
|
||||
}
|
||||
// If Symlinks is nil, the FS doesn't track symlinks;
|
||||
// leave childRealPath empty to call Realpath (preserving old behavior).
|
||||
v.visit(pathPrefix+dir, absDir, depth, childRealPath)
|
||||
}
|
||||
}
|
||||
|
||||
func matchFiles(path string, extensions, excludes, includes []string, useCaseSensitiveFileNames bool, currentDirectory string, depth int, host vfs.FS) []string {
|
||||
path = tspath.NormalizePath(path)
|
||||
currentDirectory = tspath.NormalizePath(currentDirectory)
|
||||
absolutePath := tspath.CombinePaths(currentDirectory, path)
|
||||
|
||||
fileMatcher := newGlobMatcher(includes, excludes, absolutePath, useCaseSensitiveFileNames, UsageFiles)
|
||||
directoryMatcher := newGlobMatcher(includes, excludes, absolutePath, useCaseSensitiveFileNames, UsageDirectories)
|
||||
|
||||
v := globVisitor{
|
||||
host: host,
|
||||
fileMatcher: fileMatcher,
|
||||
directoryMatcher: directoryMatcher,
|
||||
extensions: extensions,
|
||||
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
|
||||
results: make([][]string, max(len(fileMatcher.includes), 1)),
|
||||
}
|
||||
|
||||
for _, basePath := range getBasePaths(path, includes, useCaseSensitiveFileNames) {
|
||||
v.visit(basePath, tspath.CombinePaths(currentDirectory, basePath), depth, "")
|
||||
}
|
||||
|
||||
// Fast path: a single include bucket (or no includes) doesn't need flattening.
|
||||
if len(v.results) == 1 {
|
||||
return v.results[0]
|
||||
}
|
||||
return core.Flatten(v.results)
|
||||
}
|
||||
|
||||
// SpecMatcher wraps multiple glob patterns for matching paths.
|
||||
type SpecMatcher struct {
|
||||
patterns []globPattern
|
||||
}
|
||||
|
||||
// MatchString returns true if any pattern matches the path.
|
||||
func (m *SpecMatcher) MatchString(path string) bool {
|
||||
for i := range m.patterns {
|
||||
if m.patterns[i].matches(path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MatchIndex returns the index of the first matching pattern, or -1.
|
||||
func (m *SpecMatcher) MatchIndex(path string) int {
|
||||
for i := range m.patterns {
|
||||
if m.patterns[i].matches(path) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// NewSpecMatcher creates a matcher for one or more glob specs.
|
||||
// It returns a matcher that can test if paths match any of the patterns.
|
||||
func NewSpecMatcher(specs []string, basePath string, usage Usage, useCaseSensitiveFileNames bool) *SpecMatcher {
|
||||
if len(specs) == 0 {
|
||||
return nil
|
||||
}
|
||||
patterns := make([]globPattern, 0, len(specs))
|
||||
for _, spec := range specs {
|
||||
if p, ok := compileGlobPattern(spec, basePath, usage, useCaseSensitiveFileNames); ok {
|
||||
patterns = append(patterns, p)
|
||||
}
|
||||
}
|
||||
if len(patterns) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &SpecMatcher{patterns: patterns}
|
||||
}
|
||||
1938
tools/tsgo/internal/vfs/vfsmatch/vfsmatch_test.go
Normal file
1938
tools/tsgo/internal/vfs/vfsmatch/vfsmatch_test.go
Normal file
File diff suppressed because it is too large
Load Diff
580
tools/tsgo/internal/vfs/vfsmock/mock_generated.go
Normal file
580
tools/tsgo/internal/vfs/vfsmock/mock_generated.go
Normal file
@@ -0,0 +1,580 @@
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package vfsmock
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
// Ensure, that FSMock does implement vfs.FS.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ vfs.FS = &FSMock{}
|
||||
|
||||
// FSMock is a mock implementation of vfs.FS.
|
||||
//
|
||||
// func TestSomethingThatUsesFS(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked vfs.FS
|
||||
// mockedFS := &FSMock{
|
||||
// AppendFileFunc: func(path string, data string) error {
|
||||
// panic("mock out the AppendFile method")
|
||||
// },
|
||||
// ChtimesFunc: func(path string, aTime time.Time, mTime time.Time) error {
|
||||
// panic("mock out the Chtimes method")
|
||||
// },
|
||||
// DirectoryExistsFunc: func(path string) bool {
|
||||
// panic("mock out the DirectoryExists method")
|
||||
// },
|
||||
// FileExistsFunc: func(path string) bool {
|
||||
// panic("mock out the FileExists method")
|
||||
// },
|
||||
// GetAccessibleEntriesFunc: func(path string) vfs.Entries {
|
||||
// panic("mock out the GetAccessibleEntries method")
|
||||
// },
|
||||
// ReadFileFunc: func(path string) (string, bool) {
|
||||
// panic("mock out the ReadFile method")
|
||||
// },
|
||||
// RealpathFunc: func(path string) string {
|
||||
// panic("mock out the Realpath method")
|
||||
// },
|
||||
// RemoveFunc: func(path string) error {
|
||||
// panic("mock out the Remove method")
|
||||
// },
|
||||
// StatFunc: func(path string) vfs.FileInfo {
|
||||
// panic("mock out the Stat method")
|
||||
// },
|
||||
// UseCaseSensitiveFileNamesFunc: func() bool {
|
||||
// panic("mock out the UseCaseSensitiveFileNames method")
|
||||
// },
|
||||
// WalkDirFunc: func(root string, walkFn vfs.WalkDirFunc) error {
|
||||
// panic("mock out the WalkDir method")
|
||||
// },
|
||||
// WriteFileFunc: func(path string, data string) error {
|
||||
// panic("mock out the WriteFile method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedFS in code that requires vfs.FS
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type FSMock struct {
|
||||
// AppendFileFunc mocks the AppendFile method.
|
||||
AppendFileFunc func(path string, data string) error
|
||||
|
||||
// ChtimesFunc mocks the Chtimes method.
|
||||
ChtimesFunc func(path string, aTime time.Time, mTime time.Time) error
|
||||
|
||||
// DirectoryExistsFunc mocks the DirectoryExists method.
|
||||
DirectoryExistsFunc func(path string) bool
|
||||
|
||||
// FileExistsFunc mocks the FileExists method.
|
||||
FileExistsFunc func(path string) bool
|
||||
|
||||
// GetAccessibleEntriesFunc mocks the GetAccessibleEntries method.
|
||||
GetAccessibleEntriesFunc func(path string) vfs.Entries
|
||||
|
||||
// ReadFileFunc mocks the ReadFile method.
|
||||
ReadFileFunc func(path string) (string, bool)
|
||||
|
||||
// RealpathFunc mocks the Realpath method.
|
||||
RealpathFunc func(path string) string
|
||||
|
||||
// RemoveFunc mocks the Remove method.
|
||||
RemoveFunc func(path string) error
|
||||
|
||||
// StatFunc mocks the Stat method.
|
||||
StatFunc func(path string) vfs.FileInfo
|
||||
|
||||
// UseCaseSensitiveFileNamesFunc mocks the UseCaseSensitiveFileNames method.
|
||||
UseCaseSensitiveFileNamesFunc func() bool
|
||||
|
||||
// WalkDirFunc mocks the WalkDir method.
|
||||
WalkDirFunc func(root string, walkFn vfs.WalkDirFunc) error
|
||||
|
||||
// WriteFileFunc mocks the WriteFile method.
|
||||
WriteFileFunc func(path string, data string) error
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// AppendFile holds details about calls to the AppendFile method.
|
||||
AppendFile []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
// Data is the data argument value.
|
||||
Data string
|
||||
}
|
||||
// Chtimes holds details about calls to the Chtimes method.
|
||||
Chtimes []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
// ATime is the aTime argument value.
|
||||
ATime time.Time
|
||||
// MTime is the mTime argument value.
|
||||
MTime time.Time
|
||||
}
|
||||
// DirectoryExists holds details about calls to the DirectoryExists method.
|
||||
DirectoryExists []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
}
|
||||
// FileExists holds details about calls to the FileExists method.
|
||||
FileExists []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
}
|
||||
// GetAccessibleEntries holds details about calls to the GetAccessibleEntries method.
|
||||
GetAccessibleEntries []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
}
|
||||
// ReadFile holds details about calls to the ReadFile method.
|
||||
ReadFile []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
}
|
||||
// Realpath holds details about calls to the Realpath method.
|
||||
Realpath []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
}
|
||||
// Remove holds details about calls to the Remove method.
|
||||
Remove []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
}
|
||||
// Stat holds details about calls to the Stat method.
|
||||
Stat []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
}
|
||||
// UseCaseSensitiveFileNames holds details about calls to the UseCaseSensitiveFileNames method.
|
||||
UseCaseSensitiveFileNames []struct{}
|
||||
// WalkDir holds details about calls to the WalkDir method.
|
||||
WalkDir []struct {
|
||||
// Root is the root argument value.
|
||||
Root string
|
||||
// WalkFn is the walkFn argument value.
|
||||
WalkFn vfs.WalkDirFunc
|
||||
}
|
||||
// WriteFile holds details about calls to the WriteFile method.
|
||||
WriteFile []struct {
|
||||
// Path is the path argument value.
|
||||
Path string
|
||||
// Data is the data argument value.
|
||||
Data string
|
||||
}
|
||||
}
|
||||
lockAppendFile sync.RWMutex
|
||||
lockChtimes sync.RWMutex
|
||||
lockDirectoryExists sync.RWMutex
|
||||
lockFileExists sync.RWMutex
|
||||
lockGetAccessibleEntries sync.RWMutex
|
||||
lockReadFile sync.RWMutex
|
||||
lockRealpath sync.RWMutex
|
||||
lockRemove sync.RWMutex
|
||||
lockStat sync.RWMutex
|
||||
lockUseCaseSensitiveFileNames sync.RWMutex
|
||||
lockWalkDir sync.RWMutex
|
||||
lockWriteFile sync.RWMutex
|
||||
}
|
||||
|
||||
// AppendFile calls AppendFileFunc.
|
||||
func (mock *FSMock) AppendFile(path string, data string) error {
|
||||
if mock.AppendFileFunc == nil {
|
||||
panic("FSMock.AppendFileFunc: method is nil but FS.AppendFile was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
Data string
|
||||
}{
|
||||
Path: path,
|
||||
Data: data,
|
||||
}
|
||||
mock.lockAppendFile.Lock()
|
||||
mock.calls.AppendFile = append(mock.calls.AppendFile, callInfo)
|
||||
mock.lockAppendFile.Unlock()
|
||||
return mock.AppendFileFunc(path, data)
|
||||
}
|
||||
|
||||
// AppendFileCalls gets all the calls that were made to AppendFile.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.AppendFileCalls())
|
||||
func (mock *FSMock) AppendFileCalls() []struct {
|
||||
Path string
|
||||
Data string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
Data string
|
||||
}
|
||||
mock.lockAppendFile.RLock()
|
||||
calls = mock.calls.AppendFile
|
||||
mock.lockAppendFile.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Chtimes calls ChtimesFunc.
|
||||
func (mock *FSMock) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
if mock.ChtimesFunc == nil {
|
||||
panic("FSMock.ChtimesFunc: method is nil but FS.Chtimes was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
ATime time.Time
|
||||
MTime time.Time
|
||||
}{
|
||||
Path: path,
|
||||
ATime: aTime,
|
||||
MTime: mTime,
|
||||
}
|
||||
mock.lockChtimes.Lock()
|
||||
mock.calls.Chtimes = append(mock.calls.Chtimes, callInfo)
|
||||
mock.lockChtimes.Unlock()
|
||||
return mock.ChtimesFunc(path, aTime, mTime)
|
||||
}
|
||||
|
||||
// ChtimesCalls gets all the calls that were made to Chtimes.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.ChtimesCalls())
|
||||
func (mock *FSMock) ChtimesCalls() []struct {
|
||||
Path string
|
||||
ATime time.Time
|
||||
MTime time.Time
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
ATime time.Time
|
||||
MTime time.Time
|
||||
}
|
||||
mock.lockChtimes.RLock()
|
||||
calls = mock.calls.Chtimes
|
||||
mock.lockChtimes.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// DirectoryExists calls DirectoryExistsFunc.
|
||||
func (mock *FSMock) DirectoryExists(path string) bool {
|
||||
if mock.DirectoryExistsFunc == nil {
|
||||
panic("FSMock.DirectoryExistsFunc: method is nil but FS.DirectoryExists was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
}{
|
||||
Path: path,
|
||||
}
|
||||
mock.lockDirectoryExists.Lock()
|
||||
mock.calls.DirectoryExists = append(mock.calls.DirectoryExists, callInfo)
|
||||
mock.lockDirectoryExists.Unlock()
|
||||
return mock.DirectoryExistsFunc(path)
|
||||
}
|
||||
|
||||
// DirectoryExistsCalls gets all the calls that were made to DirectoryExists.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.DirectoryExistsCalls())
|
||||
func (mock *FSMock) DirectoryExistsCalls() []struct {
|
||||
Path string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
}
|
||||
mock.lockDirectoryExists.RLock()
|
||||
calls = mock.calls.DirectoryExists
|
||||
mock.lockDirectoryExists.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// FileExists calls FileExistsFunc.
|
||||
func (mock *FSMock) FileExists(path string) bool {
|
||||
if mock.FileExistsFunc == nil {
|
||||
panic("FSMock.FileExistsFunc: method is nil but FS.FileExists was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
}{
|
||||
Path: path,
|
||||
}
|
||||
mock.lockFileExists.Lock()
|
||||
mock.calls.FileExists = append(mock.calls.FileExists, callInfo)
|
||||
mock.lockFileExists.Unlock()
|
||||
return mock.FileExistsFunc(path)
|
||||
}
|
||||
|
||||
// FileExistsCalls gets all the calls that were made to FileExists.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.FileExistsCalls())
|
||||
func (mock *FSMock) FileExistsCalls() []struct {
|
||||
Path string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
}
|
||||
mock.lockFileExists.RLock()
|
||||
calls = mock.calls.FileExists
|
||||
mock.lockFileExists.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// GetAccessibleEntries calls GetAccessibleEntriesFunc.
|
||||
func (mock *FSMock) GetAccessibleEntries(path string) vfs.Entries {
|
||||
if mock.GetAccessibleEntriesFunc == nil {
|
||||
panic("FSMock.GetAccessibleEntriesFunc: method is nil but FS.GetAccessibleEntries was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
}{
|
||||
Path: path,
|
||||
}
|
||||
mock.lockGetAccessibleEntries.Lock()
|
||||
mock.calls.GetAccessibleEntries = append(mock.calls.GetAccessibleEntries, callInfo)
|
||||
mock.lockGetAccessibleEntries.Unlock()
|
||||
return mock.GetAccessibleEntriesFunc(path)
|
||||
}
|
||||
|
||||
// GetAccessibleEntriesCalls gets all the calls that were made to GetAccessibleEntries.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.GetAccessibleEntriesCalls())
|
||||
func (mock *FSMock) GetAccessibleEntriesCalls() []struct {
|
||||
Path string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
}
|
||||
mock.lockGetAccessibleEntries.RLock()
|
||||
calls = mock.calls.GetAccessibleEntries
|
||||
mock.lockGetAccessibleEntries.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// ReadFile calls ReadFileFunc.
|
||||
func (mock *FSMock) ReadFile(path string) (string, bool) {
|
||||
if mock.ReadFileFunc == nil {
|
||||
panic("FSMock.ReadFileFunc: method is nil but FS.ReadFile was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
}{
|
||||
Path: path,
|
||||
}
|
||||
mock.lockReadFile.Lock()
|
||||
mock.calls.ReadFile = append(mock.calls.ReadFile, callInfo)
|
||||
mock.lockReadFile.Unlock()
|
||||
return mock.ReadFileFunc(path)
|
||||
}
|
||||
|
||||
// ReadFileCalls gets all the calls that were made to ReadFile.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.ReadFileCalls())
|
||||
func (mock *FSMock) ReadFileCalls() []struct {
|
||||
Path string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
}
|
||||
mock.lockReadFile.RLock()
|
||||
calls = mock.calls.ReadFile
|
||||
mock.lockReadFile.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Realpath calls RealpathFunc.
|
||||
func (mock *FSMock) Realpath(path string) string {
|
||||
if mock.RealpathFunc == nil {
|
||||
panic("FSMock.RealpathFunc: method is nil but FS.Realpath was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
}{
|
||||
Path: path,
|
||||
}
|
||||
mock.lockRealpath.Lock()
|
||||
mock.calls.Realpath = append(mock.calls.Realpath, callInfo)
|
||||
mock.lockRealpath.Unlock()
|
||||
return mock.RealpathFunc(path)
|
||||
}
|
||||
|
||||
// RealpathCalls gets all the calls that were made to Realpath.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.RealpathCalls())
|
||||
func (mock *FSMock) RealpathCalls() []struct {
|
||||
Path string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
}
|
||||
mock.lockRealpath.RLock()
|
||||
calls = mock.calls.Realpath
|
||||
mock.lockRealpath.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Remove calls RemoveFunc.
|
||||
func (mock *FSMock) Remove(path string) error {
|
||||
if mock.RemoveFunc == nil {
|
||||
panic("FSMock.RemoveFunc: method is nil but FS.Remove was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
}{
|
||||
Path: path,
|
||||
}
|
||||
mock.lockRemove.Lock()
|
||||
mock.calls.Remove = append(mock.calls.Remove, callInfo)
|
||||
mock.lockRemove.Unlock()
|
||||
return mock.RemoveFunc(path)
|
||||
}
|
||||
|
||||
// RemoveCalls gets all the calls that were made to Remove.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.RemoveCalls())
|
||||
func (mock *FSMock) RemoveCalls() []struct {
|
||||
Path string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
}
|
||||
mock.lockRemove.RLock()
|
||||
calls = mock.calls.Remove
|
||||
mock.lockRemove.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Stat calls StatFunc.
|
||||
func (mock *FSMock) Stat(path string) vfs.FileInfo {
|
||||
if mock.StatFunc == nil {
|
||||
panic("FSMock.StatFunc: method is nil but FS.Stat was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
}{
|
||||
Path: path,
|
||||
}
|
||||
mock.lockStat.Lock()
|
||||
mock.calls.Stat = append(mock.calls.Stat, callInfo)
|
||||
mock.lockStat.Unlock()
|
||||
return mock.StatFunc(path)
|
||||
}
|
||||
|
||||
// StatCalls gets all the calls that were made to Stat.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.StatCalls())
|
||||
func (mock *FSMock) StatCalls() []struct {
|
||||
Path string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
}
|
||||
mock.lockStat.RLock()
|
||||
calls = mock.calls.Stat
|
||||
mock.lockStat.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// UseCaseSensitiveFileNames calls UseCaseSensitiveFileNamesFunc.
|
||||
func (mock *FSMock) UseCaseSensitiveFileNames() bool {
|
||||
if mock.UseCaseSensitiveFileNamesFunc == nil {
|
||||
panic("FSMock.UseCaseSensitiveFileNamesFunc: method is nil but FS.UseCaseSensitiveFileNames was just called")
|
||||
}
|
||||
callInfo := struct{}{}
|
||||
mock.lockUseCaseSensitiveFileNames.Lock()
|
||||
mock.calls.UseCaseSensitiveFileNames = append(mock.calls.UseCaseSensitiveFileNames, callInfo)
|
||||
mock.lockUseCaseSensitiveFileNames.Unlock()
|
||||
return mock.UseCaseSensitiveFileNamesFunc()
|
||||
}
|
||||
|
||||
// UseCaseSensitiveFileNamesCalls gets all the calls that were made to UseCaseSensitiveFileNames.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.UseCaseSensitiveFileNamesCalls())
|
||||
func (mock *FSMock) UseCaseSensitiveFileNamesCalls() []struct{} {
|
||||
var calls []struct{}
|
||||
mock.lockUseCaseSensitiveFileNames.RLock()
|
||||
calls = mock.calls.UseCaseSensitiveFileNames
|
||||
mock.lockUseCaseSensitiveFileNames.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// WalkDir calls WalkDirFunc.
|
||||
func (mock *FSMock) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
|
||||
if mock.WalkDirFunc == nil {
|
||||
panic("FSMock.WalkDirFunc: method is nil but FS.WalkDir was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Root string
|
||||
WalkFn vfs.WalkDirFunc
|
||||
}{
|
||||
Root: root,
|
||||
WalkFn: walkFn,
|
||||
}
|
||||
mock.lockWalkDir.Lock()
|
||||
mock.calls.WalkDir = append(mock.calls.WalkDir, callInfo)
|
||||
mock.lockWalkDir.Unlock()
|
||||
return mock.WalkDirFunc(root, walkFn)
|
||||
}
|
||||
|
||||
// WalkDirCalls gets all the calls that were made to WalkDir.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.WalkDirCalls())
|
||||
func (mock *FSMock) WalkDirCalls() []struct {
|
||||
Root string
|
||||
WalkFn vfs.WalkDirFunc
|
||||
} {
|
||||
var calls []struct {
|
||||
Root string
|
||||
WalkFn vfs.WalkDirFunc
|
||||
}
|
||||
mock.lockWalkDir.RLock()
|
||||
calls = mock.calls.WalkDir
|
||||
mock.lockWalkDir.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// WriteFile calls WriteFileFunc.
|
||||
func (mock *FSMock) WriteFile(path string, data string) error {
|
||||
if mock.WriteFileFunc == nil {
|
||||
panic("FSMock.WriteFileFunc: method is nil but FS.WriteFile was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Path string
|
||||
Data string
|
||||
}{
|
||||
Path: path,
|
||||
Data: data,
|
||||
}
|
||||
mock.lockWriteFile.Lock()
|
||||
mock.calls.WriteFile = append(mock.calls.WriteFile, callInfo)
|
||||
mock.lockWriteFile.Unlock()
|
||||
return mock.WriteFileFunc(path, data)
|
||||
}
|
||||
|
||||
// WriteFileCalls gets all the calls that were made to WriteFile.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedFS.WriteFileCalls())
|
||||
func (mock *FSMock) WriteFileCalls() []struct {
|
||||
Path string
|
||||
Data string
|
||||
} {
|
||||
var calls []struct {
|
||||
Path string
|
||||
Data string
|
||||
}
|
||||
mock.lockWriteFile.RLock()
|
||||
calls = mock.calls.WriteFile
|
||||
mock.lockWriteFile.RUnlock()
|
||||
return calls
|
||||
}
|
||||
21
tools/tsgo/internal/vfs/vfsmock/wrapper.go
Normal file
21
tools/tsgo/internal/vfs/vfsmock/wrapper.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package vfsmock
|
||||
|
||||
import "github.com/microsoft/typescript-go/internal/vfs"
|
||||
|
||||
// Wrap wraps a vfs.FS and returns a FSMock which calls it.
|
||||
func Wrap(fs vfs.FS) *FSMock {
|
||||
return &FSMock{
|
||||
DirectoryExistsFunc: fs.DirectoryExists,
|
||||
FileExistsFunc: fs.FileExists,
|
||||
GetAccessibleEntriesFunc: fs.GetAccessibleEntries,
|
||||
ReadFileFunc: fs.ReadFile,
|
||||
RealpathFunc: fs.Realpath,
|
||||
RemoveFunc: fs.Remove,
|
||||
ChtimesFunc: fs.Chtimes,
|
||||
StatFunc: fs.Stat,
|
||||
UseCaseSensitiveFileNamesFunc: fs.UseCaseSensitiveFileNames,
|
||||
WalkDirFunc: fs.WalkDir,
|
||||
WriteFileFunc: fs.WriteFile,
|
||||
AppendFileFunc: fs.AppendFile,
|
||||
}
|
||||
}
|
||||
26
tools/tsgo/internal/vfs/vfsmock/wrapper_test.go
Normal file
26
tools/tsgo/internal/vfs/vfsmock/wrapper_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package vfsmock
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wrapper := Wrap(vfstest.FromMap(map[string]string{}, true))
|
||||
|
||||
wrapperValue := reflect.ValueOf(wrapper).Elem()
|
||||
wrapperType := wrapperValue.Type()
|
||||
|
||||
for i := range wrapperType.NumField() {
|
||||
field := wrapperType.Field(i)
|
||||
if field.IsExported() {
|
||||
fieldValue := wrapperValue.Field(i)
|
||||
assert.Assert(t, !fieldValue.IsZero(), "field %s should not be zero; update Wrap", field.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
679
tools/tsgo/internal/vfs/vfstest/vfstest.go
Normal file
679
tools/tsgo/internal/vfs/vfstest/vfstest.go
Normal file
@@ -0,0 +1,679 @@
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"maps"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"github.com/microsoft/typescript-go/internal/vfs/iovfs"
|
||||
)
|
||||
|
||||
type MapFS struct {
|
||||
// mu protects m.
|
||||
// A single mutex is sufficient as we only use fstest.Map's Open method.
|
||||
mu sync.RWMutex
|
||||
|
||||
// keys in m are canonicalPaths
|
||||
m fstest.MapFS
|
||||
|
||||
useCaseSensitiveFileNames bool
|
||||
|
||||
symlinks map[canonicalPath]canonicalPath
|
||||
|
||||
clock Clock
|
||||
}
|
||||
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
SinceStart() time.Duration
|
||||
}
|
||||
|
||||
type clockImpl struct {
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func (c *clockImpl) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (c *clockImpl) SinceStart() time.Duration {
|
||||
return time.Since(c.start)
|
||||
}
|
||||
|
||||
var (
|
||||
_ iovfs.RealpathFS = (*MapFS)(nil)
|
||||
_ iovfs.WritableFS = (*MapFS)(nil)
|
||||
)
|
||||
|
||||
type sys struct {
|
||||
original any
|
||||
realpath string
|
||||
}
|
||||
|
||||
// FromMap creates a new [vfs.FS] from a map of paths to file contents.
|
||||
// Those file contents may be strings, byte slices, or [fstest.MapFile]s.
|
||||
//
|
||||
// The paths must be normalized absolute paths according to the tspath package,
|
||||
// without trailing directory separators.
|
||||
// The paths must be all POSIX-style or all Windows-style, but not both.
|
||||
func FromMap[File any](m map[string]File, useCaseSensitiveFileNames bool) vfs.FS {
|
||||
return FromMapWithClock(m, useCaseSensitiveFileNames, &clockImpl{start: time.Now()})
|
||||
}
|
||||
|
||||
// FromMapWithClock creates a new [vfs.FS] from a map of paths to file contents.
|
||||
// Those file contents may be strings, byte slices, or [fstest.MapFile]s.
|
||||
//
|
||||
// The paths must be normalized absolute paths according to the tspath package,
|
||||
// without trailing directory separators.
|
||||
// The paths must be all POSIX-style or all Windows-style, but not both.
|
||||
func FromMapWithClock[File any](m map[string]File, useCaseSensitiveFileNames bool, clock Clock) vfs.FS {
|
||||
posix := false
|
||||
windows := false
|
||||
|
||||
checkPath := func(p string) {
|
||||
if !tspath.IsRootedDiskPath(p) {
|
||||
panic(fmt.Sprintf("non-rooted path %q", p))
|
||||
}
|
||||
|
||||
if normal := tspath.RemoveTrailingDirectorySeparator(tspath.NormalizePath(p)); normal != p {
|
||||
panic(fmt.Sprintf("non-normalized path %q", p))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(p, "/") {
|
||||
posix = true
|
||||
} else {
|
||||
windows = true
|
||||
}
|
||||
}
|
||||
|
||||
mfs := make(fstest.MapFS, len(m))
|
||||
// Sorted creation to ensure times are always guaranteed to be in order.
|
||||
keys := slices.Collect(maps.Keys(m))
|
||||
slices.SortFunc(keys, comparePathsByParts)
|
||||
for _, p := range keys {
|
||||
f := m[p]
|
||||
checkPath(p)
|
||||
|
||||
var file *fstest.MapFile
|
||||
switch f := any(f).(type) {
|
||||
case string:
|
||||
file = &fstest.MapFile{Data: []byte(f), ModTime: clock.Now()}
|
||||
case []byte:
|
||||
file = &fstest.MapFile{Data: f, ModTime: clock.Now()}
|
||||
case *fstest.MapFile:
|
||||
fCopy := *f
|
||||
fCopy.ModTime = clock.Now()
|
||||
file = &fCopy
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid file type %T", f))
|
||||
}
|
||||
|
||||
if file.Mode&fs.ModeSymlink != 0 {
|
||||
target := string(file.Data)
|
||||
checkPath(target)
|
||||
|
||||
target, _ = strings.CutPrefix(target, "/")
|
||||
fileCopy := *file
|
||||
fileCopy.Data = []byte(target)
|
||||
file = &fileCopy
|
||||
}
|
||||
|
||||
p, _ = strings.CutPrefix(p, "/")
|
||||
mfs[p] = file
|
||||
}
|
||||
|
||||
if posix && windows {
|
||||
panic("mixed posix and windows paths")
|
||||
}
|
||||
|
||||
return iovfs.From(convertMapFS(mfs, useCaseSensitiveFileNames, clock), useCaseSensitiveFileNames)
|
||||
}
|
||||
|
||||
func convertMapFS(input fstest.MapFS, useCaseSensitiveFileNames bool, clock Clock) *MapFS {
|
||||
if clock == nil {
|
||||
clock = &clockImpl{start: time.Now()}
|
||||
}
|
||||
m := &MapFS{
|
||||
m: make(fstest.MapFS, len(input)),
|
||||
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
|
||||
clock: clock,
|
||||
}
|
||||
|
||||
// Verify that the input is well-formed.
|
||||
canonicalPaths := make(map[canonicalPath]string, len(input))
|
||||
for path := range input {
|
||||
canonical := m.getCanonicalPath(path)
|
||||
if other, ok := canonicalPaths[canonical]; ok {
|
||||
// Ensure consistent panic messages
|
||||
path, other = min(path, other), max(path, other)
|
||||
panic(fmt.Sprintf("duplicate path: %q and %q have the same canonical path", path, other))
|
||||
}
|
||||
canonicalPaths[canonical] = path
|
||||
}
|
||||
|
||||
// Sort the input by depth and path so we ensure parent dirs are created
|
||||
// before their children, if explicitly specified by the input.
|
||||
inputKeys := slices.Collect(maps.Keys(input))
|
||||
slices.SortFunc(inputKeys, comparePathsByParts)
|
||||
|
||||
for _, p := range inputKeys {
|
||||
file := input[p]
|
||||
|
||||
// Create all missing intermediate directories so we can attach the realpath to each of them.
|
||||
// fstest.MapFS doesn't require this as it synthesizes directories on the fly, but it's a lot
|
||||
// harder to reapply a realpath onto those when we're deep in some FileInfo method.
|
||||
if dir := dirName(p); dir != "" {
|
||||
if err := m.mkdirAll(dir, 0o777); err != nil {
|
||||
panic(fmt.Sprintf("failed to create intermediate directories for %q: %v", p, err))
|
||||
}
|
||||
}
|
||||
m.setEntry(p, m.getCanonicalPath(p), *file)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func comparePathsByParts(a, b string) int {
|
||||
for {
|
||||
aStart, aEnd, aOk := strings.Cut(a, "/")
|
||||
bStart, bEnd, bOk := strings.Cut(b, "/")
|
||||
|
||||
if !aOk || !bOk {
|
||||
return strings.Compare(a, b)
|
||||
}
|
||||
|
||||
if r := strings.Compare(aStart, bStart); r != 0 {
|
||||
return r
|
||||
}
|
||||
|
||||
a, b = aEnd, bEnd
|
||||
}
|
||||
}
|
||||
|
||||
type canonicalPath string
|
||||
|
||||
func (m *MapFS) getCanonicalPath(p string) canonicalPath {
|
||||
return canonicalPath(tspath.GetCanonicalFileName(p, m.useCaseSensitiveFileNames))
|
||||
}
|
||||
|
||||
func (m *MapFS) open(p canonicalPath) (fs.File, error) {
|
||||
return m.m.Open(string(p))
|
||||
}
|
||||
|
||||
func (m *MapFS) remove(path string) error {
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
fileInfo := m.m[canonicalString]
|
||||
if fileInfo == nil {
|
||||
// file does not exist
|
||||
return nil
|
||||
}
|
||||
delete(m.m, canonicalString)
|
||||
delete(m.symlinks, canonical)
|
||||
|
||||
if fileInfo.Mode.IsDir() {
|
||||
canonicalString += "/"
|
||||
for path := range m.m {
|
||||
if strings.HasPrefix(path, canonicalString) {
|
||||
delete(m.m, path)
|
||||
delete(m.symlinks, canonicalPath(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Symlink(target string) *fstest.MapFile {
|
||||
return &fstest.MapFile{
|
||||
Data: []byte(target),
|
||||
Mode: fs.ModeSymlink,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MapFS) getFollowingSymlinks(p canonicalPath) (*fstest.MapFile, canonicalPath, error) {
|
||||
return m.getFollowingSymlinksWorker(p, "", "")
|
||||
}
|
||||
|
||||
type brokenSymlinkError struct {
|
||||
from, to canonicalPath
|
||||
}
|
||||
|
||||
func (e *brokenSymlinkError) Error() string {
|
||||
return fmt.Sprintf("broken symlink %q -> %q", e.from, e.to)
|
||||
}
|
||||
|
||||
func isBrokenSymlinkError(err error) bool {
|
||||
_, ok := errors.AsType[*brokenSymlinkError](err)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *MapFS) getFollowingSymlinksWorker(p canonicalPath, symlinkFrom, symlinkTo canonicalPath) (*fstest.MapFile, canonicalPath, error) {
|
||||
if file, ok := m.m[string(p)]; ok && file.Mode&fs.ModeSymlink == 0 {
|
||||
return file, p, nil
|
||||
}
|
||||
|
||||
if target, ok := m.symlinks[p]; ok {
|
||||
return m.getFollowingSymlinksWorker(target, p, target)
|
||||
}
|
||||
|
||||
// This could be a path underneath a symlinked directory.
|
||||
for other, target := range m.symlinks {
|
||||
if len(other) < len(p) && other == p[:len(other)] && p[len(other)] == '/' {
|
||||
return m.getFollowingSymlinksWorker(target+p[len(other):], other, target)
|
||||
}
|
||||
}
|
||||
|
||||
err := fs.ErrNotExist
|
||||
if symlinkFrom != "" {
|
||||
err = &brokenSymlinkError{symlinkFrom, symlinkTo}
|
||||
}
|
||||
return nil, p, err
|
||||
}
|
||||
|
||||
func (m *MapFS) set(p canonicalPath, file *fstest.MapFile) {
|
||||
m.m[string(p)] = file
|
||||
}
|
||||
|
||||
func (m *MapFS) setEntry(realpath string, canonical canonicalPath, file fstest.MapFile) {
|
||||
if realpath == "" || canonical == "" {
|
||||
panic("empty path")
|
||||
}
|
||||
|
||||
file.Sys = &sys{
|
||||
original: file.Sys,
|
||||
realpath: realpath,
|
||||
}
|
||||
m.set(canonical, &file)
|
||||
|
||||
if file.Mode&fs.ModeSymlink != 0 {
|
||||
if m.symlinks == nil {
|
||||
m.symlinks = make(map[canonicalPath]canonicalPath)
|
||||
}
|
||||
m.symlinks[canonical] = m.getCanonicalPath(string(file.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func splitPath(s string, offset int) (before, after string) {
|
||||
idx := strings.IndexByte(s[offset:], '/')
|
||||
if idx < 0 {
|
||||
return s, ""
|
||||
}
|
||||
return s[:idx+offset], s[idx+1+offset:]
|
||||
}
|
||||
|
||||
func dirName(p string) string {
|
||||
dir, _ := path.Split(p)
|
||||
return strings.TrimSuffix(dir, "/")
|
||||
}
|
||||
|
||||
func baseName(p string) string {
|
||||
_, file := path.Split(p)
|
||||
return file
|
||||
}
|
||||
|
||||
func (m *MapFS) mkdirAll(p string, perm fs.FileMode) error {
|
||||
if p == "" {
|
||||
panic("empty path")
|
||||
}
|
||||
|
||||
// Fast path; already exists.
|
||||
if other, _, err := m.getFollowingSymlinks(m.getCanonicalPath(p)); err == nil {
|
||||
if !other.Mode.IsDir() {
|
||||
return fmt.Errorf("mkdir %q: path exists but is not a directory", p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var toCreate []string
|
||||
offset := 0
|
||||
for {
|
||||
dir, rest := splitPath(p, offset)
|
||||
canonical := m.getCanonicalPath(dir)
|
||||
other, otherPath, err := m.getFollowingSymlinks(canonical)
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
toCreate = append(toCreate, dir)
|
||||
} else {
|
||||
if !other.Mode.IsDir() {
|
||||
return fmt.Errorf("mkdir %q: path exists but is not a directory", otherPath)
|
||||
}
|
||||
if canonical != otherPath {
|
||||
// We have a symlinked parent, reset and start again.
|
||||
p = other.Sys.(*sys).realpath + "/" + rest
|
||||
toCreate = toCreate[:0]
|
||||
offset = 0
|
||||
continue
|
||||
}
|
||||
}
|
||||
if rest == "" {
|
||||
break
|
||||
}
|
||||
offset = len(dir) + 1
|
||||
}
|
||||
|
||||
for _, dir := range toCreate {
|
||||
m.setEntry(dir, m.getCanonicalPath(dir), fstest.MapFile{
|
||||
Mode: fs.ModeDir | perm&^umask,
|
||||
ModTime: m.clock.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
fs.FileInfo
|
||||
sys any
|
||||
realpath string
|
||||
}
|
||||
|
||||
func (fi *fileInfo) Name() string {
|
||||
return baseName(fi.realpath)
|
||||
}
|
||||
|
||||
func (fi *fileInfo) Sys() any {
|
||||
return fi.sys
|
||||
}
|
||||
|
||||
type file struct {
|
||||
fs.File
|
||||
fileInfo *fileInfo
|
||||
}
|
||||
|
||||
func (f *file) Stat() (fs.FileInfo, error) {
|
||||
return f.fileInfo, nil
|
||||
}
|
||||
|
||||
type readDirFile struct {
|
||||
fs.ReadDirFile
|
||||
fileInfo *fileInfo
|
||||
}
|
||||
|
||||
func (f *readDirFile) Stat() (fs.FileInfo, error) {
|
||||
return f.fileInfo, nil
|
||||
}
|
||||
|
||||
func (f *readDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
list, err := f.ReadDirFile.ReadDir(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries := make([]fs.DirEntry, len(list))
|
||||
for i, entry := range list {
|
||||
info := must(entry.Info())
|
||||
newInfo, ok := convertInfo(info)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unexpected synthesized dir: %q", info.Name()))
|
||||
}
|
||||
entries[i] = fs.FileInfoToDirEntry(newInfo)
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (m *MapFS) Open(name string) (fs.File, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
_, cp, _ := m.getFollowingSymlinks(m.getCanonicalPath(name))
|
||||
f, err := m.open(cp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := must(f.Stat())
|
||||
|
||||
newInfo, ok := convertInfo(info)
|
||||
if !ok {
|
||||
// This is a synthesized dir.
|
||||
if name != "." {
|
||||
panic(fmt.Sprintf("unexpected synthesized dir: %q", name))
|
||||
}
|
||||
|
||||
return &readDirFile{
|
||||
ReadDirFile: f.(fs.ReadDirFile),
|
||||
fileInfo: &fileInfo{
|
||||
FileInfo: info,
|
||||
sys: info.Sys(),
|
||||
realpath: ".",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if f, ok := f.(fs.ReadDirFile); ok {
|
||||
return &readDirFile{
|
||||
ReadDirFile: f,
|
||||
fileInfo: newInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &file{
|
||||
File: f,
|
||||
fileInfo: newInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MapFS) Realpath(name string) (string, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
file, _, err := m.getFollowingSymlinks(m.getCanonicalPath(name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return file.Sys.(*sys).realpath, nil
|
||||
}
|
||||
|
||||
func convertInfo(info fs.FileInfo) (*fileInfo, bool) {
|
||||
sys, ok := info.Sys().(*sys)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &fileInfo{
|
||||
FileInfo: info,
|
||||
sys: sys.original,
|
||||
realpath: sys.realpath,
|
||||
}, true
|
||||
}
|
||||
|
||||
const umask = 0o022
|
||||
|
||||
func (m *MapFS) MkdirAll(path string, perm fs.FileMode) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return m.mkdirAll(path, perm)
|
||||
}
|
||||
|
||||
func (m *MapFS) AddSymlink(path string, target string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
canonical := m.getCanonicalPath(path)
|
||||
m.setEntry(path, canonical, fstest.MapFile{
|
||||
Data: []byte(target),
|
||||
Mode: fs.ModeSymlink,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *MapFS) WriteFile(path string, data string, perm fs.FileMode) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if parent := dirName(path); parent != "" {
|
||||
canonical := m.getCanonicalPath(parent)
|
||||
parentFile, _, err := m.getFollowingSymlinks(canonical)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write %q: %w", path, err)
|
||||
}
|
||||
if !parentFile.Mode.IsDir() {
|
||||
return fmt.Errorf("write %q: parent path exists but is not a directory", path)
|
||||
}
|
||||
}
|
||||
|
||||
file, cp, err := m.getFollowingSymlinks(m.getCanonicalPath(path))
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) && !isBrokenSymlinkError(err) {
|
||||
// No other errors are possible.
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if !file.Mode.IsRegular() {
|
||||
return fmt.Errorf("write %q: path exists but is not a regular file", path)
|
||||
}
|
||||
}
|
||||
|
||||
m.setEntry(path, cp, fstest.MapFile{
|
||||
Data: unsafe.Slice(unsafe.StringData(data), len(data)),
|
||||
ModTime: m.clock.Now(),
|
||||
Mode: perm &^ umask,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MapFS) AppendFile(path string, data string, perm fs.FileMode) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if parent := dirName(path); parent != "" {
|
||||
canonical := m.getCanonicalPath(parent)
|
||||
parentFile, _, err := m.getFollowingSymlinks(canonical)
|
||||
if err != nil {
|
||||
return fmt.Errorf("append %q: %w", path, err)
|
||||
}
|
||||
if !parentFile.Mode.IsDir() {
|
||||
return fmt.Errorf("append %q: parent path exists but is not a directory", path)
|
||||
}
|
||||
}
|
||||
|
||||
var existing []byte
|
||||
var existingMode fs.FileMode
|
||||
file, cp, err := m.getFollowingSymlinks(m.getCanonicalPath(path))
|
||||
if err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) && !isBrokenSymlinkError(err) {
|
||||
// No other errors are possible.
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if !file.Mode.IsRegular() {
|
||||
return fmt.Errorf("append %q: path exists but is not a regular file", path)
|
||||
}
|
||||
existing = file.Data
|
||||
existingMode = file.Mode
|
||||
}
|
||||
|
||||
combined := make([]byte, 0, len(existing)+len(data))
|
||||
combined = append(combined, existing...)
|
||||
combined = append(combined, data...)
|
||||
|
||||
mode := existingMode
|
||||
if mode == 0 {
|
||||
mode = perm &^ umask
|
||||
}
|
||||
|
||||
m.setEntry(path, cp, fstest.MapFile{
|
||||
Data: combined,
|
||||
ModTime: m.clock.Now(),
|
||||
Mode: mode,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MapFS) Remove(path string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return m.remove(path)
|
||||
}
|
||||
|
||||
func (m *MapFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
fileInfo := m.m[canonicalString]
|
||||
if fileInfo == nil {
|
||||
// file does not exist
|
||||
return fs.ErrNotExist
|
||||
}
|
||||
fileInfo.ModTime = mTime
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MapFS) GetTargetOfSymlink(path string) (string, bool) {
|
||||
path, _ = strings.CutPrefix(path, "/")
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
if fileInfo, ok := m.m[canonicalString]; ok {
|
||||
if fileInfo.Mode&fs.ModeSymlink != 0 {
|
||||
return "/" + string(fileInfo.Data), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (m *MapFS) GetModTime(path string) time.Time {
|
||||
path, _ = strings.CutPrefix(path, "/")
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
if fileInfo, ok := m.m[canonicalString]; ok {
|
||||
return fileInfo.ModTime
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (m *MapFS) Entries() iter.Seq2[string, *fstest.MapFile] {
|
||||
return func(yield func(string, *fstest.MapFile) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
inputKeys := slices.Collect(maps.Keys(m.m))
|
||||
slices.SortFunc(inputKeys, comparePathsByParts)
|
||||
|
||||
for _, p := range inputKeys {
|
||||
file := m.m[p]
|
||||
path := file.Sys.(*sys).realpath
|
||||
if !tspath.PathIsAbsolute(path) {
|
||||
path = "/" + path
|
||||
}
|
||||
if !yield(path, file) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MapFS) GetFileInfo(path string) *fstest.MapFile {
|
||||
path, _ = strings.CutPrefix(path, "/")
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
canonical := m.getCanonicalPath(path)
|
||||
canonicalString := string(canonical)
|
||||
return m.m[canonicalString]
|
||||
}
|
||||
|
||||
func must[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
751
tools/tsgo/internal/vfs/vfstest/vfstest_test.go
Normal file
751
tools/tsgo/internal/vfs/vfstest/vfstest_test.go
Normal file
@@ -0,0 +1,751 @@
|
||||
package vfstest
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io/fs"
|
||||
"math/rand/v2"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/testutil"
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestInsensitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
contents := []byte("bar")
|
||||
|
||||
vfs := convertMapFS(fstest.MapFS{
|
||||
"foo/bar/baz": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"foo/bar2/baz2": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"foo/bar3/baz3": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
}, false /*useCaseSensitiveFileNames*/, nil)
|
||||
|
||||
sensitive, err := fs.ReadFile(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, sensitive, contents)
|
||||
sensitiveInfo, err := fs.Stat(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, sensitiveInfo.Sys(), 1234)
|
||||
sensitiveRealPath, err := vfs.Realpath("foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, sensitiveRealPath, "foo/bar/baz")
|
||||
entries, err := fs.ReadDir(vfs, "foo")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, dirEntriesToNames(entries), []string{"bar", "bar2", "bar3"})
|
||||
|
||||
_, err = vfs.Realpath("does/not/exist")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
_, err = fs.Stat(vfs, "does/not/exist")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
|
||||
assert.NilError(t, fstest.TestFS(vfs, "foo/bar/baz"))
|
||||
|
||||
insensitive, err := fs.ReadFile(vfs, "Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, insensitive, contents)
|
||||
insensitiveInfo, err := fs.Stat(vfs, "Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, insensitiveInfo.Sys(), 1234)
|
||||
insensitiveRealPath, err := vfs.Realpath("Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, insensitiveRealPath, "foo/bar/baz")
|
||||
entries, err = fs.ReadDir(vfs, "Foo")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, dirEntriesToNames(entries), []string{"bar", "bar2", "bar3"})
|
||||
|
||||
_, err = vfs.Realpath("Does/Not/Exist")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
_, err = fs.Stat(vfs, "Does/Not/Exist")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
|
||||
// TODO: TestFS doesn't understand case-insensitive file systems.
|
||||
// This same thing would happen with an os.Dir on Windows.
|
||||
// assert.NilError(t, fstest.TestFS(vfs, "Foo/Bar/Baz"))
|
||||
}
|
||||
|
||||
func TestInsensitiveUpper(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
contents := []byte("bar")
|
||||
|
||||
vfs := convertMapFS(fstest.MapFS{
|
||||
"Foo/Bar/Baz": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"Foo/Bar2/Baz2": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"Foo/Bar3/Baz3": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
}, false /*useCaseSensitiveFileNames*/, nil)
|
||||
|
||||
sensitive, err := fs.ReadFile(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, sensitive, contents)
|
||||
sensitiveInfo, err := fs.Stat(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, sensitiveInfo.Sys(), 1234)
|
||||
entries, err := fs.ReadDir(vfs, "foo")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, dirEntriesToNames(entries), []string{"Bar", "Bar2", "Bar3"})
|
||||
|
||||
// assert.NilError(t, fstest.TestFS(vfs, "foo/bar/baz"))
|
||||
|
||||
insensitive, err := fs.ReadFile(vfs, "Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, insensitive, contents)
|
||||
insensitiveInfo, err := fs.Stat(vfs, "Foo/Bar/Baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, insensitiveInfo.Sys(), 1234)
|
||||
entries, err = fs.ReadDir(vfs, "Foo")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, dirEntriesToNames(entries), []string{"Bar", "Bar2", "Bar3"})
|
||||
|
||||
assert.NilError(t, fstest.TestFS(vfs, "Foo/Bar/Baz"))
|
||||
}
|
||||
|
||||
func TestSensitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
contents := []byte("bar")
|
||||
|
||||
vfs := convertMapFS(fstest.MapFS{
|
||||
"foo/bar/baz": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"foo/bar2/baz2": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
"foo/bar3/baz3": &fstest.MapFile{
|
||||
Data: contents,
|
||||
Sys: 1234,
|
||||
},
|
||||
}, true /*useCaseSensitiveFileNames*/, nil)
|
||||
|
||||
sensitive, err := fs.ReadFile(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, sensitive, contents)
|
||||
sensitiveInfo, err := fs.Stat(vfs, "foo/bar/baz")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, sensitiveInfo.Sys(), 1234)
|
||||
|
||||
assert.NilError(t, fstest.TestFS(vfs, "foo/bar/baz"))
|
||||
|
||||
_, err = fs.ReadFile(vfs, "Foo/Bar/Baz")
|
||||
assert.ErrorContains(t, err, "file does not exist")
|
||||
}
|
||||
|
||||
func TestSensitiveDuplicatePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testfs := fstest.MapFS{
|
||||
"foo": &fstest.MapFile{
|
||||
Data: []byte("bar"),
|
||||
},
|
||||
"Foo": &fstest.MapFile{
|
||||
Data: []byte("baz"),
|
||||
},
|
||||
}
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
convertMapFS(testfs, false /*useCaseSensitiveFileNames*/, nil)
|
||||
}, `duplicate path: "Foo" and "foo" have the same canonical path`)
|
||||
}
|
||||
|
||||
func TestInsensitiveDuplicatePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testfs := fstest.MapFS{
|
||||
"foo": &fstest.MapFile{
|
||||
Data: []byte("bar"),
|
||||
},
|
||||
"Foo": &fstest.MapFile{
|
||||
Data: []byte("baz"),
|
||||
},
|
||||
}
|
||||
|
||||
convertMapFS(testfs, true /*useCaseSensitiveFileNames*/, nil)
|
||||
}
|
||||
|
||||
func dirEntriesToNames(entries []fs.DirEntry) []string {
|
||||
names := make([]string, len(entries))
|
||||
for i, entry := range entries {
|
||||
names[i] = entry.Name()
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func TestWritableFS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap[any](nil, false)
|
||||
|
||||
err := fs.WriteFile("/foo/bar/baz", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok := fs.ReadFile("/foo/bar/baz")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
err = fs.WriteFile("/foo/bar/baz", "goodbye, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok = fs.ReadFile("/foo/bar/baz")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "goodbye, world")
|
||||
|
||||
err = fs.WriteFile("/foo/bar/baz/oops", "goodbye, world")
|
||||
assert.ErrorContains(t, err, `mkdir "foo/bar/baz": path exists but is not a directory`)
|
||||
}
|
||||
|
||||
func TestWritableFSDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := FromMap[any](nil, false)
|
||||
|
||||
_ = fs.WriteFile("/foo/bar/file.ts", "remove")
|
||||
assert.Assert(t, fs.FileExists("/foo/bar/file.ts"))
|
||||
err := fs.Remove("/foo/bar/file.ts")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.FileExists("/foo/bar/file.ts"))
|
||||
|
||||
_ = fs.WriteFile("/foo/bar/test/remove2.ts", "remove2")
|
||||
assert.Assert(t, fs.DirectoryExists("/foo/bar/test"))
|
||||
err = fs.Remove("/foo/bar/test")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.FileExists("/foo/bar/test/remove2.ts"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/foo/bar/test"))
|
||||
|
||||
// no errors when removing file/dir that does not exist
|
||||
err = fs.Remove("/foo/bar/test")
|
||||
assert.NilError(t, err)
|
||||
err = fs.Remove("/foo/bar/file.ts")
|
||||
assert.NilError(t, err)
|
||||
|
||||
_ = fs.WriteFile("/foo/barbar", "remove2")
|
||||
_ = fs.Remove("/foo/bar")
|
||||
assert.Assert(t, fs.FileExists("/foo/barbar"))
|
||||
}
|
||||
|
||||
func TestStress(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap[any](nil, false)
|
||||
|
||||
ops := []func(){
|
||||
func() { _ = fs.WriteFile("/foo/bar/baz.txt", "hello, world") },
|
||||
func() { fs.ReadFile("/foo/bar/baz.txt") },
|
||||
func() { fs.DirectoryExists("/foo/bar") },
|
||||
func() { fs.FileExists("/foo/bar") },
|
||||
func() { fs.FileExists("/foo/bar/baz.txt") },
|
||||
func() { fs.GetAccessibleEntries("/foo/bar") },
|
||||
func() { fs.Realpath("/foo/bar/baz.txt") },
|
||||
func() {
|
||||
_ = fs.WalkDir("/", func(path string, d vfs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.Info()
|
||||
return err
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range runtime.GOMAXPROCS(0) {
|
||||
wg.Go(func() {
|
||||
randomOps := slices.Clone(ops)
|
||||
rand.Shuffle(len(randomOps), func(i, j int) {
|
||||
randomOps[i], randomOps[j] = randomOps[j], randomOps[i]
|
||||
})
|
||||
|
||||
for i := range 10000 {
|
||||
randomOps[i%len(randomOps)]()
|
||||
}
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestParentDirFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testfs := fstest.MapFS{
|
||||
"foo": &fstest.MapFile{
|
||||
Data: []byte("bar"),
|
||||
},
|
||||
"foo/oops": &fstest.MapFile{
|
||||
Data: []byte("baz"),
|
||||
},
|
||||
}
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
convertMapFS(testfs, false /*useCaseSensitiveFileNames*/, nil)
|
||||
}, `failed to create intermediate directories for "foo/oops": mkdir "foo": path exists but is not a directory`)
|
||||
}
|
||||
|
||||
func TestFromMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("POSIX", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/string": "hello, world",
|
||||
"/bytes": []byte("hello, world"),
|
||||
"/mapfile": &fstest.MapFile{
|
||||
Data: []byte("hello, world"),
|
||||
},
|
||||
}, false)
|
||||
|
||||
content, ok := fs.ReadFile("/string")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/bytes")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/mapfile")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
})
|
||||
|
||||
t.Run("Windows", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"c:/string": "hello, world",
|
||||
"d:/bytes": []byte("hello, world"),
|
||||
"e:/mapfile": &fstest.MapFile{
|
||||
Data: []byte("hello, world"),
|
||||
},
|
||||
}, false)
|
||||
|
||||
content, ok := fs.ReadFile("c:/string")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("d:/bytes")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("e:/mapfile")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
})
|
||||
|
||||
t.Run("Mixed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"/string": "hello, world",
|
||||
"c:/bytes": []byte("hello, world"),
|
||||
}, false)
|
||||
}, `mixed posix and windows paths`)
|
||||
})
|
||||
|
||||
t.Run("NonRooted", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"string": "hello, world",
|
||||
}, false)
|
||||
}, `non-rooted path "string"`)
|
||||
})
|
||||
|
||||
t.Run("NonNormalized", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"/string/": "hello, world",
|
||||
}, false)
|
||||
}, `non-normalized path "/string/"`)
|
||||
})
|
||||
|
||||
t.Run("NonNormalized2", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"/string/../foo": "hello, world",
|
||||
}, false)
|
||||
}, `non-normalized path "/string/../foo"`)
|
||||
})
|
||||
|
||||
t.Run("InvalidFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testutil.AssertPanics(t, func() {
|
||||
FromMap(map[string]any{
|
||||
"/string": 1234,
|
||||
}, false)
|
||||
}, `invalid file type int`)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVFSTestMapFS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]string{
|
||||
"/foo.ts": "hello, world",
|
||||
"/dir1/file1.ts": "export const foo = 42;",
|
||||
"/dir1/file2.ts": "export const foo = 42;",
|
||||
"/dir2/file1.ts": "export const foo = 42;",
|
||||
}, false /*useCaseSensitiveFileNames*/)
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, ok := fs.ReadFile("/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/does/not/exist.ts")
|
||||
assert.Assert(t, !ok)
|
||||
assert.Equal(t, content, "")
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
realpath := fs.Realpath("/foo.ts")
|
||||
assert.Equal(t, realpath, "/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("/Foo.ts")
|
||||
assert.Equal(t, realpath, "/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("/does/not/exist.ts")
|
||||
assert.Equal(t, realpath, "/does/not/exist.ts")
|
||||
})
|
||||
|
||||
t.Run("UseCaseSensitiveFileNames", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, !fs.UseCaseSensitiveFileNames())
|
||||
})
|
||||
}
|
||||
|
||||
func TestVFSTestMapFSWindows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]string{
|
||||
"c:/foo.ts": "hello, world",
|
||||
"c:/dir1/file1.ts": "export const foo = 42;",
|
||||
"c:/dir1/file2.ts": "export const foo = 42;",
|
||||
"c:/dir2/file1.ts": "export const foo = 42;",
|
||||
}, false)
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, ok := fs.ReadFile("c:/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("c:/does/not/exist.ts")
|
||||
assert.Assert(t, !ok)
|
||||
assert.Equal(t, content, "")
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
realpath := fs.Realpath("c:/foo.ts")
|
||||
assert.Equal(t, realpath, "c:/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("c:/Foo.ts")
|
||||
assert.Equal(t, realpath, "c:/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("c:/does/not/exist.ts")
|
||||
assert.Equal(t, realpath, "c:/does/not/exist.ts")
|
||||
})
|
||||
}
|
||||
|
||||
func TestBOM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const expected = "hello, world"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
order binary.ByteOrder
|
||||
bom [2]byte
|
||||
}{
|
||||
{"BigEndian", binary.BigEndian, [2]byte{0xFE, 0xFF}},
|
||||
{"LittleEndian", binary.LittleEndian, [2]byte{0xFF, 0xFE}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var codePoints []uint16
|
||||
|
||||
for _, r := range expected {
|
||||
codePoints = utf16.AppendRune(codePoints, r)
|
||||
}
|
||||
|
||||
buf := tt.bom[:]
|
||||
|
||||
for _, r := range codePoints {
|
||||
var err error
|
||||
buf, err = binary.Append(buf, tt.order, r)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
fs := FromMap(map[string][]byte{
|
||||
"/foo.ts": buf,
|
||||
}, true)
|
||||
|
||||
content, ok := fs.ReadFile("/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, expected)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("UTF8", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string][]byte{
|
||||
"/foo.ts": []byte("\xEF\xBB\xBF" + expected),
|
||||
}, true)
|
||||
|
||||
content, ok := fs.ReadFile("/foo.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, expected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSymlink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/foo.ts": "hello, world",
|
||||
"/symlink.ts": Symlink("/foo.ts"),
|
||||
"/some/dir/file.ts": "hello, world",
|
||||
"/some/dirlink": Symlink("/some/dir"),
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d/existing.ts": "this is existing.ts",
|
||||
}, false)
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, ok := fs.ReadFile("/symlink.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/some/dirlink/file.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/a/existing.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "this is existing.ts")
|
||||
})
|
||||
|
||||
t.Run("Realpath", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
realpath := fs.Realpath("/symlink.ts")
|
||||
assert.Equal(t, realpath, "/foo.ts")
|
||||
|
||||
realpath = fs.Realpath("/some/dirlink")
|
||||
assert.Equal(t, realpath, "/some/dir")
|
||||
|
||||
realpath = fs.Realpath("/some/dirlink/file.ts")
|
||||
assert.Equal(t, realpath, "/some/dir/file.ts")
|
||||
})
|
||||
|
||||
t.Run("FileExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, fs.FileExists("/symlink.ts"))
|
||||
assert.Assert(t, fs.FileExists("/some/dirlink/file.ts"))
|
||||
assert.Assert(t, fs.FileExists("/a/existing.ts"))
|
||||
})
|
||||
|
||||
t.Run("DirectoryExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Assert(t, fs.DirectoryExists("/some/dirlink"))
|
||||
assert.Assert(t, fs.DirectoryExists("/d"))
|
||||
assert.Assert(t, fs.DirectoryExists("/c"))
|
||||
assert.Assert(t, fs.DirectoryExists("/b"))
|
||||
assert.Assert(t, fs.DirectoryExists("/a"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWritableFSSymlink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/some/dir/other.ts": "NOTHING",
|
||||
"/other.ts": Symlink("/some/dir/other.ts"),
|
||||
"/some/dirlink": Symlink("/some/dir"),
|
||||
"/brokenlink": Symlink("/does/not/exist"),
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d/existing.ts": "hello, world",
|
||||
}, false)
|
||||
|
||||
err := fs.WriteFile("/some/dirlink/file.ts", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok := fs.ReadFile("/some/dirlink/file.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/some/dir/file.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
err = fs.WriteFile("/some/dirlink/file.ts", "goodbye, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok = fs.ReadFile("/some/dirlink/file.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "goodbye, world")
|
||||
|
||||
err = fs.WriteFile("/other.ts", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
|
||||
content, ok = fs.ReadFile("/other.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
content, ok = fs.ReadFile("/some/dir/other.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
|
||||
err = fs.WriteFile("/some/dirlink", "hello, world")
|
||||
assert.Error(t, err, `write "some/dirlink": path exists but is not a regular file`)
|
||||
|
||||
// Can't write inside a broken dir symlink
|
||||
err = fs.WriteFile("/brokenlink/file.ts", "hello, world")
|
||||
assert.Error(t, err, `broken symlink "brokenlink" -> "does/not/exist"`)
|
||||
|
||||
err = fs.WriteFile("/brokenlink/also/wrong/file.ts", "hello, world")
|
||||
assert.Error(t, err, `broken symlink "brokenlink" -> "does/not/exist"`)
|
||||
|
||||
// But we can write to a broken file symlink
|
||||
err = fs.WriteFile("/brokenlink", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
content, ok = fs.ReadFile("/brokenlink")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
content, ok = fs.ReadFile("/does/not/exist")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "hello, world")
|
||||
}
|
||||
|
||||
func TestWritableFSSymlinkChain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d/existing.ts": "hello, world",
|
||||
}, false)
|
||||
|
||||
err := fs.WriteFile("/a/foo/bar/new.ts", "this is new.ts")
|
||||
assert.NilError(t, err)
|
||||
content, ok := fs.ReadFile("/a/foo/bar/new.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "this is new.ts")
|
||||
content, ok = fs.ReadFile("/b/foo/bar/new.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "this is new.ts")
|
||||
content, ok = fs.ReadFile("/d/foo/bar/new.ts")
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, content, "this is new.ts")
|
||||
}
|
||||
|
||||
func TestWritableFSSymlinkChainNotDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d": "hello, world",
|
||||
}, false)
|
||||
|
||||
err := fs.WriteFile("/a/foo/bar/new.ts", "this is new.ts")
|
||||
assert.Error(t, err, `mkdir "d": path exists but is not a directory`)
|
||||
}
|
||||
|
||||
func TestWritableFSSymlinkDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fs := FromMap(map[string]any{
|
||||
"/some/dir/other.ts": "NOTHING",
|
||||
"/other.ts": Symlink("/some/dir/other.ts"),
|
||||
"/some/dirlink": Symlink("/some/dir"),
|
||||
"/brokenlink": Symlink("/does/not/exist"),
|
||||
"/a": Symlink("/b"),
|
||||
"/b": Symlink("/c"),
|
||||
"/c": Symlink("/d"),
|
||||
"/d/existing.ts": "hello, world",
|
||||
}, false)
|
||||
|
||||
err := fs.Remove("/a")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.DirectoryExists("/a"))
|
||||
assert.Assert(t, fs.DirectoryExists("/b"))
|
||||
assert.Assert(t, fs.DirectoryExists("/c"))
|
||||
assert.Assert(t, fs.FileExists("/d/existing.ts"))
|
||||
|
||||
// symlinks should still exist even if underlying file/dir is deleted
|
||||
err = fs.Remove("/d")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.DirectoryExists("/b"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/c"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/d"))
|
||||
assert.Assert(t, !fs.FileExists("/d/again.ts"))
|
||||
err = fs.WriteFile("/d/again.ts", "d exists again")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, fs.DirectoryExists("/b"))
|
||||
assert.Assert(t, fs.DirectoryExists("/c"))
|
||||
content, _ := fs.ReadFile("/b/again.ts")
|
||||
assert.Equal(t, content, "d exists again")
|
||||
|
||||
assert.Assert(t, !fs.FileExists("/brokenlink"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/brokenlink"))
|
||||
err = fs.Remove("/does/not/exist") // should do nothing
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !fs.FileExists("/brokenlink"))
|
||||
assert.Assert(t, !fs.DirectoryExists("/brokenlink"))
|
||||
err = fs.WriteFile("/does/not/exist", "hello, world")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, fs.FileExists("/brokenlink"))
|
||||
}
|
||||
132
tools/tsgo/internal/vfs/wrapvfs/wrapvfs.go
Normal file
132
tools/tsgo/internal/vfs/wrapvfs/wrapvfs.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package wrapvfs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/vfs"
|
||||
)
|
||||
|
||||
type Replacements struct {
|
||||
UseCaseSensitiveFileNames func() bool
|
||||
FileExists func(string) bool
|
||||
ReadFile func(string) (string, bool)
|
||||
WriteFile func(string, string) error
|
||||
AppendFile func(string, string) error
|
||||
Remove func(string) error
|
||||
Chtimes func(string, time.Time, time.Time) error
|
||||
DirectoryExists func(string) bool
|
||||
GetAccessibleEntries func(string) vfs.Entries
|
||||
Stat func(string) vfs.FileInfo
|
||||
WalkDir func(string, vfs.WalkDirFunc) error
|
||||
Realpath func(string) string
|
||||
}
|
||||
|
||||
func Wrap(fs vfs.FS, replacements Replacements) vfs.FS {
|
||||
return &wrappedFS{
|
||||
fs: fs,
|
||||
replacements: replacements,
|
||||
}
|
||||
}
|
||||
|
||||
type wrappedFS struct {
|
||||
fs vfs.FS
|
||||
replacements Replacements
|
||||
}
|
||||
|
||||
// UseCaseSensitiveFileNames implements [vfs.FS].
|
||||
func (w *wrappedFS) UseCaseSensitiveFileNames() bool {
|
||||
if w.replacements.UseCaseSensitiveFileNames != nil {
|
||||
return w.replacements.UseCaseSensitiveFileNames()
|
||||
}
|
||||
return w.fs.UseCaseSensitiveFileNames()
|
||||
}
|
||||
|
||||
// FileExists implements [vfs.FS].
|
||||
func (w *wrappedFS) FileExists(path string) bool {
|
||||
if w.replacements.FileExists != nil {
|
||||
return w.replacements.FileExists(path)
|
||||
}
|
||||
return w.fs.FileExists(path)
|
||||
}
|
||||
|
||||
// ReadFile implements [vfs.FS].
|
||||
func (w *wrappedFS) ReadFile(path string) (contents string, ok bool) {
|
||||
if w.replacements.ReadFile != nil {
|
||||
return w.replacements.ReadFile(path)
|
||||
}
|
||||
return w.fs.ReadFile(path)
|
||||
}
|
||||
|
||||
// WriteFile implements [vfs.FS].
|
||||
func (w *wrappedFS) WriteFile(path string, data string) error {
|
||||
if w.replacements.WriteFile != nil {
|
||||
return w.replacements.WriteFile(path, data)
|
||||
}
|
||||
return w.fs.WriteFile(path, data)
|
||||
}
|
||||
|
||||
// AppendFile implements [vfs.FS].
|
||||
func (w *wrappedFS) AppendFile(path string, data string) error {
|
||||
if w.replacements.AppendFile != nil {
|
||||
return w.replacements.AppendFile(path, data)
|
||||
}
|
||||
return w.fs.AppendFile(path, data)
|
||||
}
|
||||
|
||||
// Remove implements [vfs.FS].
|
||||
func (w *wrappedFS) Remove(path string) error {
|
||||
if w.replacements.Remove != nil {
|
||||
return w.replacements.Remove(path)
|
||||
}
|
||||
return w.fs.Remove(path)
|
||||
}
|
||||
|
||||
// Chtimes implements [vfs.FS].
|
||||
func (w *wrappedFS) Chtimes(path string, aTime time.Time, mTime time.Time) error {
|
||||
if w.replacements.Chtimes != nil {
|
||||
return w.replacements.Chtimes(path, aTime, mTime)
|
||||
}
|
||||
return w.fs.Chtimes(path, aTime, mTime)
|
||||
}
|
||||
|
||||
// DirectoryExists implements [vfs.FS].
|
||||
func (w *wrappedFS) DirectoryExists(path string) bool {
|
||||
if w.replacements.DirectoryExists != nil {
|
||||
return w.replacements.DirectoryExists(path)
|
||||
}
|
||||
return w.fs.DirectoryExists(path)
|
||||
}
|
||||
|
||||
// GetAccessibleEntries implements [vfs.FS].
|
||||
func (w *wrappedFS) GetAccessibleEntries(path string) vfs.Entries {
|
||||
if w.replacements.GetAccessibleEntries != nil {
|
||||
return w.replacements.GetAccessibleEntries(path)
|
||||
}
|
||||
return w.fs.GetAccessibleEntries(path)
|
||||
}
|
||||
|
||||
// Stat implements [vfs.FS].
|
||||
func (w *wrappedFS) Stat(path string) vfs.FileInfo {
|
||||
if w.replacements.Stat != nil {
|
||||
return w.replacements.Stat(path)
|
||||
}
|
||||
return w.fs.Stat(path)
|
||||
}
|
||||
|
||||
// WalkDir implements [vfs.FS].
|
||||
func (w *wrappedFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
|
||||
if w.replacements.WalkDir != nil {
|
||||
return w.replacements.WalkDir(root, walkFn)
|
||||
}
|
||||
return w.fs.WalkDir(root, walkFn)
|
||||
}
|
||||
|
||||
// Realpath implements [vfs.FS].
|
||||
func (w *wrappedFS) Realpath(path string) string {
|
||||
if w.replacements.Realpath != nil {
|
||||
return w.replacements.Realpath(path)
|
||||
}
|
||||
return w.fs.Realpath(path)
|
||||
}
|
||||
|
||||
var _ vfs.FS = (*wrappedFS)(nil)
|
||||
Reference in New Issue
Block a user