diff --git a/tsc/internal/contentmapper/contentmapper.go b/tsc/internal/contentmapper/contentmapper.go index d799a4d517106..158e6ee4be1b5 100644 --- a/tsc/internal/contentmapper/contentmapper.go +++ b/tsc/internal/contentmapper/contentmapper.go @@ -61,12 +61,12 @@ type Mapper struct { ContributionID string `json:"-"` } -var supportedVirtualExtensions = collections.NewSetFromItems( - ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".json", -) - func IsSupportedVirtualExtension(extension string) bool { - return supportedVirtualExtensions.Has(extension) + switch extension { + case ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".json": + return true + } + return false } func IsValidExtension(extension string) bool { diff --git a/tsc/internal/execute/build/buildtask.go b/tsc/internal/execute/build/buildtask.go index bd3cddee85c25..c31d37a094c69 100644 --- a/tsc/internal/execute/build/buildtask.go +++ b/tsc/internal/execute/build/buildtask.go @@ -2,7 +2,6 @@ package build import ( "fmt" - "iter" "slices" "strconv" "strings" @@ -491,7 +490,7 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp if seenRoots.Has(inputPath) || resolvedRoots.Has(inputPath) { continue } - if isContentMapperSupplementalBuildInfoPath(inputPath, getBuildInfoRootInfoReader().Roots()) && + if isContentMapperVirtualFilePath(inputPath, getBuildInfoRootInfoReader()) && !orchestrator.host.FS().FileExists(inputFileName) { continue } @@ -620,21 +619,25 @@ func (t *BuildTask) getUpToDateStatus(orchestrator *Orchestrator, configPath tsp } } -func isContentMapperSupplementalBuildInfoPath(inputPath tspath.PathKey, roots iter.Seq[tspath.PathKey]) bool { - for root := range roots { - suffix, ok := strings.CutPrefix(string(inputPath), string(root)+".") - if !ok { - continue - } - index, extension, ok := strings.Cut(suffix, ".") - if !ok || extension == "" { - continue - } - if _, err := strconv.Atoi(index); err == nil && contentmapper.IsSupportedVirtualExtension("."+extension) { - return true - } +func isContentMapperVirtualFilePath(inputPath tspath.PathKey, roots *incremental.BuildInfoRootInfoReader) bool { + extension := inputPath.Extension() + if !contentmapper.IsSupportedVirtualExtension(extension) { + return false } - return false + + pathWithoutExtension := tspath.RemoveExtension(inputPath.AsString(), extension) + rootName, index, ok := strings.CutLast(pathWithoutExtension, ".") + if !ok { + return false + } + + rootPath, ok := tspath.TryPathKeyFromCanonical(rootName) + if !ok || !roots.HasRoot(rootPath) { + return false + } + + _, err := strconv.Atoi(index) + return err == nil } func (t *BuildTask) reportUpToDateStatus(orchestrator *Orchestrator) { diff --git a/tsc/internal/execute/build/buildtask_contentmapper_test.go b/tsc/internal/execute/build/buildtask_contentmapper_test.go index 6c0be83180f01..c34203e71aaf0 100644 --- a/tsc/internal/execute/build/buildtask_contentmapper_test.go +++ b/tsc/internal/execute/build/buildtask_contentmapper_test.go @@ -1,20 +1,98 @@ package build import ( - "slices" + "strconv" "testing" + "github.com/microsoft/TypeScript/tsc/internal/execute/incremental" "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) -func TestIsContentMapperSupplementalBuildInfoPath(t *testing.T) { +func TestContentMapperVirtualFilePaths(t *testing.T) { t.Parallel() - roots := []tspath.PathKey{"/src/app.vue", "/src/index.ts"} + buildInfo := &incremental.BuildInfo{ + Root: []*incremental.BuildInfoRoot{ + {NonIncremental: "/src/a.vue"}, + {NonIncremental: "/src/b.ts"}, + {NonIncremental: "/src/a.vue.0"}, + }, + } + roots := buildInfo.GetBuildInfoRootInfoReader("/", tspath.CaseSensitive) - assert.Assert(t, isContentMapperSupplementalBuildInfoPath("/src/app.vue.0.ts", slices.Values(roots))) - assert.Assert(t, isContentMapperSupplementalBuildInfoPath("/src/app.vue.12.mts", slices.Values(roots))) - assert.Assert(t, !isContentMapperSupplementalBuildInfoPath("/src/app.vue.ts", slices.Values(roots))) - assert.Assert(t, !isContentMapperSupplementalBuildInfoPath("/src/app.vue.0.txt", slices.Values(roots))) - assert.Assert(t, !isContentMapperSupplementalBuildInfoPath("/src/other.vue.0.ts", slices.Values(roots))) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.0.ts", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.1.tsx", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.12.mts", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.2.cts", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.0.js", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.0.jsx", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.0.mjs", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.0.cjs", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.0.json", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/b.ts.0.ts", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.0.1.ts", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.+1.ts", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.-1.ts", roots)) + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.001.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.0.txt", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.0.d.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.0.d.mts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.0.d.cts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.0", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue..ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.x.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.99999999999999999999.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vue.1.0.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/b.vue.0.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.vuex.0.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/a.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/.0.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/..0.ts", roots)) +} + +func TestContentMapperVirtualFilePathsUseOriginalRoots(t *testing.T) { + t.Parallel() + buildInfo := &incremental.BuildInfo{ + FileNames: []incremental.BuildInfoPath{"./src/a.vue", "./src/b.vue"}, + Root: []*incremental.BuildInfoRoot{{Start: 2}}, + ResolvedRoot: []*incremental.BuildInfoResolvedRoot{{Resolved: 2, Root: 1}}, + } + roots := buildInfo.GetBuildInfoRootInfoReader("/", tspath.CaseSensitive) + + assert.Assert(t, isContentMapperVirtualFilePath("/src/a.vue.0.ts", roots)) + assert.Assert(t, !isContentMapperVirtualFilePath("/src/b.vue.0.ts", roots)) +} + +func BenchmarkContentMapperVirtualFilePaths(b *testing.B) { + for _, count := range []int{100, 1000, 10000} { + buildInfo := &incremental.BuildInfo{} + for i := range count { + buildInfo.Root = append(buildInfo.Root, &incremental.BuildInfoRoot{ + NonIncremental: incremental.BuildInfoPath("/project/packages/a/src/components/a" + strconv.Itoa(i) + ".vue"), + }) + } + roots := buildInfo.GetBuildInfoRootInfoReader("/", tspath.CaseSensitive) + b.Run("roots="+strconv.Itoa(count), func(b *testing.B) { + b.Run("declarationFile", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + isContentMapperVirtualFilePath("/project/node_modules/a/b.d.ts", roots) + } + }) + b.Run("sourceFile", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + isContentMapperVirtualFilePath("/project/node_modules/a/b.js", roots) + } + }) + b.Run("virtualFile", func(b *testing.B) { + path := tspath.PathKeyFromCanonical(buildInfo.Root[count-1].NonIncremental.AsString() + ".0.ts") + b.ReportAllocs() + for b.Loop() { + isContentMapperVirtualFilePath(path, roots) + } + }) + }) + } } diff --git a/tsc/internal/execute/incremental/buildInfo.go b/tsc/internal/execute/incremental/buildInfo.go index 4e248c77a64b7..c0990ef457525 100644 --- a/tsc/internal/execute/incremental/buildInfo.go +++ b/tsc/internal/execute/incremental/buildInfo.go @@ -687,6 +687,10 @@ func (b *BuildInfoRootInfoReader) Roots() iter.Seq[tspath.PathKey] { return b.rootToResolved.Keys() } +func (b *BuildInfoRootInfoReader) HasRoot(path tspath.PathKey) bool { + return b.rootToResolved.Has(path) +} + func (b *BuildInfoRootInfoReader) RootFileName(path tspath.PathKey) tspath.RootedFilePath { fileName, ok := b.rootFileNames[path] if !ok {