Skip to content

Commit e973805

Browse files
authored
Write path normalization without array allocations (#60812)
1 parent e1cef5f commit e973805

File tree

2 files changed

+121
-6
lines changed

2 files changed

+121
-6
lines changed

Diff for: src/compiler/path.ts

+106-6
Original file line numberDiff line numberDiff line change
@@ -624,28 +624,128 @@ export function getNormalizedPathComponents(path: string, currentDirectory: stri
624624
}
625625

626626
/** @internal */
627-
export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined): string {
628-
return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory));
627+
export function getNormalizedAbsolutePath(path: string, currentDirectory: string | undefined): string {
628+
let rootLength = getRootLength(path);
629+
if (rootLength === 0 && currentDirectory) {
630+
path = combinePaths(currentDirectory, path);
631+
rootLength = getRootLength(path);
632+
}
633+
else {
634+
// combinePaths normalizes slashes, so not necessary in the other branch
635+
path = normalizeSlashes(path);
636+
}
637+
638+
const simpleNormalized = simpleNormalizePath(path);
639+
if (simpleNormalized !== undefined) {
640+
return simpleNormalized.length > rootLength ? removeTrailingDirectorySeparator(simpleNormalized) : simpleNormalized;
641+
}
642+
643+
const length = path.length;
644+
const root = path.substring(0, rootLength);
645+
// `normalized` is only initialized once `path` is determined to be non-normalized
646+
let normalized;
647+
let index = rootLength;
648+
let segmentStart = index;
649+
let normalizedUpTo = index;
650+
let seenNonDotDotSegment = rootLength !== 0;
651+
while (index < length) {
652+
// At beginning of segment
653+
segmentStart = index;
654+
let ch = path.charCodeAt(index);
655+
while (ch === CharacterCodes.slash && index + 1 < length) {
656+
index++;
657+
ch = path.charCodeAt(index);
658+
}
659+
if (index > segmentStart) {
660+
// Seen superfluous separator
661+
normalized ??= path.substring(0, segmentStart - 1);
662+
segmentStart = index;
663+
}
664+
// Past any superfluous separators
665+
let segmentEnd = path.indexOf(directorySeparator, index + 1);
666+
if (segmentEnd === -1) {
667+
segmentEnd = length;
668+
}
669+
const segmentLength = segmentEnd - segmentStart;
670+
if (segmentLength === 1 && path.charCodeAt(index) === CharacterCodes.dot) {
671+
// "." segment (skip)
672+
normalized ??= path.substring(0, normalizedUpTo);
673+
}
674+
else if (segmentLength === 2 && path.charCodeAt(index) === CharacterCodes.dot && path.charCodeAt(index + 1) === CharacterCodes.dot) {
675+
// ".." segment
676+
if (!seenNonDotDotSegment) {
677+
if (normalized !== undefined) {
678+
normalized += normalized.length === rootLength ? ".." : "/..";
679+
}
680+
else {
681+
normalizedUpTo = index + 2;
682+
}
683+
}
684+
else if (normalized === undefined) {
685+
if (normalizedUpTo - 2 >= 0) {
686+
normalized = path.substring(0, Math.max(rootLength, path.lastIndexOf(directorySeparator, normalizedUpTo - 2)));
687+
}
688+
else {
689+
normalized = path.substring(0, normalizedUpTo);
690+
}
691+
}
692+
else {
693+
const lastSlash = normalized.lastIndexOf(directorySeparator);
694+
if (lastSlash !== -1) {
695+
normalized = normalized.substring(0, Math.max(rootLength, lastSlash));
696+
}
697+
else {
698+
normalized = root;
699+
}
700+
if (normalized.length === rootLength) {
701+
seenNonDotDotSegment = rootLength !== 0;
702+
}
703+
}
704+
}
705+
else if (normalized !== undefined) {
706+
if (normalized.length !== rootLength) {
707+
normalized += directorySeparator;
708+
}
709+
seenNonDotDotSegment = true;
710+
normalized += path.substring(segmentStart, segmentEnd);
711+
}
712+
else {
713+
seenNonDotDotSegment = true;
714+
normalizedUpTo = segmentEnd;
715+
}
716+
index = segmentEnd + 1;
717+
}
718+
return normalized ?? (length > rootLength ? removeTrailingDirectorySeparator(path) : path);
629719
}
630720

631721
/** @internal */
632722
export function normalizePath(path: string): string {
633723
path = normalizeSlashes(path);
724+
let normalized = simpleNormalizePath(path);
725+
if (normalized !== undefined) {
726+
return normalized;
727+
}
728+
normalized = getNormalizedAbsolutePath(path, "");
729+
return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized;
730+
}
731+
732+
function simpleNormalizePath(path: string): string | undefined {
634733
// Most paths don't require normalization
635734
if (!relativePathSegmentRegExp.test(path)) {
636735
return path;
637736
}
638737
// Some paths only require cleanup of `/./` or leading `./`
639-
const simplified = path.replace(/\/\.\//g, "/").replace(/^\.\//, "");
738+
let simplified = path.replace(/\/\.\//g, "/");
739+
if (simplified.startsWith("./")) {
740+
simplified = simplified.slice(2);
741+
}
640742
if (simplified !== path) {
641743
path = simplified;
642744
if (!relativePathSegmentRegExp.test(path)) {
643745
return path;
644746
}
645747
}
646-
// Other paths require full normalization
647-
const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(path)));
648-
return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized;
748+
return undefined;
649749
}
650750

651751
function getPathWithoutRoot(pathComponents: readonly string[]) {

Diff for: src/testRunner/unittests/paths.ts

+15
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,24 @@ describe("unittests:: core paths", () => {
317317
assert.strictEqual(ts.getNormalizedAbsolutePath("", ""), "");
318318
assert.strictEqual(ts.getNormalizedAbsolutePath(".", ""), "");
319319
assert.strictEqual(ts.getNormalizedAbsolutePath("./", ""), "");
320+
assert.strictEqual(ts.getNormalizedAbsolutePath("./a", ""), "a");
320321
// Strangely, these do not normalize to the empty string.
321322
assert.strictEqual(ts.getNormalizedAbsolutePath("..", ""), "..");
322323
assert.strictEqual(ts.getNormalizedAbsolutePath("../", ""), "..");
324+
assert.strictEqual(ts.getNormalizedAbsolutePath("../..", ""), "../..");
325+
assert.strictEqual(ts.getNormalizedAbsolutePath("../../", ""), "../..");
326+
assert.strictEqual(ts.getNormalizedAbsolutePath("./..", ""), "..");
327+
assert.strictEqual(ts.getNormalizedAbsolutePath("../../a/..", ""), "../..");
328+
329+
// More .. segments
330+
assert.strictEqual(ts.getNormalizedAbsolutePath("src/ts/foo/../../../bar/bar.ts", ""), "bar/bar.ts");
331+
assert.strictEqual(ts.getNormalizedAbsolutePath("src/ts/foo/../../..", ""), "");
332+
// not a real URL root!
333+
assert.strictEqual(ts.getNormalizedAbsolutePath("file:/Users/matb/projects/san/../../../../../../typings/@epic/Core.d.ts", ""), "../typings/@epic/Core.d.ts");
334+
// the root is `file://Users/`
335+
assert.strictEqual(ts.getNormalizedAbsolutePath("file://Users/matb/projects/san/../../../../../../typings/@epic/Core.d.ts", ""), "file://Users/typings/@epic/Core.d.ts");
336+
// this is real
337+
assert.strictEqual(ts.getNormalizedAbsolutePath("file:///Users/matb/projects/san/../../../../../../typings/@epic/Core.d.ts", ""), "file:///typings/@epic/Core.d.ts");
323338

324339
// Interaction between relative paths and currentDirectory.
325340
assert.strictEqual(ts.getNormalizedAbsolutePath("", "/home"), "/home");

0 commit comments

Comments
 (0)