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