From 1dff0af6465bc74ffbe12b37c256091fc9927e79 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Sun, 29 Apr 2018 21:31:33 -0700 Subject: [PATCH] More path cleanup --- src/compiler/commandLineParser.ts | 4 +- src/compiler/core.ts | 597 ++++++++++++------ src/compiler/program.ts | 2 +- src/compiler/watchUtilities.ts | 9 +- src/harness/parallel/worker.ts | 2 + src/harness/projectsRunner.ts | 6 +- src/harness/unittests/paths.ts | 292 +++++++++ src/harness/utils.ts | 6 +- src/harness/vfs.ts | 14 +- src/harness/vpath.ts | 280 +------- src/services/codefixes/importFixes.ts | 17 +- src/services/getEditsForFileRename.ts | 4 +- src/services/pathCompletions.ts | 30 +- .../reference/importWithTrailingSlash.js | 4 +- 14 files changed, 767 insertions(+), 500 deletions(-) create mode 100644 src/harness/unittests/paths.ts diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 126c1529094..b3bfdec0364 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1819,7 +1819,7 @@ namespace ts { } function getDefaultCompilerOptions(configFileName?: string) { - const options: CompilerOptions = getBaseFileName(configFileName) === "jsconfig.json" + const options: CompilerOptions = configFileName && getBaseFileName(configFileName) === "jsconfig.json" ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true, noEmit: true } : {}; return options; @@ -1834,7 +1834,7 @@ namespace ts { } function getDefaultTypeAcquisition(configFileName?: string): TypeAcquisition { - return { enable: getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] }; + return { enable: configFileName && getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] }; } function convertTypeAcquisitionFromJsonWorker(jsonOptions: any, diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 78d80ab8ade..a5c10d5883f 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1928,85 +1928,6 @@ namespace ts { return text1 ? Comparison.GreaterThan : Comparison.LessThan; } - /** - * Normalize path separators. - */ - export function normalizeSlashes(path: string): string { - return path.replace(/\\/g, "/"); - } - - /** - * Returns length of path root (i.e. length of "/", "x:/", "//server/share/, file:///user/files") - */ - export function getRootLength(path: string): number { - if (path.charCodeAt(0) === CharacterCodes.slash) { - if (path.charCodeAt(1) !== CharacterCodes.slash) return 1; - const p1 = path.indexOf("/", 2); - if (p1 < 0) return 2; - const p2 = path.indexOf("/", p1 + 1); - if (p2 < 0) return p1 + 1; - return p2 + 1; - } - if (path.charCodeAt(1) === CharacterCodes.colon) { - if (path.charCodeAt(2) === CharacterCodes.slash || path.charCodeAt(2) === CharacterCodes.backslash) return 3; - } - // Per RFC 1738 'file' URI schema has the shape file:/// - // if is omitted then it is assumed that host value is 'localhost', - // however slash after the omitted is not removed. - // file:///folder1/file1 - this is a correct URI - // file://folder2/file2 - this is an incorrect URI - if (path.lastIndexOf("file:///", 0) === 0) { - return "file:///".length; - } - const idx = path.indexOf("://"); - if (idx !== -1) { - return idx + "://".length; - } - return 0; - } - - /** - * Internally, we represent paths as strings with '/' as the directory separator. - * When we make system calls (eg: LanguageServiceHost.getDirectory()), - * we expect the host to correctly handle paths in our specified format. - */ - export const directorySeparator = "/"; - - export function normalizePath(path: string): string { - return normalizePathAndParts(path).path; - } - - export function normalizePathAndParts(path: string): { path: string, parts: string[] } { - path = normalizeSlashes(path); - const [root, ...parts] = reducePathComponents(getPathComponents(path)); - if (parts.length) { - const joinedParts = root + parts.join(directorySeparator); - return { path: hasTrailingDirectorySeparator(path) ? joinedParts + directorySeparator : joinedParts, parts }; - } - else { - return { path: root, parts }; - } - } - - /** - * Returns the path except for its basename. Eg: - * - * /path/to/file.ext -> /path/to - */ - export function getDirectoryPath(path: Path): Path; - export function getDirectoryPath(path: string): string; - export function getDirectoryPath(path: string): string { - return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator))); - } - - export function isUrl(path: string) { - return path && !isRootedDiskPath(path) && stringContains(path, "://"); - } - - export function pathIsRelative(path: string): boolean { - return /^\.\.?($|[\\/])/.test(path); - } - export function getEmitScriptTarget(compilerOptions: CompilerOptions) { return compilerOptions.target || ScriptTarget.ES3; } @@ -2060,19 +1981,207 @@ namespace ts { return true; } + // + // Paths + // + + + /** + * Internally, we represent paths as strings with '/' as the directory separator. + * When we make system calls (eg: LanguageServiceHost.getDirectory()), + * we expect the host to correctly handle paths in our specified format. + */ + export const directorySeparator = "/"; + const altDirectorySeparator = "\\"; + const urlSchemeSeparator = "://"; + + const backslashRegExp = /\\/g; + + /** + * Normalize path separators. + */ + export function normalizeSlashes(path: string): string { + return path.replace(backslashRegExp, directorySeparator); + } + + function isVolumeCharacter(charCode: number) { + return (charCode >= CharacterCodes.a && charCode <= CharacterCodes.z) || + (charCode >= CharacterCodes.A && charCode <= CharacterCodes.Z); + } + + function getFileUrlVolumeSeparatorEnd(url: string, start: number) { + const ch0 = url.charCodeAt(start); + if (ch0 === CharacterCodes.colon) return start + 1; + if (ch0 === CharacterCodes.percent && url.charCodeAt(start + 1) === CharacterCodes._3) { + const ch2 = url.charCodeAt(start + 2); + if (ch2 === CharacterCodes.a || ch2 === CharacterCodes.A) return start + 3; + } + return -1; + } + + /** + * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). + * If the root is part of a URL, the twos-complement of the root length is returned. + */ + function getEncodedRootLength(path: string): number { + if (!path) return 0; + const ch0 = path.charCodeAt(0); + + // POSIX or UNC + if (ch0 === CharacterCodes.slash || ch0 === CharacterCodes.backslash) { + if (path.charCodeAt(1) !== ch0) return 1; // POSIX: "/" (or non-normalized "\") + + const p1 = path.indexOf(ch0 === CharacterCodes.slash ? directorySeparator : altDirectorySeparator, 2); + if (p1 < 0) return path.length; // UNC: "//server" or "\\server" + + return p1 + 1; // UNC: "//server/" or "\\server\" + } + + // DOS + if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodes.colon) { + const ch2 = path.charCodeAt(2); + if (ch2 === CharacterCodes.slash || ch2 === CharacterCodes.backslash) return 3; // DOS: "c:/" or "c:\" + if (path.length === 2) return 2; // DOS: "c:" (but not "c:d") + } + + // URL + const schemeEnd = path.indexOf(urlSchemeSeparator); + if (schemeEnd !== -1) { + const authorityStart = schemeEnd + urlSchemeSeparator.length; + const authorityEnd = path.indexOf(directorySeparator, authorityStart); + if (authorityEnd !== -1) { // URL: "file:///", "file://server/", "file://server/path" + // For local "file" URLs, include the leading DOS volume (if present). + // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a + // special case interpreted as "the machine from which the URL is being interpreted". + const scheme = path.slice(0, schemeEnd); + const authority = path.slice(authorityStart, authorityEnd); + if (scheme === "file" && (authority === "" || authority === "localhost") && + isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) { + const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2); + if (volumeSeparatorEnd !== -1) { + if (path.charCodeAt(volumeSeparatorEnd) === CharacterCodes.slash) { + // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/" + return ~(volumeSeparatorEnd + 1); + } + if (volumeSeparatorEnd === path.length) { + // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a" + // but not "file:///c:d" or "file:///c%3ad" + return ~volumeSeparatorEnd; + } + } + } + return ~(authorityEnd + 1); // URL: "file://server/", "http://server/" + } + return ~path.length; // URL: "file://server", "http://server" + } + + // relative + return 0; + } + + /** + * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files"). + * + * For example: + * ```ts + * getRootLength("a") === 0 // "" + * getRootLength("/") === 1 // "/" + * getRootLength("c:") === 2 // "c:" + * getRootLength("c:d") === 0 // "" + * getRootLength("c:/") === 3 // "c:/" + * getRootLength("c:\\") === 3 // "c:\\" + * getRootLength("//server") === 7 // "//server" + * getRootLength("//server/share") === 8 // "//server/" + * getRootLength("\\\\server") === 7 // "\\\\server" + * getRootLength("\\\\server\\share") === 8 // "\\\\server\\" + * getRootLength("file:///path") === 8 // "file:///" + * getRootLength("file:///c:") === 10 // "file:///c:" + * getRootLength("file:///c:d") === 8 // "file:///" + * getRootLength("file:///c:/path") === 11 // "file:///c:/" + * getRootLength("file://server") === 13 // "file://server" + * getRootLength("file://server/path") === 14 // "file://server/" + * getRootLength("http://server") === 13 // "http://server" + * getRootLength("http://server/path") === 14 // "http://server/" + * ``` + */ + export function getRootLength(path: string) { + const rootLength = getEncodedRootLength(path); + return rootLength < 0 ? ~rootLength : rootLength; + } + + // TODO(rbuckton): replace references with `resolvePath` + export function normalizePath(path: string): string { + return resolvePath(path); + } + + export function normalizePathAndParts(path: string): { path: string, parts: string[] } { + path = normalizeSlashes(path); + const [root, ...parts] = reducePathComponents(getPathComponents(path)); + if (parts.length) { + const joinedParts = root + parts.join(directorySeparator); + return { path: hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(joinedParts) : joinedParts, parts }; + } + else { + return { path: root, parts }; + } + } + + /** + * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` + * except that we support URL's as well. + * + * ```js + * getDirectoryPath("/path/to/file.ext") === "/path/to" + * getDirectoryPath("/path/to/") === "/path" + * getDirectoryPath("/") === "/" + * ``` + */ + export function getDirectoryPath(path: Path): Path; + /** + * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` + * except that we support URL's as well. + * + * ```js + * getDirectoryPath("/path/to/file.ext") === "/path/to" + * getDirectoryPath("/path/to/") === "/path" + * getDirectoryPath("/") === "/" + * ``` + */ + export function getDirectoryPath(path: string): string; + export function getDirectoryPath(path: string): string { + path = normalizeSlashes(path); + + // If the path provided is itself the root, then return it. + const rootLength = getRootLength(path); + if (rootLength === path.length) return path; + + // return the leading portion of the path up to the last (non-terminal) directory separator + // but not including any trailing directory separator. + path = removeTrailingDirectorySeparator(path); + return path.slice(0, Math.max(rootLength, path.lastIndexOf(directorySeparator))); + } + + export function isUrl(path: string) { + return getEncodedRootLength(path) < 0; + } + + export function pathIsRelative(path: string): boolean { + return /^\.\.?($|[\\/])/.test(path); + } + /** * Determines whether a path is an absolute path (e.g. starts with `/`, or a dos path * like `c:`, `c:\` or `c:/`). */ export function isRootedDiskPath(path: string) { - return path && getRootLength(path) !== 0; + return getEncodedRootLength(path) > 0; } /** * Determines whether a path consists only of a path root. */ export function isDiskPathRoot(path: string) { - const rootLength = getRootLength(path); + const rootLength = getEncodedRootLength(path); return rootLength > 0 && rootLength === path.length; } @@ -2101,7 +2210,12 @@ namespace ts { return pathComponents(path, rootLength); } + /** + * Reduce an array of path components to a more simplified path by navigating any + * `"."` or `".."` entries in the path. + */ export function reducePathComponents(components: ReadonlyArray) { + if (!some(components)) return []; const reduced = [components[0]]; for (let i = 1; i < components.length; i++) { const component = components[i]; @@ -2131,137 +2245,140 @@ namespace ts { } export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string) { - return getNormalizedPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); + return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); } /** - * Formats a parsed path consisting of a root component and zero or more path segments. + * Formats a parsed path consisting of a root component (at index 0) and zero or more path + * segments (at indices > 0). */ - export function getNormalizedPathFromPathComponents(pathComponents: ReadonlyArray) { - if (pathComponents && pathComponents.length) { - return pathComponents[0] + pathComponents.slice(1).join(directorySeparator); - } - return ""; + export function getPathFromPathComponents(pathComponents: ReadonlyArray) { + if (pathComponents.length === 0) return ""; + + const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]); + if (pathComponents.length === 1) return root; + + return root + pathComponents.slice(1).join(directorySeparator); } - function getNormalizedPathComponentsOfUrl(url: string) { - // Get root length of http://www.website.com/folder1/folder2/ - // In this example the root is: http://www.website.com/ - // normalized path components should be ["http://www.website.com/", "folder1", "folder2"] + function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) { + const fromComponents = reducePathComponents(getPathComponents(from)); + const toComponents = reducePathComponents(getPathComponents(to)); - const urlLength = url.length; - // Initial root length is http:// part - let rootLength = url.indexOf("://") + "://".length; - while (rootLength < urlLength) { - // Consume all immediate slashes in the protocol - // eg.initial rootlength is just file:// but it needs to consume another "/" in file:/// - if (url.charCodeAt(rootLength) === CharacterCodes.slash) { - rootLength++; - } - else { - // non slash character means we continue proceeding to next component of root search - break; - } + let start: number; + for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { + const fromComponent = getCanonicalFileName(fromComponents[start]); + const toComponent = getCanonicalFileName(toComponents[start]); + const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer; + if (!comparer(fromComponent, toComponent)) break; } - // there are no parts after http:// just return current string as the pathComponent - if (rootLength === urlLength) { - return [url]; + if (start === 0) { + return toComponents; } - // Find the index of "/" after website.com so the root can be http://www.website.com/ (from existing http://) - const indexOfNextSlash = url.indexOf(directorySeparator, rootLength); - if (indexOfNextSlash !== -1) { - // Found the "/" after the website.com so the root is length of http://www.website.com/ - // and get components after the root normally like any other folder components - rootLength = indexOfNextSlash + 1; - return reducePathComponents(pathComponents(url, rootLength)); - } - else { - // Can't find the host assume the rest of the string as component - // but make sure we append "/" to it as root is not joined using "/" - // eg. if url passed in was http://website.com we want to use root as [http://website.com/] - // so that other path manipulations will be correct and it can be merged with relative paths correctly - return [url + directorySeparator]; + const components = toComponents.slice(start); + const relative: string[] = []; + for (; start < fromComponents.length; start++) { + relative.push(".."); } + return ["", ...relative, ...components]; } - function getNormalizedPathOrUrlComponents(pathOrUrl: string, currentDirectory: string) { - if (isUrl(pathOrUrl)) { - return getNormalizedPathComponentsOfUrl(pathOrUrl); - } - else { - return getNormalizedPathComponents(pathOrUrl, currentDirectory); - } + /** + * Gets a relative path that can be used to traverse between `from` and `to`. + */ + export function getRelativePath(from: string, to: string, ignoreCase: boolean): string; + /** + * Gets a relative path that can be used to traverse between `from` and `to`. + */ + // tslint:disable-next-line:unified-signatures + export function getRelativePath(from: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; + export function getRelativePath(from: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { + Debug.assert((getRootLength(from) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative"); + const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity; + const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false; + const pathComponents = getPathComponentsRelativeTo(from, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); + return getPathFromPathComponents(pathComponents); } export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { - const pathComponents = getNormalizedPathOrUrlComponents(relativeOrAbsolutePath, currentDirectory); - const directoryComponents = getNormalizedPathOrUrlComponents(directoryPathOrUrl, currentDirectory); - if (directoryComponents.length > 1 && lastOrUndefined(directoryComponents) === "") { - // If the directory path given was of type test/cases/ then we really need components of directory to be only till its name - // that is ["test", "cases", ""] needs to be actually ["test", "cases"] - directoryComponents.pop(); + const pathComponents = getPathComponentsRelativeTo( + resolvePath(currentDirectory, directoryPathOrUrl), + resolvePath(currentDirectory, relativeOrAbsolutePath), + equateStringsCaseSensitive, + getCanonicalFileName + ); + + const firstComponent = pathComponents[0]; + if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) { + const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///"; + pathComponents[0] = prefix + firstComponent; } - // Find the component that differs - let joinStartIndex: number; - for (joinStartIndex = 0; joinStartIndex < pathComponents.length && joinStartIndex < directoryComponents.length; joinStartIndex++) { - if (getCanonicalFileName(directoryComponents[joinStartIndex]) !== getCanonicalFileName(pathComponents[joinStartIndex])) { - break; - } - } - - // Get the relative path - if (joinStartIndex) { - let relativePath = ""; - const relativePathComponents = pathComponents.slice(joinStartIndex, pathComponents.length); - for (; joinStartIndex < directoryComponents.length; joinStartIndex++) { - if (directoryComponents[joinStartIndex] !== "") { - relativePath = relativePath + ".." + directorySeparator; - } - } - - return relativePath + relativePathComponents.join(directorySeparator); - } - - // Cant find the relative path, get the absolute path - let absolutePath = getNormalizedPathFromPathComponents(pathComponents); - if (isAbsolutePathAnUrl && isRootedDiskPath(absolutePath)) { - absolutePath = "file:///" + absolutePath; - } - - return absolutePath; - } - - export function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) { - const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - return ensurePathIsRelative(relativePath); - } - - export function ensurePathIsRelative(path: string): string { - return !pathIsRelative(path) ? "./" + path : path; - } - - export function getBaseFileName(path: string) { - if (path === undefined) { - return undefined; - } - const i = path.lastIndexOf(directorySeparator); - return i < 0 ? path : path.substring(i + 1); + return getPathFromPathComponents(pathComponents); } /** - * Combines two paths. If a path is absolute, it replaces any previous path. + * Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed + * with `./` or `../`) so as not to be confused with an unprefixed module name. */ - export function combinePaths(path1: string, path2: string): string { - if (path1) path1 = normalizeSlashes(path1); - if (path2) path2 = normalizeSlashes(path2); - if (!(path1 && path1.length)) return path2; - if (!(path2 && path2.length)) return path1; - if (getRootLength(path2) !== 0) return path2; - if (hasTrailingDirectorySeparator(path1)) return path1 + path2; - return path1 + directorySeparator + path2; + export function ensurePathIsNonModuleName(path: string): string { + return getRootLength(path) === 0 && !pathIsRelative(path) ? "./" + path : path; + } + + /** + * Gets the portion of a path following the last separator (`/`). + * Semantics align with NodeJS's `path.basename` except that we support URL's as well. + */ + export function getBaseFileName(path: string): string; + /** + * Gets the portion of a path following the last separator (`/`). + * Semantics align with NodeJS's `path.basename` except that we support URL's as well. + * If the base name has any one of the provided extensions, it is removed. + */ + export function getBaseFileName(path: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; + export function getBaseFileName(path: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) { + path = normalizeSlashes(path); + + // if the path provided is itself the root, then it has not file name. + const rootLength = getRootLength(path); + if (rootLength === path.length) return ""; + + // return the trailing portion of the path starting after the last (non-terminal) directory + // separator but not including any trailing directory separator. + path = removeTrailingDirectorySeparator(path); + const name = path.slice(Math.max(getRootLength(path), path.lastIndexOf(directorySeparator) + 1)); + const extension = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(name, extensions, ignoreCase) : undefined; + return extension ? name.slice(0, name.length - extension.length) : name; + } + + /** + * Combines paths. If a path is absolute, it replaces any previous path. + */ + export function combinePaths(path: string, ...paths: string[]): string { + if (path) path = normalizeSlashes(path); + for (let relativePath of paths) { + if (!relativePath) continue; + relativePath = normalizeSlashes(relativePath); + if (!path || getRootLength(relativePath) !== 0) { + path = relativePath; + } + else { + path = ensureTrailingDirectorySeparator(path) + relativePath; + } + } + return path; + } + + /** + * Combines and resolves paths. If a path is absolute, it replaces any previous path. Any + * `.` and `..` path components are resolved. + */ + export function resolvePath(path: string, ...paths: string[]): string { + const combined = some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path); + const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(combined))); + return normalized && hasTrailingDirectorySeparator(combined) ? ensureTrailingDirectorySeparator(normalized) : normalized; } /** @@ -2301,40 +2418,71 @@ namespace ts { return path; } - export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean) { + function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => Comparison) { if (a === b) return Comparison.EqualTo; if (a === undefined) return Comparison.LessThan; if (b === undefined) return Comparison.GreaterThan; - a = removeTrailingDirectorySeparator(a); - b = removeTrailingDirectorySeparator(b); - const aComponents = getNormalizedPathComponents(a, currentDirectory); - const bComponents = getNormalizedPathComponents(b, currentDirectory); + const aComponents = reducePathComponents(getPathComponents(a)); + const bComponents = reducePathComponents(getPathComponents(b)); const sharedLength = Math.min(aComponents.length, bComponents.length); - const comparer = ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive; for (let i = 0; i < sharedLength; i++) { - const result = comparer(aComponents[i], bComponents[i]); + const stringComparer = i === 0 ? compareStringsCaseInsensitive : componentComparer; + const result = stringComparer(aComponents[i], bComponents[i]); if (result !== Comparison.EqualTo) { return result; } } - return compareValues(aComponents.length, bComponents.length); } - export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean) { + /** + * Performs a case-sensitive comparison of two paths. + */ + export function comparePathsCaseSensitive(a: string, b: string) { + return comparePathsWorker(a, b, compareStringsCaseSensitive); + } + + /** + * Performs a case-insensitive comparison of two paths. + */ + export function comparePathsCaseInsensitive(a: string, b: string) { + return comparePathsWorker(a, b, compareStringsCaseInsensitive); + } + + export function comparePaths(a: string, b: string, ignoreCase?: boolean): Comparison; + export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean): Comparison; + export function comparePaths(a: string, b: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { + if (typeof currentDirectory === "string") { + a = combinePaths(currentDirectory, a); + b = combinePaths(currentDirectory, b); + } + else if (typeof currentDirectory === "boolean") { + ignoreCase = currentDirectory; + } + return comparePathsWorker(a, b, ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive); + } + + export function containsPath(parent: string, child: string, ignoreCase?: boolean): boolean; + export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean): boolean; + export function containsPath(parent: string, child: string, currentDirectory?: string | boolean, ignoreCase?: boolean) { + if (typeof currentDirectory === "string") { + parent = combinePaths(currentDirectory, parent); + child = combinePaths(currentDirectory, child); + } + else if (typeof currentDirectory === "boolean") { + ignoreCase = currentDirectory; + } if (parent === undefined || child === undefined) return false; if (parent === child) return true; - parent = removeTrailingDirectorySeparator(parent); - child = removeTrailingDirectorySeparator(child); - if (parent === child) return true; - const parentComponents = getNormalizedPathComponents(parent, currentDirectory); - const childComponents = getNormalizedPathComponents(child, currentDirectory); + const parentComponents = reducePathComponents(getPathComponents(parent)); + const childComponents = reducePathComponents(getPathComponents(child)); if (childComponents.length < parentComponents.length) { return false; } - const equalityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive; + const componentEqualityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive; for (let i = 0; i < parentComponents.length; i++) { + const equalityComparer = i === 0 ? equateStringsCaseInsensitive : componentEqualityComparer; if (!equalityComparer(parentComponents[i], childComponents[i])) { return false; } @@ -2816,7 +2964,14 @@ namespace ts { } export function changeExtension(path: T, newExtension: string): T { - return (removeFileExtension(path) + newExtension); + return changeAnyExtension(path, newExtension, extensionsToRemove, /*ignoreCase*/ false); + } + + export function changeAnyExtension(path: string, ext: string): string; + export function changeAnyExtension(path: string, ext: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; + export function changeAnyExtension(path: string, ext: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) { + const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path); + return pathext ? path.slice(0, path.length - pathext.length) + (ts.startsWith(ext, ".") ? ext : "." + ext) : path; } /** @@ -3150,14 +3305,40 @@ namespace ts { return find(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e)); } - // Retrieves any string from the final "." onwards from a base file name. - // Unlike extensionFromPath, which throws an exception on unrecognized extensions. - export function getAnyExtensionFromPath(path: string): string | undefined { + function getAnyExtensionFromPathWorker(path: string, extensions: string | ReadonlyArray, stringEqualityComparer: (a: string, b: string) => boolean) { + if (typeof extensions === "string") extensions = [extensions]; + for (let extension of extensions) { + if (!startsWith(extension, ".")) extension = "." + extension; + if (path.length >= extension.length && path.charAt(path.length - extension.length) === ".") { + const pathExtension = path.slice(path.length - extension.length); + if (stringEqualityComparer(pathExtension, extension)) { + return pathExtension; + } + } + } + return ""; + } + + /** + * Gets the file extension for a path. + */ + export function getAnyExtensionFromPath(path: string): string; + /** + * Gets the file extension for a path, provided it is one of the provided extensions. + */ + export function getAnyExtensionFromPath(path: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; + export function getAnyExtensionFromPath(path: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean): string { + // Retrieves any string from the final "." onwards from a base file name. + // Unlike extensionFromPath, which throws an exception on unrecognized extensions. + if (extensions) { + return getAnyExtensionFromPathWorker(path, extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive); + } const baseFileName = getBaseFileName(path); const extensionIndex = baseFileName.lastIndexOf("."); if (extensionIndex >= 0) { return baseFileName.substring(extensionIndex); } + return ""; } export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 75cdd07845f..da1f60ba6eb 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -57,7 +57,7 @@ namespace ts { return currentDirectory; } - return getNormalizedPathFromPathComponents(commonPathComponents); + return getPathFromPathComponents(commonPathComponents); } interface OutputFingerprint { diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 4fa9b28e162..755acde0bfd 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -63,7 +63,7 @@ namespace ts { } function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined { - return cachedReadDirectoryResult.get(rootDirPath); + return cachedReadDirectoryResult.get(ensureTrailingDirectorySeparator(rootDirPath)); } function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined { @@ -80,7 +80,7 @@ namespace ts { directories: host.getDirectories(rootDir) || [] }; - cachedReadDirectoryResult.set(rootDirPath, resultFromHost); + cachedReadDirectoryResult.set(ensureTrailingDirectorySeparator(rootDirPath), resultFromHost); return resultFromHost; } @@ -90,6 +90,7 @@ namespace ts { * The host request is done under try catch block to avoid caching incorrect result */ function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined { + rootDirPath = ensureTrailingDirectorySeparator(rootDirPath); const cachedResult = getCachedFileSystemEntries(rootDirPath); if (cachedResult) { return cachedResult; @@ -100,7 +101,7 @@ namespace ts { } catch (_e) { // If there is exception to read directories, dont cache the result and direct the calls to host - Debug.assert(!cachedReadDirectoryResult.has(rootDirPath)); + Debug.assert(!cachedReadDirectoryResult.has(ensureTrailingDirectorySeparator(rootDirPath))); return undefined; } } @@ -142,7 +143,7 @@ namespace ts { function directoryExists(dirPath: string): boolean { const path = toPath(dirPath); - return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath); + return cachedReadDirectoryResult.has(ensureTrailingDirectorySeparator(path)) || host.directoryExists(dirPath); } function createDirectory(dirPath: string) { diff --git a/src/harness/parallel/worker.ts b/src/harness/parallel/worker.ts index 50c043cf71d..4a0f297eee6 100644 --- a/src/harness/parallel/worker.ts +++ b/src/harness/parallel/worker.ts @@ -28,12 +28,14 @@ namespace Harness.Parallel.Worker { (global as any).describe = ((name, callback) => { testList.push({ name, callback, kind: "suite" }); }) as Mocha.IContextDefinition; + (global as any).describe.skip = ts.noop; (global as any).it = ((name, callback) => { if (!testList) { throw new Error("Tests must occur within a describe block"); } testList.push({ name, callback, kind: "test" }); }) as Mocha.ITestDefinition; + (global as any).it.skip = ts.noop; } function setTimeoutAndExecute(timeout: number | undefined, f: () => void) { diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 3aa18b1ba27..3a34fa4cbef 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -210,7 +210,9 @@ namespace project { const ignoreCase = this.vfs.ignoreCase; const resolutionInfo: ProjectRunnerTestCaseResolutionInfo & ts.CompilerOptions = JSON.parse(JSON.stringify(this.testCase)); resolutionInfo.resolvedInputFiles = this.compilerResult.program.getSourceFiles() - .map(input => utils.removeTestPathPrefixes(vpath.isAbsolute(input.fileName) ? vpath.relative(cwd, input.fileName, ignoreCase) : input.fileName)); + .map(({ fileName: input }) => vpath.beneath(vfs.builtFolder, input, this.vfs.ignoreCase) || vpath.beneath(vfs.testLibFolder, input, this.vfs.ignoreCase) ? utils.removeTestPathPrefixes(input) : + vpath.isAbsolute(input) ? vpath.relative(cwd, input, ignoreCase) : + input); resolutionInfo.emittedFiles = this.compilerResult.outputFiles .map(output => output.meta.get("fileName") || output.file) @@ -255,7 +257,7 @@ namespace project { nonSubfolderDiskFiles++; } - const content = output.text.replace(/\/\/?\.src\//g, "/"); + const content = utils.removeTestPathPrefixes(output.text, /*retainTrailingDirectorySeparator*/ true); Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + diskRelativeName, () => content); } catch (e) { diff --git a/src/harness/unittests/paths.ts b/src/harness/unittests/paths.ts new file mode 100644 index 00000000000..0cd90be0483 --- /dev/null +++ b/src/harness/unittests/paths.ts @@ -0,0 +1,292 @@ +describe("core paths", () => { + it("normalizeSlashes", () => { + assert.strictEqual(ts.normalizeSlashes("a"), "a"); + assert.strictEqual(ts.normalizeSlashes("a/b"), "a/b"); + assert.strictEqual(ts.normalizeSlashes("a\\b"), "a/b"); + assert.strictEqual(ts.normalizeSlashes("\\\\server\\path"), "//server/path"); + }); + it("getRootLength", () => { + assert.strictEqual(ts.getRootLength("a"), 0); + assert.strictEqual(ts.getRootLength("/"), 1); + assert.strictEqual(ts.getRootLength("/path"), 1); + assert.strictEqual(ts.getRootLength("c:"), 2); + assert.strictEqual(ts.getRootLength("c:d"), 0); + assert.strictEqual(ts.getRootLength("c:/"), 3); + assert.strictEqual(ts.getRootLength("c:\\"), 3); + assert.strictEqual(ts.getRootLength("//server"), 8); + assert.strictEqual(ts.getRootLength("//server/share"), 9); + assert.strictEqual(ts.getRootLength("\\\\server"), 8); + assert.strictEqual(ts.getRootLength("\\\\server\\share"), 9); + assert.strictEqual(ts.getRootLength("file:///"), 8); + assert.strictEqual(ts.getRootLength("file:///path"), 8); + assert.strictEqual(ts.getRootLength("file:///c:"), 10); + assert.strictEqual(ts.getRootLength("file:///c:d"), 8); + assert.strictEqual(ts.getRootLength("file:///c:/path"), 11); + assert.strictEqual(ts.getRootLength("file:///c%3a"), 12); + assert.strictEqual(ts.getRootLength("file:///c%3ad"), 8); + assert.strictEqual(ts.getRootLength("file:///c%3a/path"), 13); + assert.strictEqual(ts.getRootLength("file:///c%3A"), 12); + assert.strictEqual(ts.getRootLength("file:///c%3Ad"), 8); + assert.strictEqual(ts.getRootLength("file:///c%3A/path"), 13); + assert.strictEqual(ts.getRootLength("file://localhost"), 16); + assert.strictEqual(ts.getRootLength("file://localhost/"), 17); + assert.strictEqual(ts.getRootLength("file://localhost/path"), 17); + assert.strictEqual(ts.getRootLength("file://localhost/c:"), 19); + assert.strictEqual(ts.getRootLength("file://localhost/c:d"), 17); + assert.strictEqual(ts.getRootLength("file://localhost/c:/path"), 20); + assert.strictEqual(ts.getRootLength("file://localhost/c%3a"), 21); + assert.strictEqual(ts.getRootLength("file://localhost/c%3ad"), 17); + assert.strictEqual(ts.getRootLength("file://localhost/c%3a/path"), 22); + assert.strictEqual(ts.getRootLength("file://localhost/c%3A"), 21); + assert.strictEqual(ts.getRootLength("file://localhost/c%3Ad"), 17); + assert.strictEqual(ts.getRootLength("file://localhost/c%3A/path"), 22); + assert.strictEqual(ts.getRootLength("file://server"), 13); + assert.strictEqual(ts.getRootLength("file://server/"), 14); + assert.strictEqual(ts.getRootLength("file://server/path"), 14); + assert.strictEqual(ts.getRootLength("file://server/c:"), 14); + assert.strictEqual(ts.getRootLength("file://server/c:d"), 14); + assert.strictEqual(ts.getRootLength("file://server/c:/d"), 14); + assert.strictEqual(ts.getRootLength("file://server/c%3a"), 14); + assert.strictEqual(ts.getRootLength("file://server/c%3ad"), 14); + assert.strictEqual(ts.getRootLength("file://server/c%3a/d"), 14); + assert.strictEqual(ts.getRootLength("file://server/c%3A"), 14); + assert.strictEqual(ts.getRootLength("file://server/c%3Ad"), 14); + assert.strictEqual(ts.getRootLength("file://server/c%3A/d"), 14); + assert.strictEqual(ts.getRootLength("http://server"), 13); + assert.strictEqual(ts.getRootLength("http://server/path"), 14); + }); + it("isUrl", () => { + assert.isFalse(ts.isUrl("a")); + assert.isFalse(ts.isUrl("/")); + assert.isFalse(ts.isUrl("c:")); + assert.isFalse(ts.isUrl("c:d")); + assert.isFalse(ts.isUrl("c:/")); + assert.isFalse(ts.isUrl("c:\\")); + assert.isFalse(ts.isUrl("//server")); + assert.isFalse(ts.isUrl("//server/share")); + assert.isFalse(ts.isUrl("\\\\server")); + assert.isFalse(ts.isUrl("\\\\server\\share")); + assert.isTrue(ts.isUrl("file:///path")); + assert.isTrue(ts.isUrl("file:///c:")); + assert.isTrue(ts.isUrl("file:///c:d")); + assert.isTrue(ts.isUrl("file:///c:/path")); + assert.isTrue(ts.isUrl("file://server")); + assert.isTrue(ts.isUrl("file://server/path")); + assert.isTrue(ts.isUrl("http://server")); + assert.isTrue(ts.isUrl("http://server/path")); + }); + it("isRootedDiskPath", () => { + assert.isFalse(ts.isRootedDiskPath("a")); + assert.isTrue(ts.isRootedDiskPath("/")); + assert.isTrue(ts.isRootedDiskPath("c:")); + assert.isFalse(ts.isRootedDiskPath("c:d")); + assert.isTrue(ts.isRootedDiskPath("c:/")); + assert.isTrue(ts.isRootedDiskPath("c:\\")); + assert.isTrue(ts.isRootedDiskPath("//server")); + assert.isTrue(ts.isRootedDiskPath("//server/share")); + assert.isTrue(ts.isRootedDiskPath("\\\\server")); + assert.isTrue(ts.isRootedDiskPath("\\\\server\\share")); + assert.isFalse(ts.isRootedDiskPath("file:///path")); + assert.isFalse(ts.isRootedDiskPath("file:///c:")); + assert.isFalse(ts.isRootedDiskPath("file:///c:d")); + assert.isFalse(ts.isRootedDiskPath("file:///c:/path")); + assert.isFalse(ts.isRootedDiskPath("file://server")); + assert.isFalse(ts.isRootedDiskPath("file://server/path")); + assert.isFalse(ts.isRootedDiskPath("http://server")); + assert.isFalse(ts.isRootedDiskPath("http://server/path")); + }); + it("getDirectoryPath", () => { + assert.strictEqual(ts.getDirectoryPath(""), ""); + assert.strictEqual(ts.getDirectoryPath("a"), ""); + assert.strictEqual(ts.getDirectoryPath("a/b"), "a"); + assert.strictEqual(ts.getDirectoryPath("/"), "/"); + assert.strictEqual(ts.getDirectoryPath("/a"), "/"); + assert.strictEqual(ts.getDirectoryPath("/a/"), "/"); + assert.strictEqual(ts.getDirectoryPath("/a/b"), "/a"); + assert.strictEqual(ts.getDirectoryPath("/a/b/"), "/a"); + assert.strictEqual(ts.getDirectoryPath("c:"), "c:"); + assert.strictEqual(ts.getDirectoryPath("c:d"), ""); + assert.strictEqual(ts.getDirectoryPath("c:/"), "c:/"); + assert.strictEqual(ts.getDirectoryPath("c:/path"), "c:/"); + assert.strictEqual(ts.getDirectoryPath("c:/path/"), "c:/"); + assert.strictEqual(ts.getDirectoryPath("//server"), "//server"); + assert.strictEqual(ts.getDirectoryPath("//server/"), "//server/"); + assert.strictEqual(ts.getDirectoryPath("//server/share"), "//server/"); + assert.strictEqual(ts.getDirectoryPath("//server/share/"), "//server/"); + assert.strictEqual(ts.getDirectoryPath("\\\\server"), "//server"); + assert.strictEqual(ts.getDirectoryPath("\\\\server\\"), "//server/"); + assert.strictEqual(ts.getDirectoryPath("\\\\server\\share"), "//server/"); + assert.strictEqual(ts.getDirectoryPath("\\\\server\\share\\"), "//server/"); + assert.strictEqual(ts.getDirectoryPath("file:///"), "file:///"); + assert.strictEqual(ts.getDirectoryPath("file:///path"), "file:///"); + assert.strictEqual(ts.getDirectoryPath("file:///path/"), "file:///"); + assert.strictEqual(ts.getDirectoryPath("file:///c:"), "file:///c:"); + assert.strictEqual(ts.getDirectoryPath("file:///c:d"), "file:///"); + assert.strictEqual(ts.getDirectoryPath("file:///c:/"), "file:///c:/"); + assert.strictEqual(ts.getDirectoryPath("file:///c:/path"), "file:///c:/"); + assert.strictEqual(ts.getDirectoryPath("file:///c:/path/"), "file:///c:/"); + assert.strictEqual(ts.getDirectoryPath("file://server"), "file://server"); + assert.strictEqual(ts.getDirectoryPath("file://server/"), "file://server/"); + assert.strictEqual(ts.getDirectoryPath("file://server/path"), "file://server/"); + assert.strictEqual(ts.getDirectoryPath("file://server/path/"), "file://server/"); + assert.strictEqual(ts.getDirectoryPath("http://server"), "http://server"); + assert.strictEqual(ts.getDirectoryPath("http://server/"), "http://server/"); + assert.strictEqual(ts.getDirectoryPath("http://server/path"), "http://server/"); + assert.strictEqual(ts.getDirectoryPath("http://server/path/"), "http://server/"); + }); + it("getBaseFileName", () => { + assert.strictEqual(ts.getBaseFileName(""), ""); + assert.strictEqual(ts.getBaseFileName("a"), "a"); + assert.strictEqual(ts.getBaseFileName("a/"), "a"); + assert.strictEqual(ts.getBaseFileName("/"), ""); + assert.strictEqual(ts.getBaseFileName("/a"), "a"); + assert.strictEqual(ts.getBaseFileName("/a/"), "a"); + assert.strictEqual(ts.getBaseFileName("/a/b"), "b"); + assert.strictEqual(ts.getBaseFileName("c:"), ""); + assert.strictEqual(ts.getBaseFileName("c:d"), "c:d"); + assert.strictEqual(ts.getBaseFileName("c:/"), ""); + assert.strictEqual(ts.getBaseFileName("c:\\"), ""); + assert.strictEqual(ts.getBaseFileName("c:/path"), "path"); + assert.strictEqual(ts.getBaseFileName("c:/path/"), "path"); + assert.strictEqual(ts.getBaseFileName("//server"), ""); + assert.strictEqual(ts.getBaseFileName("//server/"), ""); + assert.strictEqual(ts.getBaseFileName("//server/share"), "share"); + assert.strictEqual(ts.getBaseFileName("//server/share/"), "share"); + assert.strictEqual(ts.getBaseFileName("file:///"), ""); + assert.strictEqual(ts.getBaseFileName("file:///path"), "path"); + assert.strictEqual(ts.getBaseFileName("file:///path/"), "path"); + assert.strictEqual(ts.getBaseFileName("file:///c:"), ""); + assert.strictEqual(ts.getBaseFileName("file:///c:/"), ""); + assert.strictEqual(ts.getBaseFileName("file:///c:d"), "c:d"); + assert.strictEqual(ts.getBaseFileName("file:///c:/d"), "d"); + assert.strictEqual(ts.getBaseFileName("file:///c:/d/"), "d"); + assert.strictEqual(ts.getBaseFileName("http://server"), ""); + assert.strictEqual(ts.getBaseFileName("http://server/"), ""); + assert.strictEqual(ts.getBaseFileName("http://server/a"), "a"); + assert.strictEqual(ts.getBaseFileName("http://server/a/"), "a"); + assert.strictEqual(ts.getBaseFileName("/path/a.ext", ".ext", /*ignoreCase*/ false), "a"); + assert.strictEqual(ts.getBaseFileName("/path/a.ext", ".EXT", /*ignoreCase*/ true), "a"); + assert.strictEqual(ts.getBaseFileName("/path/a.ext", "ext", /*ignoreCase*/ false), "a"); + assert.strictEqual(ts.getBaseFileName("/path/a.b", ".ext", /*ignoreCase*/ false), "a.b"); + assert.strictEqual(ts.getBaseFileName("/path/a.b", [".b", ".c"], /*ignoreCase*/ false), "a"); + assert.strictEqual(ts.getBaseFileName("/path/a.c", [".b", ".c"], /*ignoreCase*/ false), "a"); + assert.strictEqual(ts.getBaseFileName("/path/a.d", [".b", ".c"], /*ignoreCase*/ false), "a.d"); + }); + it("getAnyExtensionFromPath", () => { + assert.strictEqual(ts.getAnyExtensionFromPath(""), ""); + assert.strictEqual(ts.getAnyExtensionFromPath(".ext"), ".ext"); + assert.strictEqual(ts.getAnyExtensionFromPath("a.ext"), ".ext"); + assert.strictEqual(ts.getAnyExtensionFromPath("/a.ext"), ".ext"); + assert.strictEqual(ts.getAnyExtensionFromPath("a.ext/"), ".ext"); + assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", ".ext", /*ignoreCase*/ false), ".ext"); + assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", ".EXT", /*ignoreCase*/ true), ".ext"); + assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", "ext", /*ignoreCase*/ false), ".ext"); + assert.strictEqual(ts.getAnyExtensionFromPath("a.b", ".ext", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getAnyExtensionFromPath("a.b", [".b", ".c"], /*ignoreCase*/ false), ".b"); + assert.strictEqual(ts.getAnyExtensionFromPath("a.c", [".b", ".c"], /*ignoreCase*/ false), ".c"); + assert.strictEqual(ts.getAnyExtensionFromPath("a.d", [".b", ".c"], /*ignoreCase*/ false), ""); + }); + it("getPathComponents", () => { + assert.deepEqual(ts.getPathComponents(""), [""]); + assert.deepEqual(ts.getPathComponents("a"), ["", "a"]); + assert.deepEqual(ts.getPathComponents("./a"), ["", ".", "a"]); + assert.deepEqual(ts.getPathComponents("/"), ["/"]); + assert.deepEqual(ts.getPathComponents("/a"), ["/", "a"]); + assert.deepEqual(ts.getPathComponents("/a/"), ["/", "a"]); + assert.deepEqual(ts.getPathComponents("c:"), ["c:"]); + assert.deepEqual(ts.getPathComponents("c:d"), ["", "c:d"]); + assert.deepEqual(ts.getPathComponents("c:/"), ["c:/"]); + assert.deepEqual(ts.getPathComponents("c:/path"), ["c:/", "path"]); + assert.deepEqual(ts.getPathComponents("//server"), ["//server"]); + assert.deepEqual(ts.getPathComponents("//server/"), ["//server/"]); + assert.deepEqual(ts.getPathComponents("//server/share"), ["//server/", "share"]); + assert.deepEqual(ts.getPathComponents("file:///"), ["file:///"]); + assert.deepEqual(ts.getPathComponents("file:///path"), ["file:///", "path"]); + assert.deepEqual(ts.getPathComponents("file:///c:"), ["file:///c:"]); + assert.deepEqual(ts.getPathComponents("file:///c:d"), ["file:///", "c:d"]); + assert.deepEqual(ts.getPathComponents("file:///c:/"), ["file:///c:/"]); + assert.deepEqual(ts.getPathComponents("file:///c:/path"), ["file:///c:/", "path"]); + assert.deepEqual(ts.getPathComponents("file://server"), ["file://server"]); + assert.deepEqual(ts.getPathComponents("file://server/"), ["file://server/"]); + assert.deepEqual(ts.getPathComponents("file://server/path"), ["file://server/", "path"]); + assert.deepEqual(ts.getPathComponents("http://server"), ["http://server"]); + assert.deepEqual(ts.getPathComponents("http://server/"), ["http://server/"]); + assert.deepEqual(ts.getPathComponents("http://server/path"), ["http://server/", "path"]); + }); + it("reducePathComponents", () => { + assert.deepEqual(ts.reducePathComponents([]), []); + assert.deepEqual(ts.reducePathComponents([""]), [""]); + assert.deepEqual(ts.reducePathComponents(["", "."]), [""]); + assert.deepEqual(ts.reducePathComponents(["", ".", "a"]), ["", "a"]); + assert.deepEqual(ts.reducePathComponents(["", "a", "."]), ["", "a"]); + assert.deepEqual(ts.reducePathComponents(["", ".."]), ["", ".."]); + assert.deepEqual(ts.reducePathComponents(["", "..", ".."]), ["", "..", ".."]); + assert.deepEqual(ts.reducePathComponents(["", "..", ".", ".."]), ["", "..", ".."]); + assert.deepEqual(ts.reducePathComponents(["", "a", ".."]), [""]); + assert.deepEqual(ts.reducePathComponents(["", "..", "a"]), ["", "..", "a"]); + assert.deepEqual(ts.reducePathComponents(["/"]), ["/"]); + assert.deepEqual(ts.reducePathComponents(["/", "."]), ["/"]); + assert.deepEqual(ts.reducePathComponents(["/", ".."]), ["/"]); + assert.deepEqual(ts.reducePathComponents(["/", "a", ".."]), ["/"]); + }); + it("combinePaths", () => { + assert.strictEqual(ts.combinePaths("/", "/node_modules/@types"), "/node_modules/@types"); + assert.strictEqual(ts.combinePaths("/a/..", ""), "/a/.."); + assert.strictEqual(ts.combinePaths("/a/..", "b"), "/a/../b"); + assert.strictEqual(ts.combinePaths("/a/..", "b/"), "/a/../b/"); + assert.strictEqual(ts.combinePaths("/a/..", "/"), "/"); + assert.strictEqual(ts.combinePaths("/a/..", "/b"), "/b"); + }); + it("resolvePath", () => { + assert.strictEqual(ts.resolvePath(""), ""); + assert.strictEqual(ts.resolvePath("."), ""); + assert.strictEqual(ts.resolvePath("./"), ""); + assert.strictEqual(ts.resolvePath(".."), ".."); + assert.strictEqual(ts.resolvePath("../"), "../"); + assert.strictEqual(ts.resolvePath("/"), "/"); + assert.strictEqual(ts.resolvePath("/."), "/"); + assert.strictEqual(ts.resolvePath("/./"), "/"); + assert.strictEqual(ts.resolvePath("/../"), "/"); + assert.strictEqual(ts.resolvePath("/a"), "/a"); + assert.strictEqual(ts.resolvePath("/a/"), "/a/"); + assert.strictEqual(ts.resolvePath("/a/."), "/a"); + assert.strictEqual(ts.resolvePath("/a/./"), "/a/"); + assert.strictEqual(ts.resolvePath("/a/./b"), "/a/b"); + assert.strictEqual(ts.resolvePath("/a/./b/"), "/a/b/"); + assert.strictEqual(ts.resolvePath("/a/.."), "/"); + assert.strictEqual(ts.resolvePath("/a/../"), "/"); + assert.strictEqual(ts.resolvePath("/a/../b"), "/b"); + assert.strictEqual(ts.resolvePath("/a/../b/"), "/b/"); + assert.strictEqual(ts.resolvePath("/a/..", "b"), "/b"); + assert.strictEqual(ts.resolvePath("/a/..", "/"), "/"); + assert.strictEqual(ts.resolvePath("/a/..", "b/"), "/b/"); + assert.strictEqual(ts.resolvePath("/a/..", "/b"), "/b"); + assert.strictEqual(ts.resolvePath("/a/.", "b"), "/a/b"); + assert.strictEqual(ts.resolvePath("/a/.", "."), "/a"); + assert.strictEqual(ts.resolvePath("a", "b", "c"), "a/b/c"); + assert.strictEqual(ts.resolvePath("a", "b", "/c"), "/c"); + assert.strictEqual(ts.resolvePath("a", "b", "../c"), "a/c"); + }); + it("getPathRelativeTo", () => { + assert.strictEqual(ts.getRelativePath("/", "/", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePath("/a", "/a", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePath("/a/", "/a", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePath("/a", "/", /*ignoreCase*/ false), ".."); + assert.strictEqual(ts.getRelativePath("/a", "/b", /*ignoreCase*/ false), "../b"); + assert.strictEqual(ts.getRelativePath("/a/b", "/b", /*ignoreCase*/ false), "../../b"); + assert.strictEqual(ts.getRelativePath("/a/b/c", "/b", /*ignoreCase*/ false), "../../../b"); + assert.strictEqual(ts.getRelativePath("/a/b/c", "/b/c", /*ignoreCase*/ false), "../../../b/c"); + assert.strictEqual(ts.getRelativePath("/a/b/c", "/a/b", /*ignoreCase*/ false), ".."); + assert.strictEqual(ts.getRelativePath("c:", "d:", /*ignoreCase*/ false), "d:/"); + assert.strictEqual(ts.getRelativePath("file:///", "file:///", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePath("file:///a", "file:///a", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePath("file:///a/", "file:///a", /*ignoreCase*/ false), ""); + assert.strictEqual(ts.getRelativePath("file:///a", "file:///", /*ignoreCase*/ false), ".."); + assert.strictEqual(ts.getRelativePath("file:///a", "file:///b", /*ignoreCase*/ false), "../b"); + assert.strictEqual(ts.getRelativePath("file:///a/b", "file:///b", /*ignoreCase*/ false), "../../b"); + assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///b", /*ignoreCase*/ false), "../../../b"); + assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///b/c", /*ignoreCase*/ false), "../../../b/c"); + assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), ".."); + assert.strictEqual(ts.getRelativePath("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/"); + }); +}); \ No newline at end of file diff --git a/src/harness/utils.ts b/src/harness/utils.ts index 81da505cd90..03e9e56169b 100644 --- a/src/harness/utils.ts +++ b/src/harness/utils.ts @@ -27,9 +27,9 @@ namespace utils { } } - const testPathPrefixRegExp = /\/\.(ts|lib|src)\//g; - export function removeTestPathPrefixes(text: string) { - return text !== undefined ? text.replace(testPathPrefixRegExp, "") : undefined; + const testPathPrefixRegExp = /(?:(file:\/{3})|\/)\.(ts|lib|src)\//g; + export function removeTestPathPrefixes(text: string, retainTrailingDirectorySeparator?: boolean) { + return text !== undefined ? text.replace(testPathPrefixRegExp, (_, scheme) => scheme || (retainTrailingDirectorySeparator ? "/" : "")) : undefined; } /** diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index cbd3f5a11d2..e1107537f9e 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -332,16 +332,25 @@ namespace vfs { } } + private _depth: string[] = []; + /** * Make a directory and all of its parent paths (if they don't exist). */ public mkdirpSync(path: string) { - path = this._resolve(path); try { + this._depth.push(path); + path = this._resolve(path); this.mkdirSync(path); } catch (e) { if (e.code === "ENOENT") { + if (this._depth.length > 10) { + console.log(`path: ${path}`); + console.log(`dirname: ${vpath.dirname(path)}`); + console.log(this._depth); + throw e; + } this.mkdirpSync(vpath.dirname(path)); this.mkdirSync(path); } @@ -349,6 +358,9 @@ namespace vfs { throw e; } } + finally { + this._depth.pop(); + } } /** diff --git a/src/harness/vpath.ts b/src/harness/vpath.ts index b6f4065d854..9b42ba2a28d 100644 --- a/src/harness/vpath.ts +++ b/src/harness/vpath.ts @@ -1,7 +1,33 @@ namespace vpath { + export import sep = ts.directorySeparator; + export import normalizeSeparators = ts.normalizeSlashes; + export import isAbsolute = ts.isRootedDiskPath; + export import isRoot = ts.isDiskPathRoot; + export import hasTrailingSeparator = ts.hasTrailingDirectorySeparator; + export import addTrailingSeparator = ts.ensureTrailingDirectorySeparator; + export import removeTrailingSeparator = ts.removeTrailingDirectorySeparator; + export import normalize = ts.normalizePath; + export import combine = ts.combinePaths; + export import parse = ts.getPathComponents; + export import reduce = ts.reducePathComponents; + export import format = ts.getPathFromPathComponents; + export import resolve = ts.resolvePath; + export import compare = ts.comparePaths; + export import compareCaseSensitive = ts.comparePathsCaseSensitive; + export import compareCaseInsensitive = ts.comparePathsCaseInsensitive; + export import dirname = ts.getDirectoryPath; + export import basename = ts.getBaseFileName; + export import extname = ts.getAnyExtensionFromPath; + export import relative = ts.getRelativePath; + export import beneath = ts.containsPath; + export import changeExtension = ts.changeAnyExtension; + export import isTypeScript = ts.hasTypeScriptFileExtension; + export import isJavaScript = ts.hasJavaScriptFileExtension; + const invalidRootComponentRegExp = /^(?!(\/|\/\/\w+\/|[a-zA-Z]:\/?|)$)/; const invalidNavigableComponentRegExp = /[:*?"<>|]/; const invalidNonNavigableComponentRegExp = /^\.{1,2}$|[:*?"<>|]/; + const extRegExp = /\.\w+$/; export const enum ValidationFlags { None = 0, @@ -32,17 +58,6 @@ namespace vpath { Basename = RequireBasename | AllowExtname, } - export function valid(path: string, flags: ValidationFlags = ValidationFlags.RelativeOrAbsolute) { - return validateComponents(parse(path), flags, hasTrailingSeparator(path)); - } - - export function validate(path: string, flags: ValidationFlags = ValidationFlags.RelativeOrAbsolute) { - const components = parse(path); - const trailing = hasTrailingSeparator(path); - if (!validateComponents(components, flags, trailing)) throw vfs.createIOError("ENOENT"); - return components.length > 1 && trailing ? format(reduce(components)) + sep : format(reduce(components)); - } - function validateComponents(components: string[], flags: ValidationFlags, hasTrailingSeparator: boolean) { const hasRoot = !!components[0]; const hasDirname = components.length > 2; @@ -80,244 +95,11 @@ namespace vpath { return true; } - import getRootLength = ts.getRootLength; - - export import sep = ts.directorySeparator; - export import normalizeSeparators = ts.normalizeSlashes; - export import isAbsolute = ts.isRootedDiskPath; - export import isRoot = ts.isDiskPathRoot; - export import hasTrailingSeparator = ts.hasTrailingDirectorySeparator; - export import addTrailingSeparator = ts.ensureTrailingDirectorySeparator; - export import removeTrailingSeparator = ts.removeTrailingDirectorySeparator; - export import normalize = ts.normalizePath; - export import combine = ts.combinePaths; - export import parse = ts.getPathComponents; - export import reduce = ts.reducePathComponents; - export import format = ts.getNormalizedPathFromPathComponents; - - /** - * Combines and normalizes two paths. - */ - export function resolve(path1: string, path2: string) { - return normalize(combine(path1, path2)); - } - - // NOTE: this differs from `ts.getRelativePathToDirectoryOrUrl` in that it requires both paths - // are already absolute and does not perform "canonicalization". - function relativeWorker(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean) { - if (!isAbsolute(from)) throw new Error("Path not absolute"); - if (!isAbsolute(to)) throw new Error("Path not absolute"); - - const fromComponents = reduce(parse(from)); - const toComponents = reduce(parse(to)); - - let start: number; - for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { - if (!stringEqualityComparer(fromComponents[start], toComponents[start])) { - break; - } - } - - if (start === 0 || (start === 1 && fromComponents[0] === "/")) { - return format(toComponents); - } - - const components = toComponents.slice(start); - for (; start < fromComponents.length; start++) { - components.unshift(".."); - } - - return format(["", ...components]); - } - - function relativeCaseSensitive(from: string, to: string) { - return relativeWorker(from, to, ts.equateStringsCaseSensitive); - } - - function relativeCaseInsensitive(from: string, to: string) { - return relativeWorker(from, to, ts.equateStringsCaseInsensitive); - } - - /** - * Gets a relative path that can be used to traverse between `from` and `to`. - */ - // NOTE: this differs from `ts.getRelativePathToDirectoryOrUrl` in that it requires both paths - // are already absolute and does not perform "canonicalization". - export function relative(from: string, to: string, ignoreCase: boolean) { - return ignoreCase ? relativeCaseInsensitive(from, to) : relativeCaseSensitive(from, to); - } - - // NOTE: this differs from `ts.comparePaths` due to the behavior of `parse`. - function compareWorker(a: string, b: string, stringComparer: (a: string, b: string) => number) { - if (a === b) return 0; - a = removeTrailingSeparator(a); - b = removeTrailingSeparator(b); - if (a === b) return 0; - const aComponents = reduce(parse(a)); - const bComponents = reduce(parse(b)); - const len = Math.min(aComponents.length, bComponents.length); - for (let i = 0; i < len; i++) { - const result = stringComparer(aComponents[i], bComponents[i]); - if (result !== 0) return result; - } - return ts.compareValues(aComponents.length, bComponents.length); - } - - /** - * Performs a case-sensitive comparison of two paths. - */ - export function compareCaseSensitive(a: string, b: string) { - return compareWorker(a, b, ts.compareStringsCaseSensitive); - } - - /** - * Performs a case-insensitive comparison of two paths. - */ - export function compareCaseInsensitive(a: string, b: string) { - return compareWorker(a, b, ts.compareStringsCaseInsensitive); - } - - /** - * Compare two paths. - */ - // NOTE: this differs from `ts.comparePaths` due to the behavior of `parse`. - export function compare(a: string, b: string, ignoreCase: boolean) { - return ignoreCase ? compareCaseInsensitive(a, b) : compareCaseSensitive(a, b); - } - - /** - * Determines whether two paths are equal. - */ - export function equals(a: string, b: string, ignoreCase: boolean) { - if (!isAbsolute(a)) throw new Error("Path not absolute"); - if (!isAbsolute(b)) throw new Error("Path not absolute"); - if (a === b) return true; - a = removeTrailingSeparator(a); - b = removeTrailingSeparator(b); - if (a === b) return true; - a = normalize(a); - b = normalize(b); - if (a === b) return true; - return ignoreCase && a.toUpperCase() === b.toUpperCase(); - } - - // NOTE: this differs from `ts.containsPath` due to the behavior of `parse`. - function beneathWorker(ancestor: string, descendant: string, stringEqualityComparer: (a: string, b: string) => boolean) { - if (!isAbsolute(ancestor)) throw new Error("Path not absolute"); - if (!isAbsolute(descendant)) throw new Error("Path not absolute"); - const ancestorComponents = reduce(parse(ancestor)); - const descendantComponents = reduce(parse(descendant)); - if (descendantComponents.length < ancestorComponents.length) return false; - for (let i = 0; i < ancestorComponents.length; i++) { - if (!stringEqualityComparer(ancestorComponents[i], descendantComponents[i])) { - return false; - } - } - return true; - } - - function beneathCaseSensitive(ancestor: string, descendant: string) { - return beneathWorker(ancestor, descendant, ts.equateStringsCaseSensitive); - } - - function beneathCaseInsensitive(ancestor: string, descendant: string) { - return beneathWorker(ancestor, descendant, ts.equateStringsCaseInsensitive); - } - - /** - * Determines whether the path `descendant` is beneath the path `ancestor`. - */ - // NOTE: this differs from `containsPath` in compiler/core.ts due to the behavior of `parse`. - export function beneath(ancestor: string, descendant: string, ignoreCase: boolean) { - return ignoreCase ? beneathCaseInsensitive(ancestor, descendant) : beneathCaseSensitive(ancestor, descendant); - } - - /** - * Gets the parent directory name of a path. - */ - // NOTE: this differs from `ts.getDirectoryPath` due to the behavior of `getRootLength`. - export function dirname(path: string) { - path = normalizeSeparators(path); - path = removeTrailingSeparator(path); - return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(sep))); - } - - /** - * Gets the portion of a path following the last separator (`/`). - */ - export function basename(path: string): string; - /** - * Gets the portion of a path following the last separator (`/`). - * If the base name has any one of the provided extensions, it is removed. - */ - export function basename(path: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; - // NOTE: this differs from `ts.getBaseFileName` in that this function handles extensions in a - // fashion similar to the NodeJS `path.basename` function as well as handles case sensitivity. - export function basename(path: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) { - path = normalizeSeparators(path); - path = removeTrailingSeparator(path); - const name = path.substr(Math.max(getRootLength(path), path.lastIndexOf(sep) + 1)); - const extension = extensions !== undefined && ignoreCase !== undefined ? extname(path, extensions, ignoreCase) : undefined; - return extension ? name.slice(0, name.length - extension.length) : name; - } - - function extnameWorker(path: string, extensions: string | ReadonlyArray, stringEqualityComparer: (a: string, b: string) => boolean) { - const manyExtensions = Array.isArray(extensions) ? extensions : undefined; - const singleExtension = Array.isArray(extensions) ? undefined : extensions; - const length = manyExtensions ? manyExtensions.length : 1; - for (let i = 0; i < length; i++) { - let extension = manyExtensions ? manyExtensions[i] : singleExtension; - if (!extension.startsWith(".")) extension = "." + extension; - if (path.length >= extension.length && path.charAt(path.length - extension.length) === ".") { - const pathExtension = path.slice(path.length - extension.length); - if (stringEqualityComparer(pathExtension, extension)) { - return pathExtension; - } - } - } - return ""; - } - - const extRegExp = /\.\w+$/; - - /** - * Gets the file extension for a path. - */ - export function extname(path: string): string; - /** - * Gets the file extension for a path, provided it is one of the provided extensions. - */ - export function extname(path: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; - // NOTE: this differs from `ts.getAnyExtensionFromPath` in that this function allows you to - // restrict extensions and handle case sensitivity - export function extname(path: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) { - if (extensions) { - return extnameWorker(path, extensions, ignoreCase ? ts.equateStringsCaseInsensitive : ts.equateStringsCaseSensitive); - } - - const match = extRegExp.exec(path); - return match ? match[0] : ""; - } - - export function changeExtension(path: string, ext: string): string; - export function changeExtension(path: string, ext: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string; - // NOTE: this differs from `ts.changeExtension` in that this function allows you to - // specify extensions and handle case sensitivity - export function changeExtension(path: string, ext: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) { - const pathext = extensions !== undefined && ignoreCase !== undefined ? extname(path, extensions, ignoreCase) : extname(path); - return pathext ? path.slice(0, path.length - pathext.length) + (ext.startsWith(".") ? ext : "." + ext) : path; - } - - const typeScriptExtensions: ReadonlyArray = [".ts", ".tsx"]; - - export function isTypeScript(path: string) { - return extname(path, typeScriptExtensions, /*ignoreCase*/ false).length > 0; - } - - const javaScriptExtensions: ReadonlyArray = [".js", ".jsx"]; - - export function isJavaScript(path: string) { - return extname(path, javaScriptExtensions, /*ignoreCase*/ false).length > 0; + export function validate(path: string, flags: ValidationFlags = ValidationFlags.RelativeOrAbsolute) { + const components = parse(path); + const trailing = hasTrailingSeparator(path); + if (!validateComponents(components, flags, trailing)) throw vfs.createIOError("ENOENT"); + return components.length > 1 && trailing ? format(reduce(components)) + sep : format(reduce(components)); } export function isDeclaration(path: string) { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index fa466eb8cb9..46d6a108d47 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -266,7 +266,7 @@ namespace ts.codefix { return [global]; } - const relativePath = removeExtensionAndIndexPostFix(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName), moduleResolutionKind, addJsExtension); + const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePath(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension); if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") { return [relativePath]; } @@ -321,7 +321,7 @@ namespace ts.codefix { 1 < 2 = true In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a". */ - const pathFromSourceToBaseUrl = getRelativePath(baseUrl, sourceDirectory, getCanonicalFileName); + const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePath(sourceDirectory, baseUrl, getCanonicalFileName)); const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl); return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath]; }); @@ -343,11 +343,12 @@ namespace ts.codefix { } function getRelativePathNParents(relativePath: string): number { - let count = 0; - for (let i = 0; i + 3 <= relativePath.length && relativePath.slice(i, i + 3) === "../"; i += 3) { - count++; + const components = getPathComponents(relativePath); + if (components[0] || components.length === 1) return 0; + for (let i = 1; i < components.length; i++) { + if (components[i] !== "..") return i - 1; } - return count; + return components.length - 1; } function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined { @@ -389,7 +390,7 @@ namespace ts.codefix { } const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); - const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath, getCanonicalFileName) : normalizedTargetPath; + const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePath(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath; return removeFileExtension(relativePath); } @@ -472,7 +473,7 @@ namespace ts.codefix { return path.substring(parts.topLevelPackageNameIndex + 1); } else { - return getRelativePath(path, sourceDirectory, getCanonicalFileName); + return ensurePathIsNonModuleName(getRelativePath(sourceDirectory, path, getCanonicalFileName)); } } } diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 09bbdc11e78..741ba977184 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -63,10 +63,10 @@ namespace ts { function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined { // Get the relative path from old to new location, and append it on to the end of imports and normalize. - const rel = getRelativePath(newFilePath, getDirectoryPath(oldFilePath), createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); + const rel = ensurePathIsNonModuleName(getRelativePath(getDirectoryPath(oldFilePath), newFilePath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)))); return oldPath => { if (!pathIsRelative(oldPath)) return; - return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel))); + return ensurePathIsNonModuleName(normalizePath(combinePaths(getDirectoryPath(oldPath), rel))); }; } diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts index 99ce0afb36c..7fd3b7cfd0b 100644 --- a/src/services/pathCompletions.ts +++ b/src/services/pathCompletions.ts @@ -89,7 +89,9 @@ namespace ts.Completions.PathCompletions { * Remove the basename from the path. Note that we don't use the basename to filter completions; * the client is responsible for refining completions. */ - fragment = getDirectoryPath(fragment); + if (!hasTrailingDirectorySeparator(fragment)) { + fragment = getDirectoryPath(fragment); + } if (fragment === "") { fragment = "." + directorySeparator; @@ -97,8 +99,9 @@ namespace ts.Completions.PathCompletions { fragment = ensureTrailingDirectorySeparator(fragment); - const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); - const baseDirectory = getDirectoryPath(absolutePath); + // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths + const absolutePath = resolvePath(scriptPath, fragment); + const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath); const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); if (tryDirectoryExists(host, baseDirectory)) { @@ -178,7 +181,7 @@ namespace ts.Completions.PathCompletions { } } - const fragmentDirectory = containsSlash(fragment) ? getDirectoryPath(fragment) : undefined; + const fragmentDirectory = containsSlash(fragment) ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined; for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) { result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName)); } @@ -239,14 +242,15 @@ namespace ts.Completions.PathCompletions { // The prefix has two effective parts: the directory path and the base component after the filepath that is not a // full directory component. For example: directory/path/of/prefix/base* - const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix); - const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix); - const normalizedPrefixBase = getBaseFileName(normalizedPrefix); + const normalizedPrefix = resolvePath(parsed.prefix); + const normalizedPrefixDirectory = hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : getDirectoryPath(normalizedPrefix); + const normalizedPrefixBase = hasTrailingDirectorySeparator(parsed.prefix) ? "" : getBaseFileName(normalizedPrefix); const fragmentHasPath = containsSlash(fragment); + const fragmentDirectory = fragmentHasPath ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined; // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call - const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory; + const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory; const normalizedSuffix = normalizePath(parsed.suffix); // Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b". @@ -418,16 +422,6 @@ namespace ts.Completions.PathCompletions { return false; } - function normalizeAndPreserveTrailingSlash(path: string) { - if (normalizeSlashes(path) === "./") { - // normalizePath turns "./" into "". "" + "/" would then be a rooted path instead of a relative one, so avoid this particular case. - // There is no problem for adding "/" to a non-empty string -- it's only a problem at the beginning. - return ""; - } - const norm = normalizePath(path); - return hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(norm) : norm; - } - /** * Matches a triple slash reference directive with an incomplete string literal for its path. Used * to determine if the caret is currently within the string literal and capture the literal fragment diff --git a/tests/baselines/reference/importWithTrailingSlash.js b/tests/baselines/reference/importWithTrailingSlash.js index 9df01dcee66..cf1db0a2c56 100644 --- a/tests/baselines/reference/importWithTrailingSlash.js +++ b/tests/baselines/reference/importWithTrailingSlash.js @@ -38,6 +38,6 @@ _2["default"].aIndex; "use strict"; exports.__esModule = true; var __1 = require(".."); -var _1 = require("../"); +var __2 = require("../"); __1["default"].a; -_1["default"].aIndex; +__2["default"].aIndex;