vendor tsgo
This commit is contained in:
134
tools/tsgo/internal/symlinks/knownsymlinks.go
Normal file
134
tools/tsgo/internal/symlinks/knownsymlinks.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package symlinks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
type KnownDirectoryLink struct {
|
||||
// Matches the casing returned by `realpath`. Used to compute the `realpath` of children.
|
||||
// Always has trailing directory separator
|
||||
Real string
|
||||
// toPath(real). Stored to avoid repeated recomputation.
|
||||
// Always has trailing directory separator
|
||||
RealPath tspath.Path
|
||||
}
|
||||
|
||||
type KnownSymlinks struct {
|
||||
directories collections.SyncMap[tspath.Path, *KnownDirectoryLink]
|
||||
directoriesByRealpath collections.SyncMap[tspath.Path, *collections.SyncSet[string]]
|
||||
files collections.SyncMap[tspath.Path, string]
|
||||
filesByRealpath collections.SyncMap[tspath.Path, *collections.SyncSet[string]]
|
||||
cwd string
|
||||
useCaseSensitiveFileNames bool
|
||||
}
|
||||
|
||||
func (cache *KnownSymlinks) HasDirectory(symlinkPath tspath.Path) bool {
|
||||
_, ok := cache.directories.Load(symlinkPath.EnsureTrailingDirectorySeparator())
|
||||
return ok
|
||||
}
|
||||
|
||||
// Gets a map from symlink to realpath. Keys have trailing directory separators.
|
||||
func (cache *KnownSymlinks) Directories() *collections.SyncMap[tspath.Path, *KnownDirectoryLink] {
|
||||
return &cache.directories
|
||||
}
|
||||
|
||||
func (cache *KnownSymlinks) DirectoriesByRealpath() *collections.SyncMap[tspath.Path, *collections.SyncSet[string]] {
|
||||
return &cache.directoriesByRealpath
|
||||
}
|
||||
|
||||
// Gets a map from symlink to realpath
|
||||
func (cache *KnownSymlinks) Files() *collections.SyncMap[tspath.Path, string] {
|
||||
return &cache.files
|
||||
}
|
||||
|
||||
// Gets a map from realpath to symlinks
|
||||
func (cache *KnownSymlinks) FilesByRealpath() *collections.SyncMap[tspath.Path, *collections.SyncSet[string]] {
|
||||
return &cache.filesByRealpath
|
||||
}
|
||||
|
||||
func (cache *KnownSymlinks) SetDirectory(symlink string, symlinkPath tspath.Path, realDirectory *KnownDirectoryLink) {
|
||||
if realDirectory != nil {
|
||||
if _, ok := cache.directories.Load(symlinkPath); !ok {
|
||||
set, _ := cache.directoriesByRealpath.LoadOrStore(realDirectory.RealPath, &collections.SyncSet[string]{})
|
||||
set.Add(symlink)
|
||||
}
|
||||
}
|
||||
cache.directories.Store(symlinkPath, realDirectory)
|
||||
}
|
||||
|
||||
func (cache *KnownSymlinks) SetFile(symlink string, symlinkPath tspath.Path, realpath string) {
|
||||
if _, ok := cache.files.Load(symlinkPath); !ok {
|
||||
realpathPath := tspath.ToPath(realpath, cache.cwd, cache.useCaseSensitiveFileNames)
|
||||
set, _ := cache.filesByRealpath.LoadOrStore(realpathPath, &collections.SyncSet[string]{})
|
||||
set.Add(symlink)
|
||||
}
|
||||
cache.files.Store(symlinkPath, realpath)
|
||||
}
|
||||
|
||||
func NewKnownSymlink(currentDirectory string, useCaseSensitiveFileNames bool) *KnownSymlinks {
|
||||
return &KnownSymlinks{
|
||||
cwd: currentDirectory,
|
||||
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
|
||||
}
|
||||
}
|
||||
|
||||
func (cache *KnownSymlinks) SetSymlinksFromResolutions(
|
||||
forEachResolvedModule func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile),
|
||||
forEachResolvedTypeReferenceDirective func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile),
|
||||
) {
|
||||
forEachResolvedModule(func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path) {
|
||||
cache.ProcessResolution(resolution.OriginalPath, resolution.ResolvedFileName)
|
||||
}, nil)
|
||||
forEachResolvedTypeReferenceDirective(func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path) {
|
||||
cache.ProcessResolution(resolution.OriginalPath, resolution.ResolvedFileName)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (cache *KnownSymlinks) ProcessResolution(originalPath string, resolvedFileName string) {
|
||||
if originalPath == "" || resolvedFileName == "" {
|
||||
return
|
||||
}
|
||||
cache.SetFile(originalPath, tspath.ToPath(originalPath, cache.cwd, cache.useCaseSensitiveFileNames), resolvedFileName)
|
||||
commonResolved, commonOriginal := cache.guessDirectorySymlink(resolvedFileName, originalPath, cache.cwd)
|
||||
if commonResolved != "" && commonOriginal != "" {
|
||||
symlinkPath := tspath.ToPath(commonOriginal, cache.cwd, cache.useCaseSensitiveFileNames)
|
||||
if !tspath.ContainsIgnoredPath(string(symlinkPath)) {
|
||||
cache.SetDirectory(
|
||||
commonOriginal,
|
||||
symlinkPath.EnsureTrailingDirectorySeparator(),
|
||||
&KnownDirectoryLink{
|
||||
Real: tspath.EnsureTrailingDirectorySeparator(commonResolved),
|
||||
RealPath: tspath.ToPath(commonResolved, cache.cwd, cache.useCaseSensitiveFileNames).EnsureTrailingDirectorySeparator(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cache *KnownSymlinks) guessDirectorySymlink(a string, b string, cwd string) (string, string) {
|
||||
aParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(a, cwd), "")
|
||||
bParts := tspath.GetPathComponents(tspath.GetNormalizedAbsolutePath(b, cwd), "")
|
||||
isDirectory := false
|
||||
for len(aParts) >= 2 && len(bParts) >= 2 &&
|
||||
!cache.isNodeModulesOrScopedPackageDirectory(aParts[len(aParts)-2]) &&
|
||||
!cache.isNodeModulesOrScopedPackageDirectory(bParts[len(bParts)-2]) &&
|
||||
tspath.GetCanonicalFileName(aParts[len(aParts)-1], cache.useCaseSensitiveFileNames) == tspath.GetCanonicalFileName(bParts[len(bParts)-1], cache.useCaseSensitiveFileNames) {
|
||||
aParts = aParts[:len(aParts)-1]
|
||||
bParts = bParts[:len(bParts)-1]
|
||||
isDirectory = true
|
||||
}
|
||||
if isDirectory {
|
||||
return tspath.GetPathFromPathComponents(aParts), tspath.GetPathFromPathComponents(bParts)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func (cache *KnownSymlinks) isNodeModulesOrScopedPackageDirectory(s string) bool {
|
||||
return s != "" && (tspath.GetCanonicalFileName(s, cache.useCaseSensitiveFileNames) == "node_modules" || strings.HasPrefix(s, "@"))
|
||||
}
|
||||
73
tools/tsgo/internal/symlinks/knownsymlinks_bench_test.go
Normal file
73
tools/tsgo/internal/symlinks/knownsymlinks_bench_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package symlinks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func BenchmarkPopulateSymlinksFromResolutions(b *testing.B) {
|
||||
cache := NewKnownSymlink("/project", true)
|
||||
|
||||
deps := make([]struct{ orig, resolved string }, 50)
|
||||
for i := range 50 {
|
||||
deps[i].orig = "/project/node_modules/pkg" + string(rune('A'+i)) + "/index.js"
|
||||
deps[i].resolved = "/real/pkg" + string(rune('A'+i)) + "/index.js"
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
for _, dep := range deps {
|
||||
cache.ProcessResolution(dep.orig, dep.resolved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSetFile(b *testing.B) {
|
||||
cache := NewKnownSymlink("/project", true)
|
||||
symlink := "/project/file.ts"
|
||||
path := tspath.ToPath(symlink, "/project", true)
|
||||
|
||||
for b.Loop() {
|
||||
cache.SetFile(symlink, path, "/real/file.ts")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSetDirectory(b *testing.B) {
|
||||
cache := NewKnownSymlink("/project", true)
|
||||
symlinkPath := tspath.ToPath("/project/symlink", "/project", true).EnsureTrailingDirectorySeparator()
|
||||
realDir := &KnownDirectoryLink{
|
||||
Real: "/real/path/",
|
||||
RealPath: tspath.ToPath("/real/path", "/project", true).EnsureTrailingDirectorySeparator(),
|
||||
}
|
||||
|
||||
for b.Loop() {
|
||||
cache.SetDirectory("/project/symlink", symlinkPath, realDir)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGuessDirectorySymlink(b *testing.B) {
|
||||
cache := NewKnownSymlink("/project", true)
|
||||
|
||||
for b.Loop() {
|
||||
cache.guessDirectorySymlink(
|
||||
"/real/node_modules/package/dist/index.js",
|
||||
"/project/symlink/package/dist/index.js",
|
||||
"/project",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConcurrentAccess(b *testing.B) {
|
||||
cache := NewKnownSymlink("/project", true)
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
symlink := "/project/file" + string(rune('A'+(i%26))) + ".ts"
|
||||
path := tspath.ToPath(symlink, "/project", true)
|
||||
cache.SetFile(symlink, path, "/real/file.ts")
|
||||
cache.Files().Load(path)
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
291
tools/tsgo/internal/symlinks/knownsymlinks_test.go
Normal file
291
tools/tsgo/internal/symlinks/knownsymlinks_test.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package symlinks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
func TestNewKnownSymlink(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache := NewKnownSymlink("/test/dir", true)
|
||||
if cache == nil {
|
||||
t.Fatal("Expected non-nil cache")
|
||||
}
|
||||
if cache.cwd != "/test/dir" {
|
||||
t.Errorf("Expected cwd to be '/test/dir', got '%s'", cache.cwd)
|
||||
}
|
||||
if !cache.useCaseSensitiveFileNames {
|
||||
t.Error("Expected useCaseSensitiveFileNames to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDirectory(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache := NewKnownSymlink("/test/dir", true)
|
||||
symlinkPath := tspath.ToPath("/test/symlink", "/test/dir", true).EnsureTrailingDirectorySeparator()
|
||||
realDirectory := &KnownDirectoryLink{
|
||||
Real: "/real/path/",
|
||||
RealPath: tspath.ToPath("/real/path", "/test/dir", true).EnsureTrailingDirectorySeparator(),
|
||||
}
|
||||
|
||||
cache.SetDirectory("/test/symlink", symlinkPath, realDirectory)
|
||||
|
||||
// Check that directory was stored
|
||||
stored, ok := cache.Directories().Load(symlinkPath)
|
||||
if !ok {
|
||||
t.Fatal("Expected directory to be stored")
|
||||
}
|
||||
if stored.Real != realDirectory.Real {
|
||||
t.Errorf("Expected Real to be '%s', got '%s'", realDirectory.Real, stored.Real)
|
||||
}
|
||||
if stored.RealPath != realDirectory.RealPath {
|
||||
t.Errorf("Expected RealPath to be '%s', got '%s'", realDirectory.RealPath, stored.RealPath)
|
||||
}
|
||||
|
||||
// Check that realpath mapping was created
|
||||
set, ok := cache.DirectoriesByRealpath().Load(realDirectory.RealPath)
|
||||
if !ok || set.Size() == 0 {
|
||||
t.Fatal("Expected realpath mapping to be created")
|
||||
}
|
||||
if !set.Has("/test/symlink") {
|
||||
t.Error("Expected symlink '/test/symlink' to be in set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache := NewKnownSymlink("/test/dir", true)
|
||||
symlink := "/test/symlink/file.ts"
|
||||
symlinkPath := tspath.ToPath(symlink, "/test/dir", true)
|
||||
realpath := "/real/path/file.ts"
|
||||
|
||||
cache.SetFile(symlink, symlinkPath, realpath)
|
||||
|
||||
stored, ok := cache.Files().Load(symlinkPath)
|
||||
if !ok {
|
||||
t.Fatal("Expected file to be stored")
|
||||
}
|
||||
if stored != realpath {
|
||||
t.Errorf("Expected realpath to be '%s', got '%s'", realpath, stored)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessResolution(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache := NewKnownSymlink("/test/dir", true)
|
||||
|
||||
// Test with empty paths
|
||||
cache.ProcessResolution("", "")
|
||||
cache.ProcessResolution("original", "")
|
||||
cache.ProcessResolution("", "resolved")
|
||||
|
||||
// Test with valid paths
|
||||
originalPath := "/test/original/file.ts"
|
||||
resolvedPath := "/test/resolved/file.ts"
|
||||
cache.ProcessResolution(originalPath, resolvedPath)
|
||||
|
||||
// Check that file was stored
|
||||
symlinkPath := tspath.ToPath(originalPath, "/test/dir", true)
|
||||
stored, ok := cache.Files().Load(symlinkPath)
|
||||
if !ok {
|
||||
t.Fatal("Expected file to be stored")
|
||||
}
|
||||
if stored != resolvedPath {
|
||||
t.Errorf("Expected resolved path to be '%s', got '%s'", resolvedPath, stored)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuessDirectorySymlink(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache := NewKnownSymlink("/test/dir", true)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
cwd string
|
||||
expected [2]string // [commonResolved, commonOriginal]
|
||||
}{
|
||||
{
|
||||
name: "identical paths",
|
||||
a: "/test/path/file.ts",
|
||||
b: "/test/path/file.ts",
|
||||
cwd: "/test/dir",
|
||||
expected: [2]string{"/", "/"},
|
||||
},
|
||||
{
|
||||
name: "different files same directory",
|
||||
a: "/test/path/file1.ts",
|
||||
b: "/test/path/file2.ts",
|
||||
cwd: "/test/dir",
|
||||
expected: [2]string{"", ""},
|
||||
},
|
||||
{
|
||||
name: "different directories",
|
||||
a: "/test/path1/file.ts",
|
||||
b: "/test/path2/file.ts",
|
||||
cwd: "/test/dir",
|
||||
expected: [2]string{"/test/path1", "/test/path2"},
|
||||
},
|
||||
{
|
||||
name: "node_modules paths",
|
||||
a: "/test/node_modules/pkg/file.ts",
|
||||
b: "/test/node_modules/pkg/file.ts",
|
||||
cwd: "/test/dir",
|
||||
expected: [2]string{"/test/node_modules/pkg", "/test/node_modules/pkg"},
|
||||
},
|
||||
{
|
||||
name: "scoped package paths",
|
||||
a: "/test/node_modules/@scope/pkg/file.ts",
|
||||
b: "/test/node_modules/@scope/pkg/file.ts",
|
||||
cwd: "/test/dir",
|
||||
expected: [2]string{"/test/node_modules/@scope/pkg", "/test/node_modules/@scope/pkg"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
commonResolved, commonOriginal := cache.guessDirectorySymlink(tt.a, tt.b, tt.cwd)
|
||||
if commonResolved != tt.expected[0] {
|
||||
t.Errorf("Expected commonResolved to be '%s', got '%s'", tt.expected[0], commonResolved)
|
||||
}
|
||||
if commonOriginal != tt.expected[1] {
|
||||
t.Errorf("Expected commonOriginal to be '%s', got '%s'", tt.expected[1], commonOriginal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNodeModulesOrScopedPackageDirectory(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache := NewKnownSymlink("/test/dir", true)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dir string
|
||||
expected bool
|
||||
}{
|
||||
{"node_modules", "node_modules", true},
|
||||
{"scoped package", "@scope", true},
|
||||
{"regular directory", "src", false},
|
||||
{"empty string", "", false},
|
||||
{"case insensitive node_modules", "NODE_MODULES", false}, // The function is case sensitive
|
||||
{"case insensitive scoped", "@SCOPE", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := cache.isNodeModulesOrScopedPackageDirectory(tt.dir)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %v, got %v for directory '%s'", tt.expected, result, tt.dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSymlinksFromResolutions(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache := NewKnownSymlink("/test/dir", true)
|
||||
|
||||
// Mock resolution data
|
||||
resolvedModules := []struct {
|
||||
originalPath string
|
||||
resolvedPath string
|
||||
moduleName string
|
||||
mode core.ResolutionMode
|
||||
filePath tspath.Path
|
||||
}{
|
||||
{
|
||||
originalPath: "/test/original/file1.ts",
|
||||
resolvedPath: "/test/resolved/file1.ts",
|
||||
moduleName: "module1",
|
||||
mode: core.ResolutionModeNone,
|
||||
filePath: tspath.ToPath("/test/source.ts", "/test/dir", true),
|
||||
},
|
||||
{
|
||||
originalPath: "/test/original/file2.ts",
|
||||
resolvedPath: "/test/resolved/file2.ts",
|
||||
moduleName: "module2",
|
||||
mode: core.ResolutionModeNone,
|
||||
filePath: tspath.ToPath("/test/source.ts", "/test/dir", true),
|
||||
},
|
||||
}
|
||||
|
||||
// Mock callbacks
|
||||
forEachResolvedModule := func(callback func(resolution *module.ResolvedModule, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) {
|
||||
for _, res := range resolvedModules {
|
||||
resolution := &module.ResolvedModule{
|
||||
OriginalPath: res.originalPath,
|
||||
ResolvedFileName: res.resolvedPath,
|
||||
}
|
||||
callback(resolution, res.moduleName, res.mode, res.filePath)
|
||||
}
|
||||
}
|
||||
|
||||
forEachResolvedTypeReferenceDirective := func(callback func(resolution *module.ResolvedTypeReferenceDirective, moduleName string, mode core.ResolutionMode, filePath tspath.Path), file *ast.SourceFile) {
|
||||
// No type reference directives for this test
|
||||
}
|
||||
|
||||
cache.SetSymlinksFromResolutions(forEachResolvedModule, forEachResolvedTypeReferenceDirective)
|
||||
|
||||
// Check that files were stored
|
||||
for _, res := range resolvedModules {
|
||||
symlinkPath := tspath.ToPath(res.originalPath, "/test/dir", true)
|
||||
stored, ok := cache.Files().Load(symlinkPath)
|
||||
if !ok {
|
||||
t.Errorf("Expected file '%s' to be stored", res.originalPath)
|
||||
continue
|
||||
}
|
||||
if stored != res.resolvedPath {
|
||||
t.Errorf("Expected resolved path to be '%s', got '%s'", res.resolvedPath, stored)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnownSymlinksThreadSafety(t *testing.T) {
|
||||
t.Parallel()
|
||||
cache := NewKnownSymlink("/test/dir", true)
|
||||
|
||||
// Test concurrent access
|
||||
done := make(chan bool, 10)
|
||||
|
||||
for i := range 10 {
|
||||
go func(id int) {
|
||||
defer func() { done <- true }()
|
||||
|
||||
symlinkPath := tspath.ToPath("/test/symlink"+string(rune(id)), "/test/dir", true).EnsureTrailingDirectorySeparator()
|
||||
realDirectory := &KnownDirectoryLink{
|
||||
Real: "/real/path" + string(rune(id)) + "/",
|
||||
RealPath: tspath.ToPath("/real/path"+string(rune(id)), "/test/dir", true).EnsureTrailingDirectorySeparator(),
|
||||
}
|
||||
|
||||
cache.SetDirectory("/test/symlink"+string(rune(id)), symlinkPath, realDirectory)
|
||||
|
||||
// Read back
|
||||
stored, ok := cache.Directories().Load(symlinkPath)
|
||||
if !ok {
|
||||
t.Errorf("Goroutine %d: Expected directory to be stored", id)
|
||||
return
|
||||
}
|
||||
if stored.Real != realDirectory.Real {
|
||||
t.Errorf("Goroutine %d: Expected Real to be '%s', got '%s'", id, realDirectory.Real, stored.Real)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for range 10 {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify all directories were stored
|
||||
if cache.Directories().Size() != 10 {
|
||||
t.Errorf("Expected 10 directories to be stored, got %d", cache.Directories().Size())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user