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 {