Skip to content

Commit ff5823b

Browse files
authored
Avoid repeated realpath in vfsmatch (#3825)
1 parent 5b70f41 commit ff5823b

4 files changed

Lines changed: 57 additions & 16 deletions

File tree

internal/vfs/internal/internal.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,31 +66,34 @@ func (vfs *Common) DirectoryExists(path string) bool {
6666
}
6767

6868
func (vfs *Common) GetAccessibleEntries(path string) (result vfs.Entries) {
69-
addToResult := func(name string, mode fs.FileMode) (added bool) {
69+
result.Symlinks = map[string]struct{}{}
70+
71+
addToResult := func(name string, mode fs.FileMode, isLink bool) (added bool) {
7072
if mode.IsDir() {
7173
result.Directories = append(result.Directories, name)
72-
return true
73-
}
74-
75-
if mode.IsRegular() {
74+
} else if mode.IsRegular() {
7675
result.Files = append(result.Files, name)
77-
return true
76+
} else {
77+
return false
7878
}
7979

80-
return false
80+
if isLink {
81+
result.Symlinks[name] = struct{}{}
82+
}
83+
return true
8184
}
8285

8386
for _, entry := range vfs.getEntries(path) {
8487
entryType := entry.Type()
8588

86-
if addToResult(entry.Name(), entryType) {
89+
if addToResult(entry.Name(), entryType, false) {
8790
continue
8891
}
8992

9093
if entryType&fs.ModeSymlink != 0 {
9194
// Easy case; UNIX-like system will clearly mark symlinks.
9295
if stat := vfs.Stat(path + "/" + entry.Name()); stat != nil {
93-
addToResult(entry.Name(), stat.Mode())
96+
addToResult(entry.Name(), stat.Mode(), true)
9497
}
9598
continue
9699
}
@@ -101,7 +104,7 @@ func (vfs *Common) GetAccessibleEntries(path string) (result vfs.Entries) {
101104
fullPath := path + "/" + entry.Name()
102105
if vfs.IsReparsePoint(fullPath) {
103106
if stat := vfs.Stat(fullPath); stat != nil {
104-
addToResult(entry.Name(), stat.Mode())
107+
addToResult(entry.Name(), stat.Mode(), true)
105108
}
106109
}
107110
continue

internal/vfs/osvfs/realpath_test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88

99
"github.com/microsoft/typescript-go/internal/tspath"
1010
"gotest.tools/v3/assert"
11-
"gotest.tools/v3/assert/cmp"
1211
)
1312

1413
func TestSymlinkRealpath(t *testing.T) {
@@ -25,7 +24,8 @@ func TestSymlinkRealpath(t *testing.T) {
2524
targetRealpath := fs.Realpath(tspath.NormalizePath(targetFile))
2625
linkRealpath := fs.Realpath(tspath.NormalizePath(linkFile))
2726

28-
if !assert.Check(t, cmp.Equal(targetRealpath, linkRealpath)) {
27+
if targetRealpath != linkRealpath {
28+
t.Errorf("expected realpath of target and link to be equal, got %q and %q", targetRealpath, linkRealpath)
2929
cmd := exec.Command("node", "-e", `console.log({ native: fs.realpathSync.native(process.argv[1]), node: fs.realpathSync(process.argv[1]) })`, linkFile)
3030
out, err := cmd.CombinedOutput()
3131
assert.NilError(t, err)
@@ -109,4 +109,17 @@ func TestGetAccessibleEntries(t *testing.T) {
109109

110110
assert.DeepEqual(t, entries.Directories, []string{"dir1", "dir2"})
111111
assert.DeepEqual(t, entries.Files, []string{"file1", "file2"})
112+
assert.Check(t, entries.Symlinks != nil, "expected Symlinks to be set for directory with symlinks")
113+
assert.Equal(t, len(entries.Symlinks), 4)
114+
for _, name := range []string{"file1", "file2", "dir1", "dir2"} {
115+
_, ok := entries.Symlinks[name]
116+
assert.Check(t, ok, "expected %q to be in Symlinks", name)
117+
}
118+
119+
// Non-symlink directory should have empty Symlinks.
120+
entries = fs.GetAccessibleEntries(tspath.NormalizePath(target))
121+
assert.DeepEqual(t, entries.Directories, []string{"dir1", "dir2"})
122+
assert.DeepEqual(t, entries.Files, []string{"file1", "file2"})
123+
assert.Check(t, entries.Symlinks != nil, "expected Symlinks to be non-nil for directory without symlinks")
124+
assert.Equal(t, len(entries.Symlinks), 0)
112125
}

internal/vfs/vfs.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ type FS interface {
5252
type Entries struct {
5353
Files []string
5454
Directories []string
55+
// Symlinks contains the names of entries in Files or Directories that were
56+
// originally symbolic links (or reparse points) on disk. The names are the
57+
// same as those in Files/Directories (i.e., the link name, not the target).
58+
// nil means symlink information is not available and the entries may need
59+
// to be re-checked for symlinks.
60+
Symlinks map[string]struct{}
5561
}
5662

5763
type (

internal/vfs/vfsmatch/vfsmatch.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -587,9 +587,18 @@ type globVisitor struct {
587587
results [][]string
588588
}
589589

590-
func (v *globVisitor) visit(path, absolutePath string, depth int) {
590+
// visit walks a directory tree, collecting files that match the glob patterns.
591+
// resolvedRealPath, when non-empty, is the already-resolved real path for this
592+
// directory (computed incrementally from the parent). When empty, Realpath is
593+
// called to resolve symlinks.
594+
func (v *globVisitor) visit(path, absolutePath string, depth int, resolvedRealPath string) {
591595
// Detect symlink cycles
592-
realPath := v.host.Realpath(absolutePath)
596+
var realPath string
597+
if resolvedRealPath != "" {
598+
realPath = resolvedRealPath
599+
} else {
600+
realPath = v.host.Realpath(absolutePath)
601+
}
593602
canonicalPath := tspath.GetCanonicalFileName(realPath, v.useCaseSensitiveFileNames)
594603
if v.visited.Has(canonicalPath) {
595604
return
@@ -622,7 +631,17 @@ func (v *globVisitor) visit(path, absolutePath string, depth int) {
622631
continue
623632
}
624633
absDir := absPrefix + dir
625-
v.visit(pathPrefix+dir, absDir, depth)
634+
var childRealPath string
635+
if entries.Symlinks != nil {
636+
if _, isSymlink := entries.Symlinks[dir]; !isSymlink {
637+
// Non-symlink directory: compute realpath incrementally.
638+
childRealPath = tspath.CombinePaths(realPath, dir)
639+
}
640+
// else: symlink directory; leave childRealPath empty to force Realpath call.
641+
}
642+
// If Symlinks is nil, the FS doesn't track symlinks;
643+
// leave childRealPath empty to call Realpath (preserving old behavior).
644+
v.visit(pathPrefix+dir, absDir, depth, childRealPath)
626645
}
627646
}
628647

@@ -644,7 +663,7 @@ func matchFiles(path string, extensions, excludes, includes []string, useCaseSen
644663
}
645664

646665
for _, basePath := range getBasePaths(path, includes, useCaseSensitiveFileNames) {
647-
v.visit(basePath, tspath.CombinePaths(currentDirectory, basePath), depth)
666+
v.visit(basePath, tspath.CombinePaths(currentDirectory, basePath), depth, "")
648667
}
649668

650669
// Fast path: a single include bucket (or no includes) doesn't need flattening.

0 commit comments

Comments
 (0)