From 73b02d83bb52addd86c9d6967f9255ca8475498e Mon Sep 17 00:00:00 2001 From: no-yan <63000297+no-yan@users.noreply.github.com> Date: Sun, 16 Aug 2026 11:22:14 +0900 Subject: [PATCH] Add fast paths to CombinePaths and defer path canonicalization in checkSourceFilesBelongToPath CombinePaths returned via a string builder even when the last argument is already slash-rooted (the common case for ContainsPath and module resolution callers); return it directly instead. Drive-letter and URL-rooted last arguments still take the general case, which is kept byte-for-byte unchanged as combinePathsSlow and doubles as the differential oracle for the exhaustive test. checkSourceFilesBelongToPath computed the canonical absolute path for every file even though it is only used in the error branch. Co-Authored-By: Claude Fable 5 --- tsc/internal/compiler/program.go | 2 +- tsc/internal/tspath/combinepaths_diff_test.go | 38 +++++++++++++++++++ tsc/internal/tspath/path.go | 19 +++++++++- tsc/internal/tspath/path_test.go | 2 + 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 tsc/internal/tspath/combinepaths_diff_test.go diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index 5b05af6697d04..ea03ef81339e7 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -1746,8 +1746,8 @@ func (p *Program) CommonSourceDirectory() string { func (p *Program) checkSourceFilesBelongToPath(sourceFiles []string, rootDirectory string) bool { allFilesBelongToPath := true for _, file := range sourceFiles { - absoluteSourceFilePath := tspath.GetCanonicalFileName(tspath.GetNormalizedAbsolutePath(file, p.GetCurrentDirectory()), p.UseCaseSensitiveFileNames()) if !tspath.ContainsPath(rootDirectory, file, p.comparePathsOptions) { + absoluteSourceFilePath := tspath.GetCanonicalFileName(tspath.GetNormalizedAbsolutePath(file, p.GetCurrentDirectory()), p.UseCaseSensitiveFileNames()) p.includeProcessor.addProcessingDiagnostic(&processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, data: &includeExplainingDiagnostic{ diff --git a/tsc/internal/tspath/combinepaths_diff_test.go b/tsc/internal/tspath/combinepaths_diff_test.go new file mode 100644 index 0000000000000..642c92a5a05e7 --- /dev/null +++ b/tsc/internal/tspath/combinepaths_diff_test.go @@ -0,0 +1,38 @@ +package tspath + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +// CombinePaths' fast paths must be pure shortcuts of combinePathsSlow, which is +// the unmodified pre-fast-path implementation. This test compares the two +// directly over all combinations of representative path pieces. + +var combinePathsPieces = []string{ + "", "/", "a", "a/b", "/abs", "/abs/child", "c:/drive", "c:relative", + "..", "../up", ".", "./same", "file:///url", "with\\backslash", "\\\\server\\share", + "trailing/", "/trailing/", "//", +} + +func TestCombinePathsMatchesSlowPath(t *testing.T) { + t.Parallel() + + // All (first), (first, p1), and (first, p1, p2) combinations. + for _, first := range combinePathsPieces { + got := CombinePaths(first) + want := combinePathsSlow(first, nil) + assert.Equal(t, got, want, "CombinePaths(%q)", first) + for _, p1 := range combinePathsPieces { + got := CombinePaths(first, p1) + want := combinePathsSlow(first, []string{p1}) + assert.Equal(t, got, want, "CombinePaths(%q, %q)", first, p1) + for _, p2 := range combinePathsPieces { + got := CombinePaths(first, p1, p2) + want := combinePathsSlow(first, []string{p1, p2}) + assert.Equal(t, got, want, "CombinePaths(%q, %q, %q)", first, p1, p2) + } + } + } +} diff --git a/tsc/internal/tspath/path.go b/tsc/internal/tspath/path.go index 8b464c6752484..29c2172f1d2da 100644 --- a/tsc/internal/tspath/path.go +++ b/tsc/internal/tspath/path.go @@ -89,8 +89,23 @@ func HasTrailingDirectorySeparator(path string) bool { // CombinePaths("file:///path", "file:///to", "file.ext") === "file:///to/file.ext" // ``` func CombinePaths(firstPath string, paths ...string) string { - // TODO (drosen): There is potential for a fast path here. - // In the case where we find the last absolute path and just path.Join from there. + // Fast paths: no trailing paths, or the last trailing path is slash-rooted + // and supersedes everything before it. NormalizeSlashes does not allocate unless + // the path contains backslashes. + if len(paths) == 0 { + return NormalizeSlashes(firstPath) + } + // Only recognize slash-rooted paths here to keep the check cheap; + // drive-letter and URL-rooted paths take the general path below. + if p := paths[len(paths)-1]; p != "" && p[0] == '/' && strings.IndexByte(p, '\\') < 0 { + return p + } + // The general case is kept byte-for-byte unchanged as combinePathsSlow, + // which also serves as the differential-test oracle for the fast paths. + return combinePathsSlow(firstPath, paths) +} + +func combinePathsSlow(firstPath string, paths []string) string { firstPath = NormalizeSlashes(firstPath) var b strings.Builder diff --git a/tsc/internal/tspath/path_test.go b/tsc/internal/tspath/path_test.go index bf99924ac7ce0..0d7e7627f5f72 100644 --- a/tsc/internal/tspath/path_test.go +++ b/tsc/internal/tspath/path_test.go @@ -265,6 +265,8 @@ func BenchmarkCombinePaths(b *testing.B) { {"/path", "/to", "file.ext"}, {"c:/path", "to", "file.ext"}, {"file:///path", "to", "file.ext"}, + {"/path", "/abs/to/file.ext"}, + {"/lone/path/to/file.ext"}, } for _, test := range tests {