vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View 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)
}

View 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()))
}

View 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))
}

View 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
}

View 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())
})
}

View 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)
}
}

View 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())
}

View 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())
}
})
}

View 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)
}

View 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)
}

View 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
)

View 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)
}
})
}
}

View 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.

View 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)
}
}
})
}
}

View 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]]
}

View 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}
}

File diff suppressed because it is too large Load Diff

View 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
}

View 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,
}
}

View 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)
}
}
}

View 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
}

View 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"))
}

View 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)