• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2    CharacterCodes,
3    compareStringsCaseInsensitive,
4    compareStringsCaseSensitive,
5    compareValues,
6    Comparison,
7    Debug,
8    endsWith,
9    equateStringsCaseInsensitive,
10    equateStringsCaseSensitive,
11    GetCanonicalFileName,
12    getStringComparer,
13    identity,
14    lastOrUndefined,
15    Path,
16    some,
17    startsWith,
18    stringContains,
19} from "./_namespaces/ts";
20
21/**
22 * Internally, we represent paths as strings with '/' as the directory separator.
23 * When we make system calls (eg: LanguageServiceHost.getDirectory()),
24 * we expect the host to correctly handle paths in our specified format.
25 *
26 * @internal
27 */
28export const directorySeparator = "/";
29/** @internal */
30export const altDirectorySeparator = "\\";
31const urlSchemeSeparator = "://";
32const backslashRegExp = /\\/g;
33
34//// Path Tests
35
36/**
37 * Determines whether a charCode corresponds to `/` or `\`.
38 *
39 * @internal
40 */
41export function isAnyDirectorySeparator(charCode: number): boolean {
42    return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash;
43}
44
45/**
46 * Determines whether a path starts with a URL scheme (e.g. starts with `http://`, `ftp://`, `file://`, etc.).
47 *
48 * @internal
49 */
50export function isUrl(path: string) {
51    return getEncodedRootLength(path) < 0;
52}
53
54/**
55 * Determines whether a path is an absolute disk path (e.g. starts with `/`, or a dos path
56 * like `c:`, `c:\` or `c:/`).
57 *
58 * @internal
59 */
60export function isRootedDiskPath(path: string) {
61    return getEncodedRootLength(path) > 0;
62}
63
64/**
65 * Determines whether a path consists only of a path root.
66 *
67 * @internal
68 */
69export function isDiskPathRoot(path: string) {
70    const rootLength = getEncodedRootLength(path);
71    return rootLength > 0 && rootLength === path.length;
72}
73
74/**
75 * Determines whether a path starts with an absolute path component (i.e. `/`, `c:/`, `file://`, etc.).
76 *
77 * ```ts
78 * // POSIX
79 * pathIsAbsolute("/path/to/file.ext") === true
80 * // DOS
81 * pathIsAbsolute("c:/path/to/file.ext") === true
82 * // URL
83 * pathIsAbsolute("file:///path/to/file.ext") === true
84 * // Non-absolute
85 * pathIsAbsolute("path/to/file.ext") === false
86 * pathIsAbsolute("./path/to/file.ext") === false
87 * ```
88 *
89 * @internal
90 */
91export function pathIsAbsolute(path: string): boolean {
92    return getEncodedRootLength(path) !== 0;
93}
94
95/**
96 * Determines whether a path starts with a relative path component (i.e. `.` or `..`).
97 *
98 * @internal
99 */
100export function pathIsRelative(path: string): boolean {
101    return /^\.\.?($|[\\/])/.test(path);
102}
103
104/**
105 * Determines whether a path is neither relative nor absolute, e.g. "path/to/file".
106 * Also known misleadingly as "non-relative".
107 *
108 * @internal
109 */
110export function pathIsBareSpecifier(path: string): boolean {
111    return !pathIsAbsolute(path) && !pathIsRelative(path);
112}
113
114/** @internal */
115export function hasExtension(fileName: string): boolean {
116    return stringContains(getBaseFileName(fileName), ".");
117}
118
119/** @internal */
120export function fileExtensionIs(path: string, extension: string): boolean {
121    return path.length > extension.length && endsWith(path, extension);
122}
123
124/** @internal */
125export function fileExtensionIsOneOf(path: string, extensions: readonly string[]): boolean {
126    for (const extension of extensions) {
127        if (fileExtensionIs(path, extension)) {
128            return true;
129        }
130    }
131
132    return false;
133}
134
135/**
136 * Determines whether a path has a trailing separator (`/` or `\\`).
137 *
138 * @internal
139 */
140export function hasTrailingDirectorySeparator(path: string) {
141    return path.length > 0 && isAnyDirectorySeparator(path.charCodeAt(path.length - 1));
142}
143
144//// Path Parsing
145
146function isVolumeCharacter(charCode: number) {
147    return (charCode >= CharacterCodes.a && charCode <= CharacterCodes.z) ||
148        (charCode >= CharacterCodes.A && charCode <= CharacterCodes.Z);
149}
150
151function getFileUrlVolumeSeparatorEnd(url: string, start: number) {
152    const ch0 = url.charCodeAt(start);
153    if (ch0 === CharacterCodes.colon) return start + 1;
154    if (ch0 === CharacterCodes.percent && url.charCodeAt(start + 1) === CharacterCodes._3) {
155        const ch2 = url.charCodeAt(start + 2);
156        if (ch2 === CharacterCodes.a || ch2 === CharacterCodes.A) return start + 3;
157    }
158    return -1;
159}
160
161/**
162 * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files").
163 * If the root is part of a URL, the twos-complement of the root length is returned.
164 */
165function getEncodedRootLength(path: string): number {
166    if (!path) return 0;
167    const ch0 = path.charCodeAt(0);
168
169    // POSIX or UNC
170    if (ch0 === CharacterCodes.slash || ch0 === CharacterCodes.backslash) {
171        if (path.charCodeAt(1) !== ch0) return 1; // POSIX: "/" (or non-normalized "\")
172
173        const p1 = path.indexOf(ch0 === CharacterCodes.slash ? directorySeparator : altDirectorySeparator, 2);
174        if (p1 < 0) return path.length; // UNC: "//server" or "\\server"
175
176        return p1 + 1; // UNC: "//server/" or "\\server\"
177    }
178
179    // DOS
180    if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodes.colon) {
181        const ch2 = path.charCodeAt(2);
182        if (ch2 === CharacterCodes.slash || ch2 === CharacterCodes.backslash) return 3; // DOS: "c:/" or "c:\"
183        if (path.length === 2) return 2; // DOS: "c:" (but not "c:d")
184    }
185
186    // URL
187    const schemeEnd = path.indexOf(urlSchemeSeparator);
188    if (schemeEnd !== -1) {
189        const authorityStart = schemeEnd + urlSchemeSeparator.length;
190        const authorityEnd = path.indexOf(directorySeparator, authorityStart);
191        if (authorityEnd !== -1) { // URL: "file:///", "file://server/", "file://server/path"
192            // For local "file" URLs, include the leading DOS volume (if present).
193            // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a
194            // special case interpreted as "the machine from which the URL is being interpreted".
195            const scheme = path.slice(0, schemeEnd);
196            const authority = path.slice(authorityStart, authorityEnd);
197            if (scheme === "file" && (authority === "" || authority === "localhost") &&
198                isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) {
199                const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2);
200                if (volumeSeparatorEnd !== -1) {
201                    if (path.charCodeAt(volumeSeparatorEnd) === CharacterCodes.slash) {
202                        // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/"
203                        return ~(volumeSeparatorEnd + 1);
204                    }
205                    if (volumeSeparatorEnd === path.length) {
206                        // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a"
207                        // but not "file:///c:d" or "file:///c%3ad"
208                        return ~volumeSeparatorEnd;
209                    }
210                }
211            }
212            return ~(authorityEnd + 1); // URL: "file://server/", "http://server/"
213        }
214        return ~path.length; // URL: "file://server", "http://server"
215    }
216
217    // relative
218    return 0;
219}
220
221/**
222 * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files").
223 *
224 * For example:
225 * ```ts
226 * getRootLength("a") === 0                   // ""
227 * getRootLength("/") === 1                   // "/"
228 * getRootLength("c:") === 2                  // "c:"
229 * getRootLength("c:d") === 0                 // ""
230 * getRootLength("c:/") === 3                 // "c:/"
231 * getRootLength("c:\\") === 3                // "c:\\"
232 * getRootLength("//server") === 7            // "//server"
233 * getRootLength("//server/share") === 8      // "//server/"
234 * getRootLength("\\\\server") === 7          // "\\\\server"
235 * getRootLength("\\\\server\\share") === 8   // "\\\\server\\"
236 * getRootLength("file:///path") === 8        // "file:///"
237 * getRootLength("file:///c:") === 10         // "file:///c:"
238 * getRootLength("file:///c:d") === 8         // "file:///"
239 * getRootLength("file:///c:/path") === 11    // "file:///c:/"
240 * getRootLength("file://server") === 13      // "file://server"
241 * getRootLength("file://server/path") === 14 // "file://server/"
242 * getRootLength("http://server") === 13      // "http://server"
243 * getRootLength("http://server/path") === 14 // "http://server/"
244 * ```
245 *
246 * @internal
247 */
248export function getRootLength(path: string) {
249    const rootLength = getEncodedRootLength(path);
250    return rootLength < 0 ? ~rootLength : rootLength;
251}
252
253/**
254 * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname`
255 * except that we support URLs as well.
256 *
257 * ```ts
258 * // POSIX
259 * getDirectoryPath("/path/to/file.ext") === "/path/to"
260 * getDirectoryPath("/path/to/") === "/path"
261 * getDirectoryPath("/") === "/"
262 * // DOS
263 * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to"
264 * getDirectoryPath("c:/path/to/") === "c:/path"
265 * getDirectoryPath("c:/") === "c:/"
266 * getDirectoryPath("c:") === "c:"
267 * // URL
268 * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to"
269 * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path"
270 * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/"
271 * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org"
272 * ```
273 *
274 * @internal
275 */
276export function getDirectoryPath(path: Path): Path;
277/**
278 * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname`
279 * except that we support URLs as well.
280 *
281 * ```ts
282 * // POSIX
283 * getDirectoryPath("/path/to/file.ext") === "/path/to"
284 * getDirectoryPath("/path/to/") === "/path"
285 * getDirectoryPath("/") === "/"
286 * // DOS
287 * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to"
288 * getDirectoryPath("c:/path/to/") === "c:/path"
289 * getDirectoryPath("c:/") === "c:/"
290 * getDirectoryPath("c:") === "c:"
291 * // URL
292 * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to"
293 * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path"
294 * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/"
295 * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org"
296 * getDirectoryPath("file://server/path/to/file.ext") === "file://server/path/to"
297 * getDirectoryPath("file://server/path/to") === "file://server/path"
298 * getDirectoryPath("file://server/") === "file://server/"
299 * getDirectoryPath("file://server") === "file://server"
300 * getDirectoryPath("file:///path/to/file.ext") === "file:///path/to"
301 * getDirectoryPath("file:///path/to") === "file:///path"
302 * getDirectoryPath("file:///") === "file:///"
303 * getDirectoryPath("file://") === "file://"
304 * ```
305 *
306 * @internal
307 */
308export function getDirectoryPath(path: string): string;
309/** @internal */
310export function getDirectoryPath(path: string): string {
311    path = normalizeSlashes(path);
312
313    // If the path provided is itself the root, then return it.
314    const rootLength = getRootLength(path);
315    if (rootLength === path.length) return path;
316
317    // return the leading portion of the path up to the last (non-terminal) directory separator
318    // but not including any trailing directory separator.
319    path = removeTrailingDirectorySeparator(path);
320    return path.slice(0, Math.max(rootLength, path.lastIndexOf(directorySeparator)));
321}
322
323/**
324 * Returns the path except for its containing directory name.
325 * Semantics align with NodeJS's `path.basename` except that we support URL's as well.
326 *
327 * ```ts
328 * // POSIX
329 * getBaseFileName("/path/to/file.ext") === "file.ext"
330 * getBaseFileName("/path/to/") === "to"
331 * getBaseFileName("/") === ""
332 * // DOS
333 * getBaseFileName("c:/path/to/file.ext") === "file.ext"
334 * getBaseFileName("c:/path/to/") === "to"
335 * getBaseFileName("c:/") === ""
336 * getBaseFileName("c:") === ""
337 * // URL
338 * getBaseFileName("http://typescriptlang.org/path/to/file.ext") === "file.ext"
339 * getBaseFileName("http://typescriptlang.org/path/to/") === "to"
340 * getBaseFileName("http://typescriptlang.org/") === ""
341 * getBaseFileName("http://typescriptlang.org") === ""
342 * getBaseFileName("file://server/path/to/file.ext") === "file.ext"
343 * getBaseFileName("file://server/path/to/") === "to"
344 * getBaseFileName("file://server/") === ""
345 * getBaseFileName("file://server") === ""
346 * getBaseFileName("file:///path/to/file.ext") === "file.ext"
347 * getBaseFileName("file:///path/to/") === "to"
348 * getBaseFileName("file:///") === ""
349 * getBaseFileName("file://") === ""
350 * ```
351 *
352 * @internal
353 */
354export function getBaseFileName(path: string): string;
355/**
356 * Gets the portion of a path following the last (non-terminal) separator (`/`).
357 * Semantics align with NodeJS's `path.basename` except that we support URL's as well.
358 * If the base name has any one of the provided extensions, it is removed.
359 *
360 * ```ts
361 * getBaseFileName("/path/to/file.ext", ".ext", true) === "file"
362 * getBaseFileName("/path/to/file.js", ".ext", true) === "file.js"
363 * getBaseFileName("/path/to/file.js", [".ext", ".js"], true) === "file"
364 * getBaseFileName("/path/to/file.ext", ".EXT", false) === "file.ext"
365 * ```
366 *
367 * @internal
368 */
369export function getBaseFileName(path: string, extensions: string | readonly string[], ignoreCase: boolean): string;
370/** @internal */
371export function getBaseFileName(path: string, extensions?: string | readonly string[], ignoreCase?: boolean) {
372    path = normalizeSlashes(path);
373
374    // if the path provided is itself the root, then it has not file name.
375    const rootLength = getRootLength(path);
376    if (rootLength === path.length) return "";
377
378    // return the trailing portion of the path starting after the last (non-terminal) directory
379    // separator but not including any trailing directory separator.
380    path = removeTrailingDirectorySeparator(path);
381    const name = path.slice(Math.max(getRootLength(path), path.lastIndexOf(directorySeparator) + 1));
382    const extension = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(name, extensions, ignoreCase) : undefined;
383    return extension ? name.slice(0, name.length - extension.length) : name;
384}
385
386function tryGetExtensionFromPath(path: string, extension: string, stringEqualityComparer: (a: string, b: string) => boolean) {
387    if (!startsWith(extension, ".")) extension = "." + extension;
388    if (path.length >= extension.length && path.charCodeAt(path.length - extension.length) === CharacterCodes.dot) {
389        const pathExtension = path.slice(path.length - extension.length);
390        if (stringEqualityComparer(pathExtension, extension)) {
391            return pathExtension;
392        }
393    }
394}
395
396function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], stringEqualityComparer: (a: string, b: string) => boolean) {
397    if (typeof extensions === "string") {
398        return tryGetExtensionFromPath(path, extensions, stringEqualityComparer) || "";
399    }
400    for (const extension of extensions) {
401        const result = tryGetExtensionFromPath(path, extension, stringEqualityComparer);
402        if (result) return result;
403    }
404    return "";
405}
406
407/**
408 * Gets the file extension for a path.
409 *
410 * ```ts
411 * getAnyExtensionFromPath("/path/to/file.ext") === ".ext"
412 * getAnyExtensionFromPath("/path/to/file.ext/") === ".ext"
413 * getAnyExtensionFromPath("/path/to/file") === ""
414 * getAnyExtensionFromPath("/path/to.ext/file") === ""
415 * ```
416 *
417 * @internal
418 */
419export function getAnyExtensionFromPath(path: string): string;
420/**
421 * Gets the file extension for a path, provided it is one of the provided extensions.
422 *
423 * ```ts
424 * getAnyExtensionFromPath("/path/to/file.ext", ".ext", true) === ".ext"
425 * getAnyExtensionFromPath("/path/to/file.js", ".ext", true) === ""
426 * getAnyExtensionFromPath("/path/to/file.js", [".ext", ".js"], true) === ".js"
427 * getAnyExtensionFromPath("/path/to/file.ext", ".EXT", false) === ""
428 *
429 * @internal
430 */
431export function getAnyExtensionFromPath(path: string, extensions: string | readonly string[], ignoreCase: boolean): string;
432/** @internal */
433export function getAnyExtensionFromPath(path: string, extensions?: string | readonly string[], ignoreCase?: boolean): string {
434    // Retrieves any string from the final "." onwards from a base file name.
435    // Unlike extensionFromPath, which throws an exception on unrecognized extensions.
436    if (extensions) {
437        return getAnyExtensionFromPathWorker(removeTrailingDirectorySeparator(path), extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive);
438    }
439    const baseFileName = getBaseFileName(path);
440    const extensionIndex = baseFileName.lastIndexOf(".");
441    if (extensionIndex >= 0) {
442        return baseFileName.substring(extensionIndex);
443    }
444    return "";
445}
446
447function pathComponents(path: string, rootLength: number) {
448    const root = path.substring(0, rootLength);
449    const rest = path.substring(rootLength).split(directorySeparator);
450    if (rest.length && !lastOrUndefined(rest)) rest.pop();
451    return [root, ...rest];
452}
453
454/**
455 * Parse a path into an array containing a root component (at index 0) and zero or more path
456 * components (at indices > 0). The result is not normalized.
457 * If the path is relative, the root component is `""`.
458 * If the path is absolute, the root component includes the first path separator (`/`).
459 *
460 * ```ts
461 * // POSIX
462 * getPathComponents("/path/to/file.ext") === ["/", "path", "to", "file.ext"]
463 * getPathComponents("/path/to/") === ["/", "path", "to"]
464 * getPathComponents("/") === ["/"]
465 * // DOS
466 * getPathComponents("c:/path/to/file.ext") === ["c:/", "path", "to", "file.ext"]
467 * getPathComponents("c:/path/to/") === ["c:/", "path", "to"]
468 * getPathComponents("c:/") === ["c:/"]
469 * getPathComponents("c:") === ["c:"]
470 * // URL
471 * getPathComponents("http://typescriptlang.org/path/to/file.ext") === ["http://typescriptlang.org/", "path", "to", "file.ext"]
472 * getPathComponents("http://typescriptlang.org/path/to/") === ["http://typescriptlang.org/", "path", "to"]
473 * getPathComponents("http://typescriptlang.org/") === ["http://typescriptlang.org/"]
474 * getPathComponents("http://typescriptlang.org") === ["http://typescriptlang.org"]
475 * getPathComponents("file://server/path/to/file.ext") === ["file://server/", "path", "to", "file.ext"]
476 * getPathComponents("file://server/path/to/") === ["file://server/", "path", "to"]
477 * getPathComponents("file://server/") === ["file://server/"]
478 * getPathComponents("file://server") === ["file://server"]
479 * getPathComponents("file:///path/to/file.ext") === ["file:///", "path", "to", "file.ext"]
480 * getPathComponents("file:///path/to/") === ["file:///", "path", "to"]
481 * getPathComponents("file:///") === ["file:///"]
482 * getPathComponents("file://") === ["file://"]
483 *
484 * @internal
485 */
486export function getPathComponents(path: string, currentDirectory = "") {
487    path = combinePaths(currentDirectory, path);
488    return pathComponents(path, getRootLength(path));
489}
490
491//// Path Formatting
492
493/**
494 * Formats a parsed path consisting of a root component (at index 0) and zero or more path
495 * segments (at indices > 0).
496 *
497 * ```ts
498 * getPathFromPathComponents(["/", "path", "to", "file.ext"]) === "/path/to/file.ext"
499 * ```
500 *
501 * @internal
502 */
503export function getPathFromPathComponents(pathComponents: readonly string[]) {
504    if (pathComponents.length === 0) return "";
505
506    const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]);
507    return root + pathComponents.slice(1).join(directorySeparator);
508}
509
510//// Path Normalization
511
512/**
513 * Normalize path separators, converting `\` into `/`.
514 *
515 * @internal
516 */
517export function normalizeSlashes(path: string): string {
518    return path.indexOf("\\") !== -1
519        ? path.replace(backslashRegExp, directorySeparator)
520        : path;
521}
522
523/**
524 * Reduce an array of path components to a more simplified path by navigating any
525 * `"."` or `".."` entries in the path.
526 *
527 * @internal
528 */
529export function reducePathComponents(components: readonly string[]) {
530    if (!some(components)) return [];
531    const reduced = [components[0]];
532    for (let i = 1; i < components.length; i++) {
533        const component = components[i];
534        if (!component) continue;
535        if (component === ".") continue;
536        if (component === "..") {
537            if (reduced.length > 1) {
538                if (reduced[reduced.length - 1] !== "..") {
539                    reduced.pop();
540                    continue;
541                }
542            }
543            else if (reduced[0]) continue;
544        }
545        reduced.push(component);
546    }
547    return reduced;
548}
549
550/**
551 * Combines paths. If a path is absolute, it replaces any previous path. Relative paths are not simplified.
552 *
553 * ```ts
554 * // Non-rooted
555 * combinePaths("path", "to", "file.ext") === "path/to/file.ext"
556 * combinePaths("path", "dir", "..", "to", "file.ext") === "path/dir/../to/file.ext"
557 * // POSIX
558 * combinePaths("/path", "to", "file.ext") === "/path/to/file.ext"
559 * combinePaths("/path", "/to", "file.ext") === "/to/file.ext"
560 * // DOS
561 * combinePaths("c:/path", "to", "file.ext") === "c:/path/to/file.ext"
562 * combinePaths("c:/path", "c:/to", "file.ext") === "c:/to/file.ext"
563 * // URL
564 * combinePaths("file:///path", "to", "file.ext") === "file:///path/to/file.ext"
565 * combinePaths("file:///path", "file:///to", "file.ext") === "file:///to/file.ext"
566 * ```
567 *
568 * @internal
569 */
570export function combinePaths(path: string, ...paths: (string | undefined)[]): string {
571    if (path) path = normalizeSlashes(path);
572    for (let relativePath of paths) {
573        if (!relativePath) continue;
574        relativePath = normalizeSlashes(relativePath);
575        if (!path || getRootLength(relativePath) !== 0) {
576            path = relativePath;
577        }
578        else {
579            path = ensureTrailingDirectorySeparator(path) + relativePath;
580        }
581    }
582    return path;
583}
584
585/**
586 * Combines and resolves paths. If a path is absolute, it replaces any previous path. Any
587 * `.` and `..` path components are resolved. Trailing directory separators are preserved.
588 *
589 * ```ts
590 * resolvePath("/path", "to", "file.ext") === "path/to/file.ext"
591 * resolvePath("/path", "to", "file.ext/") === "path/to/file.ext/"
592 * resolvePath("/path", "dir", "..", "to", "file.ext") === "path/to/file.ext"
593 * ```
594 *
595 * @internal
596 */
597export function resolvePath(path: string, ...paths: (string | undefined)[]): string {
598    return normalizePath(some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path));
599}
600
601/**
602 * Parse a path into an array containing a root component (at index 0) and zero or more path
603 * components (at indices > 0). The result is normalized.
604 * If the path is relative, the root component is `""`.
605 * If the path is absolute, the root component includes the first path separator (`/`).
606 *
607 * ```ts
608 * getNormalizedPathComponents("to/dir/../file.ext", "/path/") === ["/", "path", "to", "file.ext"]
609 * ```
610 *
611 * @internal
612 */
613export function getNormalizedPathComponents(path: string, currentDirectory: string | undefined) {
614    return reducePathComponents(getPathComponents(path, currentDirectory));
615}
616
617/** @internal */
618export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined) {
619    return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory));
620}
621
622/** @internal */
623export function normalizePath(path: string): string {
624    path = normalizeSlashes(path);
625    // Most paths don't require normalization
626    if (!relativePathSegmentRegExp.test(path)) {
627        return path;
628    }
629    // Some paths only require cleanup of `/./` or leading `./`
630    const simplified = path.replace(/\/\.\//g, "/").replace(/^\.\//, "");
631    if (simplified !== path) {
632        path = simplified;
633        if (!relativePathSegmentRegExp.test(path)) {
634            return path;
635        }
636    }
637    // Other paths require full normalization
638    const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(path)));
639    return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized;
640}
641
642function getPathWithoutRoot(pathComponents: readonly string[]) {
643    if (pathComponents.length === 0) return "";
644    return pathComponents.slice(1).join(directorySeparator);
645}
646
647/** @internal */
648export function getNormalizedAbsolutePathWithoutRoot(fileName: string, currentDirectory: string | undefined) {
649    return getPathWithoutRoot(getNormalizedPathComponents(fileName, currentDirectory));
650}
651
652/** @internal */
653export function toPath(fileName: string, basePath: string | undefined, getCanonicalFileName: (path: string) => string): Path {
654    const nonCanonicalizedPath = isRootedDiskPath(fileName)
655        ? normalizePath(fileName)
656        : getNormalizedAbsolutePath(fileName, basePath);
657    return getCanonicalFileName(nonCanonicalizedPath) as Path;
658}
659
660//// Path Mutation
661
662/**
663 * Removes a trailing directory separator from a path, if it does not already have one.
664 *
665 * ```ts
666 * removeTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext"
667 * removeTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext"
668 * ```
669 *
670 * @internal
671 */
672export function removeTrailingDirectorySeparator(path: Path): Path;
673/** @internal */
674export function removeTrailingDirectorySeparator(path: string): string;
675/** @internal */
676export function removeTrailingDirectorySeparator(path: string) {
677    if (hasTrailingDirectorySeparator(path)) {
678        return path.substr(0, path.length - 1);
679    }
680
681    return path;
682}
683
684/**
685 * Adds a trailing directory separator to a path, if it does not already have one.
686 *
687 * ```ts
688 * ensureTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext/"
689 * ensureTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext/"
690 * ```
691 *
692 * @internal
693 */
694export function ensureTrailingDirectorySeparator(path: Path): Path;
695/** @internal */
696export function ensureTrailingDirectorySeparator(path: string): string;
697/** @internal */
698export function ensureTrailingDirectorySeparator(path: string) {
699    if (!hasTrailingDirectorySeparator(path)) {
700        return path + directorySeparator;
701    }
702
703    return path;
704}
705
706/**
707 * Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed
708 * with `./` or `../`) so as not to be confused with an unprefixed module name.
709 *
710 * ```ts
711 * ensurePathIsNonModuleName("/path/to/file.ext") === "/path/to/file.ext"
712 * ensurePathIsNonModuleName("./path/to/file.ext") === "./path/to/file.ext"
713 * ensurePathIsNonModuleName("../path/to/file.ext") === "../path/to/file.ext"
714 * ensurePathIsNonModuleName("path/to/file.ext") === "./path/to/file.ext"
715 * ```
716 *
717 * @internal
718 */
719export function ensurePathIsNonModuleName(path: string): string {
720    return !pathIsAbsolute(path) && !pathIsRelative(path) ? "./" + path : path;
721}
722
723/**
724 * Changes the extension of a path to the provided extension.
725 *
726 * ```ts
727 * changeAnyExtension("/path/to/file.ext", ".js") === "/path/to/file.js"
728 * ```
729 *
730 * @internal
731 */
732export function changeAnyExtension(path: string, ext: string): string;
733/**
734 * Changes the extension of a path to the provided extension if it has one of the provided extensions.
735 *
736 * ```ts
737 * changeAnyExtension("/path/to/file.ext", ".js", ".ext") === "/path/to/file.js"
738 * changeAnyExtension("/path/to/file.ext", ".js", ".ts") === "/path/to/file.ext"
739 * changeAnyExtension("/path/to/file.ext", ".js", [".ext", ".ts"]) === "/path/to/file.js"
740 * ```
741 *
742 * @internal
743 */
744export function changeAnyExtension(path: string, ext: string, extensions: string | readonly string[], ignoreCase: boolean): string;
745/** @internal */
746export function changeAnyExtension(path: string, ext: string, extensions?: string | readonly string[], ignoreCase?: boolean) {
747    const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path);
748    return pathext ? path.slice(0, path.length - pathext.length) + (startsWith(ext, ".") ? ext : "." + ext) : path;
749}
750
751//// Path Comparisons
752
753// check path for these segments: '', '.'. '..'
754const relativePathSegmentRegExp = /(?:\/\/)|(?:^|\/)\.\.?(?:$|\/)/;
755
756function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => Comparison) {
757    if (a === b) return Comparison.EqualTo;
758    if (a === undefined) return Comparison.LessThan;
759    if (b === undefined) return Comparison.GreaterThan;
760
761    // NOTE: Performance optimization - shortcut if the root segments differ as there would be no
762    //       need to perform path reduction.
763    const aRoot = a.substring(0, getRootLength(a));
764    const bRoot = b.substring(0, getRootLength(b));
765    const result = compareStringsCaseInsensitive(aRoot, bRoot);
766    if (result !== Comparison.EqualTo) {
767        return result;
768    }
769
770    // NOTE: Performance optimization - shortcut if there are no relative path segments in
771    //       the non-root portion of the path
772    const aRest = a.substring(aRoot.length);
773    const bRest = b.substring(bRoot.length);
774    if (!relativePathSegmentRegExp.test(aRest) && !relativePathSegmentRegExp.test(bRest)) {
775        return componentComparer(aRest, bRest);
776    }
777
778    // The path contains a relative path segment. Normalize the paths and perform a slower component
779    // by component comparison.
780    const aComponents = reducePathComponents(getPathComponents(a));
781    const bComponents = reducePathComponents(getPathComponents(b));
782    const sharedLength = Math.min(aComponents.length, bComponents.length);
783    for (let i = 1; i < sharedLength; i++) {
784        const result = componentComparer(aComponents[i], bComponents[i]);
785        if (result !== Comparison.EqualTo) {
786            return result;
787        }
788    }
789    return compareValues(aComponents.length, bComponents.length);
790}
791
792/**
793 * Performs a case-sensitive comparison of two paths. Path roots are always compared case-insensitively.
794 *
795 * @internal
796 */
797export function comparePathsCaseSensitive(a: string, b: string) {
798    return comparePathsWorker(a, b, compareStringsCaseSensitive);
799}
800
801/**
802 * Performs a case-insensitive comparison of two paths.
803 *
804 * @internal
805 */
806export function comparePathsCaseInsensitive(a: string, b: string) {
807    return comparePathsWorker(a, b, compareStringsCaseInsensitive);
808}
809
810/**
811 * Compare two paths using the provided case sensitivity.
812 *
813 * @internal
814 */
815export function comparePaths(a: string, b: string, ignoreCase?: boolean): Comparison;
816/** @internal */
817export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean): Comparison;
818/** @internal */
819export function comparePaths(a: string, b: string, currentDirectory?: string | boolean, ignoreCase?: boolean) {
820    if (typeof currentDirectory === "string") {
821        a = combinePaths(currentDirectory, a);
822        b = combinePaths(currentDirectory, b);
823    }
824    else if (typeof currentDirectory === "boolean") {
825        ignoreCase = currentDirectory;
826    }
827    return comparePathsWorker(a, b, getStringComparer(ignoreCase));
828}
829
830/**
831 * Determines whether a `parent` path contains a `child` path using the provide case sensitivity.
832 *
833 * @internal
834 */
835export function containsPath(parent: string, child: string, ignoreCase?: boolean): boolean;
836/** @internal */
837export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean): boolean;
838/** @internal */
839export function containsPath(parent: string, child: string, currentDirectory?: string | boolean, ignoreCase?: boolean) {
840    if (typeof currentDirectory === "string") {
841        parent = combinePaths(currentDirectory, parent);
842        child = combinePaths(currentDirectory, child);
843    }
844    else if (typeof currentDirectory === "boolean") {
845        ignoreCase = currentDirectory;
846    }
847    if (parent === undefined || child === undefined) return false;
848    if (parent === child) return true;
849    const parentComponents = reducePathComponents(getPathComponents(parent));
850    const childComponents = reducePathComponents(getPathComponents(child));
851    if (childComponents.length < parentComponents.length) {
852        return false;
853    }
854
855    const componentEqualityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive;
856    for (let i = 0; i < parentComponents.length; i++) {
857        const equalityComparer = i === 0 ? equateStringsCaseInsensitive : componentEqualityComparer;
858        if (!equalityComparer(parentComponents[i], childComponents[i])) {
859            return false;
860        }
861    }
862
863    return true;
864}
865
866/**
867 * Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback.
868 * Comparison is case-sensitive between the canonical paths.
869 *
870 * Use `containsPath` if file names are not already reduced and absolute.
871 *
872 * @internal
873 */
874export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: GetCanonicalFileName): boolean {
875    const canonicalFileName = getCanonicalFileName(fileName);
876    const canonicalDirectoryName = getCanonicalFileName(directoryName);
877    return startsWith(canonicalFileName, canonicalDirectoryName + "/") || startsWith(canonicalFileName, canonicalDirectoryName + "\\");
878}
879
880//// Relative Paths
881
882/** @internal */
883export function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) {
884    const fromComponents = reducePathComponents(getPathComponents(from));
885    const toComponents = reducePathComponents(getPathComponents(to));
886
887    let start: number;
888    for (start = 0; start < fromComponents.length && start < toComponents.length; start++) {
889        const fromComponent = getCanonicalFileName(fromComponents[start]);
890        const toComponent = getCanonicalFileName(toComponents[start]);
891        const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer;
892        if (!comparer(fromComponent, toComponent)) break;
893    }
894
895    if (start === 0) {
896        return toComponents;
897    }
898
899    const components = toComponents.slice(start);
900    const relative: string[] = [];
901    for (; start < fromComponents.length; start++) {
902        relative.push("..");
903    }
904    return ["", ...relative, ...components];
905}
906
907/**
908 * Gets a relative path that can be used to traverse between `from` and `to`.
909 *
910 * @internal
911 */
912export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string;
913/**
914 * Gets a relative path that can be used to traverse between `from` and `to`.
915 *
916 * @internal
917 */
918export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; // eslint-disable-line @typescript-eslint/unified-signatures
919/** @internal */
920export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) {
921    Debug.assert((getRootLength(fromDirectory) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative");
922    const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity;
923    const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false;
924    const pathComponents = getPathComponentsRelativeTo(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName);
925    return getPathFromPathComponents(pathComponents);
926}
927
928/** @internal */
929export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string {
930    return !isRootedDiskPath(absoluteOrRelativePath)
931        ? absoluteOrRelativePath
932        : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
933}
934
935/** @internal */
936export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: GetCanonicalFileName) {
937    return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName));
938}
939
940/** @internal */
941export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) {
942    const pathComponents = getPathComponentsRelativeTo(
943        resolvePath(currentDirectory, directoryPathOrUrl),
944        resolvePath(currentDirectory, relativeOrAbsolutePath),
945        equateStringsCaseSensitive,
946        getCanonicalFileName
947    );
948
949    const firstComponent = pathComponents[0];
950    if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) {
951        const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///";
952        pathComponents[0] = prefix + firstComponent;
953    }
954
955    return getPathFromPathComponents(pathComponents);
956}
957
958//// Path Traversal
959
960/**
961 * Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result.
962 *
963 * @internal
964 */
965export function forEachAncestorDirectory<T>(directory: Path, callback: (directory: Path) => T | undefined): T | undefined;
966/** @internal */
967export function forEachAncestorDirectory<T>(directory: string, callback: (directory: string) => T | undefined): T | undefined;
968/** @internal */
969export function forEachAncestorDirectory<T>(directory: Path, callback: (directory: Path) => T | undefined): T | undefined {
970    while (true) {
971        const result = callback(directory);
972        if (result !== undefined) {
973            return result;
974        }
975
976        const parentPath = getDirectoryPath(directory);
977        if (parentPath === directory) {
978            return undefined;
979        }
980
981        directory = parentPath;
982    }
983}
984
985/** @internal */
986export function isNodeModulesDirectory(dirPath: Path) {
987    return endsWith(dirPath, "/node_modules");
988}
989