diff --git a/Gulpfile.js b/Gulpfile.js index b5ed366ee64ff..4c40a73cdba6e 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -90,8 +90,18 @@ const localize = async () => { } }; +const buildShims = () => buildProject("src/shims"); +const cleanShims = () => cleanProject("src/shims"); +cleanTasks.push(cleanShims); + +const buildDebugTools = () => buildProject("src/debug"); +const cleanDebugTools = () => cleanProject("src/debug"); +cleanTasks.push(cleanDebugTools); + +const buildShimsAndTools = parallel(buildShims, buildDebugTools); + // Pre-build steps when targeting the LKG compiler -const lkgPreBuild = parallel(generateLibs, series(buildScripts, generateDiagnostics)); +const lkgPreBuild = parallel(generateLibs, series(buildScripts, generateDiagnostics, buildShimsAndTools)); const buildTsc = () => buildProject("src/tsc"); task("tsc", series(lkgPreBuild, buildTsc)); @@ -107,7 +117,7 @@ task("watch-tsc", series(lkgPreBuild, parallel(watchLib, watchDiagnostics, watch task("watch-tsc").description = "Watch for changes and rebuild the command-line compiler only."; // Pre-build steps when targeting the built/local compiler. -const localPreBuild = parallel(generateLibs, series(buildScripts, generateDiagnostics, buildTsc)); +const localPreBuild = parallel(generateLibs, series(buildScripts, generateDiagnostics, buildShimsAndTools, buildTsc)); // Pre-build steps to use based on supplied options. const preBuild = cmdLineOptions.lkg ? lkgPreBuild : localPreBuild; diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 6bd6045a55101..a3d2e870d53a1 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -161,7 +161,12 @@ namespace ts { IsObjectLiteralOrClassExpressionMethod = 1 << 7, } - let flowNodeCreated: (node: T) => T = identity; + function initFlowNode(node: T) { + Debug.attachFlowNodeDebugInfo(node); + return node; + } + + let flowNodeCreated: (node: T) => T = initFlowNode; const binder = createBinder(); @@ -238,6 +243,10 @@ namespace ts { Symbol = objectAllocator.getSymbolConstructor(); + // Attach debugging information if necessary + Debug.attachFlowNodeDebugInfo(unreachableFlow); + Debug.attachFlowNodeDebugInfo(reportedUnreachableFlow); + if (!file.locals) { bind(file); file.symbolCount = symbolCount; @@ -626,7 +635,7 @@ namespace ts { // A non-async, non-generator IIFE is considered part of the containing control flow. Return statements behave // similarly to break statements that exit to a label just past the statement body. if (!isIIFE) { - currentFlow = { flags: FlowFlags.Start }; + currentFlow = initFlowNode({ flags: FlowFlags.Start }); if (containerFlags & (ContainerFlags.IsFunctionExpression | ContainerFlags.IsObjectLiteralOrClassExpressionMethod)) { currentFlow.node = node; } @@ -638,7 +647,7 @@ namespace ts { currentContinueTarget = undefined; activeLabels = undefined; hasExplicitReturn = false; - flowNodeCreated = identity; + flowNodeCreated = initFlowNode; bindChildren(node); // Reset all reachability check related flags on node (for incremental scenarios) node.flags &= ~NodeFlags.ReachabilityAndEmitFlags; @@ -920,11 +929,11 @@ namespace ts { } function createBranchLabel(): FlowLabel { - return { flags: FlowFlags.BranchLabel, antecedents: undefined }; + return initFlowNode({ flags: FlowFlags.BranchLabel, antecedents: undefined }); } function createLoopLabel(): FlowLabel { - return { flags: FlowFlags.LoopLabel, antecedents: undefined }; + return initFlowNode({ flags: FlowFlags.LoopLabel, antecedents: undefined }); } function setFlowNodeReferenced(flow: FlowNode) { @@ -1194,7 +1203,7 @@ namespace ts { // as possible antecedents of the start of the `catch` or `finally` blocks. // Don't bother intercepting the call if there's no finally or catch block that needs the information if (node.catchClause || node.finallyBlock) { - flowNodeCreated = node => (tryPriors.push(node), node); + flowNodeCreated = node => (tryPriors.push(node), initFlowNode(node)); } bind(node.tryBlock); flowNodeCreated = oldFlowNodeCreated; @@ -1263,7 +1272,7 @@ namespace ts { // // extra edges that we inject allows to control this behavior // if when walking the flow we step on post-finally edge - we can mark matching pre-finally edge as locked so it will be skipped. - const preFinallyFlow: PreFinallyFlow = { flags: FlowFlags.PreFinally, antecedent: preFinallyPrior, lock: {} }; + const preFinallyFlow: PreFinallyFlow = initFlowNode({ flags: FlowFlags.PreFinally, antecedent: preFinallyPrior, lock: {} }); addAntecedent(preFinallyLabel, preFinallyFlow); currentFlow = finishFlowLabel(preFinallyLabel); @@ -1984,7 +1993,7 @@ namespace ts { const host = getJSDocHost(typeAlias); container = findAncestor(host.parent, n => !!(getContainerFlags(n) & ContainerFlags.IsContainer)) || file; blockScopeContainer = getEnclosingBlockScopeContainer(host) || file; - currentFlow = { flags: FlowFlags.Start }; + currentFlow = initFlowNode({ flags: FlowFlags.Start }); parent = typeAlias; bind(typeAlias.typeExpression); const declName = getNameOfDeclaration(typeAlias); diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 8703f3de8f7c2..4941257cabf64 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -42,6 +42,12 @@ namespace ts { clear(): void; } + /* @internal */ + export interface MapConstructor { + // eslint-disable-next-line @typescript-eslint/prefer-function-type + new (): Map; + } + /** ES6 Iterator type. */ export interface Iterator { next(): { value: T, done?: false } | { value: never, done: true }; @@ -68,26 +74,38 @@ namespace ts { /* @internal */ namespace ts { - export const emptyArray: never[] = [] as never[]; + // Natives + // NOTE: This must be declared in a separate block from the one below so that we don't collide with the exported definition of `Map`. + declare const Map: (new () => Map) | undefined; - /** Create a MapLike with good performance. */ - function createDictionaryObject(): MapLike { - const map = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null + /** + * Returns the native Map implementation if it is available and compatible (i.e. supports iteration). + */ + export function tryGetNativeMap(): MapConstructor | undefined { + // Internet Explorer's Map doesn't support iteration, so don't use it. + // eslint-disable-next-line no-in-operator + return typeof Map !== "undefined" && "entries" in Map.prototype ? Map : undefined; + } +} - // Using 'delete' on an object causes V8 to put the object in dictionary mode. - // This disables creation of hidden classes, which are expensive when an object is - // constantly changing shape. - map.__ = undefined; - delete map.__; +/* @internal */ +namespace ts { + export const emptyArray: never[] = [] as never[]; - return map; - } + export const Map: MapConstructor = tryGetNativeMap() || (() => { + // NOTE: createMapShim will be defined for typescriptServices.js but not for tsc.js, so we must test for it. + if (typeof createMapShim === "function") { + return createMapShim(); + } + throw new Error("TypeScript requires an environment that provides a compatible native Map implementation."); + })(); - /** Create a new map. If a template object is provided, the map will copy entries from it. */ + /** Create a new map. */ export function createMap(): Map { - return new MapCtr(); + return new Map(); } + /** Create a new map from an array of entries. */ export function createMapFromEntries(entries: [string, T][]): Map { const map = createMap(); for (const [key, value] of entries) { @@ -96,8 +114,9 @@ namespace ts { return map; } + /** Create a new map from a template object is provided, the map will copy entries from it. */ export function createMapFromTemplate(template: MapLike): Map { - const map: Map = new MapCtr(); + const map: Map = new Map(); // Copies keys/values from template. Note that for..in will not throw if // template is undefined, and instead will just exit the loop. @@ -110,204 +129,6 @@ namespace ts { return map; } - // The global Map object. This may not be available, so we must test for it. - declare const Map: (new () => Map) | undefined; - // Internet Explorer's Map doesn't support iteration, so don't use it. - // eslint-disable-next-line no-in-operator - export const MapCtr = typeof Map !== "undefined" && "entries" in Map.prototype ? Map : shimMap(); - - // Keep the class inside a function so it doesn't get compiled if it's not used. - export function shimMap(): new () => Map { - - interface MapEntry { - readonly key?: string; - value?: T; - - // Linked list references for iterators. - nextEntry?: MapEntry; - previousEntry?: MapEntry; - - /** - * Specifies if iterators should skip the next entry. - * This will be set when an entry is deleted. - * See https://github.com/Microsoft/TypeScript/pull/27292 for more information. - */ - skipNext?: boolean; - } - - class MapIterator { - private currentEntry?: MapEntry; - private selector: (key: string, value: T) => U; - - constructor(currentEntry: MapEntry, selector: (key: string, value: T) => U) { - this.currentEntry = currentEntry; - this.selector = selector; - } - - public next(): { value: U, done: false } | { value: never, done: true } { - // Navigate to the next entry. - while (this.currentEntry) { - const skipNext = !!this.currentEntry.skipNext; - this.currentEntry = this.currentEntry.nextEntry; - - if (!skipNext) { - break; - } - } - - if (this.currentEntry) { - return { value: this.selector(this.currentEntry.key!, this.currentEntry.value!), done: false }; - } - else { - return { value: undefined as never, done: true }; - } - } - } - - return class implements Map { - private data = createDictionaryObject>(); - public size = 0; - - // Linked list references for iterators. - // See https://github.com/Microsoft/TypeScript/pull/27292 - // for more information. - - /** - * The first entry in the linked list. - * Note that this is only a stub that serves as starting point - * for iterators and doesn't contain a key and a value. - */ - private readonly firstEntry: MapEntry; - private lastEntry: MapEntry; - - constructor() { - // Create a first (stub) map entry that will not contain a key - // and value but serves as starting point for iterators. - this.firstEntry = {}; - // When the map is empty, the last entry is the same as the - // first one. - this.lastEntry = this.firstEntry; - } - - get(key: string): T | undefined { - const entry = this.data[key] as MapEntry | undefined; - return entry && entry.value!; - } - - set(key: string, value: T): this { - if (!this.has(key)) { - this.size++; - - // Create a new entry that will be appended at the - // end of the linked list. - const newEntry: MapEntry = { - key, - value - }; - this.data[key] = newEntry; - - // Adjust the references. - const previousLastEntry = this.lastEntry; - previousLastEntry.nextEntry = newEntry; - newEntry.previousEntry = previousLastEntry; - this.lastEntry = newEntry; - } - else { - this.data[key].value = value; - } - - return this; - } - - has(key: string): boolean { - // eslint-disable-next-line no-in-operator - return key in this.data; - } - - delete(key: string): boolean { - if (this.has(key)) { - this.size--; - const entry = this.data[key]; - delete this.data[key]; - - // Adjust the linked list references of the neighbor entries. - const previousEntry = entry.previousEntry!; - previousEntry.nextEntry = entry.nextEntry; - if (entry.nextEntry) { - entry.nextEntry.previousEntry = previousEntry; - } - - // When the deleted entry was the last one, we need to - // adjust the lastEntry reference. - if (this.lastEntry === entry) { - this.lastEntry = previousEntry; - } - - // Adjust the forward reference of the deleted entry - // in case an iterator still references it. This allows us - // to throw away the entry, but when an active iterator - // (which points to the current entry) continues, it will - // navigate to the entry that originally came before the - // current one and skip it. - entry.previousEntry = undefined; - entry.nextEntry = previousEntry; - entry.skipNext = true; - - return true; - } - return false; - } - - clear(): void { - this.data = createDictionaryObject>(); - this.size = 0; - - // Reset the linked list. Note that we must adjust the forward - // references of the deleted entries to ensure iterators stuck - // in the middle of the list don't continue with deleted entries, - // but can continue with new entries added after the clear() - // operation. - const firstEntry = this.firstEntry; - let currentEntry = firstEntry.nextEntry; - while (currentEntry) { - const nextEntry = currentEntry.nextEntry; - currentEntry.previousEntry = undefined; - currentEntry.nextEntry = firstEntry; - currentEntry.skipNext = true; - - currentEntry = nextEntry; - } - firstEntry.nextEntry = undefined; - this.lastEntry = firstEntry; - } - - keys(): Iterator { - return new MapIterator(this.firstEntry, key => key); - } - - values(): Iterator { - return new MapIterator(this.firstEntry, (_key, value) => value); - } - - entries(): Iterator<[string, T]> { - return new MapIterator(this.firstEntry, (key, value) => [key, value] as [string, T]); - } - - forEach(action: (value: T, key: string) => void): void { - const iterator = this.entries(); - while (true) { - const iterResult = iterator.next(); - if (iterResult.done) { - break; - } - - const [key, value] = iterResult.value; - action(value, key); - } - } - }; - } - export function length(array: readonly any[] | undefined): number { return array ? array.length : 0; } @@ -2052,20 +1873,6 @@ namespace ts { return str.indexOf(substring) !== -1; } - export function fileExtensionIs(path: string, extension: string): boolean { - return path.length > extension.length && endsWith(path, extension); - } - - export function fileExtensionIsOneOf(path: string, extensions: readonly string[]): boolean { - for (const extension of extensions) { - if (fileExtensionIs(path, extension)) { - return true; - } - } - - return false; - } - /** * Takes a string like "jquery-min.4.2.3" and returns "jquery" */ diff --git a/src/compiler/debug.ts b/src/compiler/debug.ts index 09d230f24e9e8..40ac6c9c94f60 100644 --- a/src/compiler/debug.ts +++ b/src/compiler/debug.ts @@ -221,6 +221,40 @@ namespace ts { let isDebugInfoEnabled = false; + interface ExtendedDebugModule { + init(_ts: typeof ts): void; + formatControlFlowGraph(flowNode: FlowNode): string; + } + + let extendedDebugModule: ExtendedDebugModule | undefined; + + function extendedDebug() { + enableDebugInfo(); + if (!extendedDebugModule) { + throw new Error("Debugging helpers could not be loaded."); + } + return extendedDebugModule; + } + + export function printControlFlowGraph(flowNode: FlowNode) { + return console.log(formatControlFlowGraph(flowNode)); + } + + export function formatControlFlowGraph(flowNode: FlowNode) { + return extendedDebug().formatControlFlowGraph(flowNode); + } + + export function attachFlowNodeDebugInfo(flowNode: FlowNode) { + if (isDebugInfoEnabled) { + if (!("__debugFlowFlags" in flowNode)) { // eslint-disable-line no-in-operator + Object.defineProperties(flowNode, { + __debugFlowFlags: { get(this: FlowNode) { return formatEnum(this.flags, (ts as any).FlowFlags, /*isFlags*/ true); } }, + __debugToString: { value(this: FlowNode) { return formatControlFlowGraph(this); } } + }); + } + } + } + /** * Injects debug information into frequently used types. */ @@ -266,6 +300,21 @@ namespace ts { } } + // attempt to load extended debugging information + try { + if (sys && sys.require) { + const basePath = getDirectoryPath(resolvePath(sys.getExecutingFilePath())); + const result = sys.require(basePath, "./compiler-debug") as RequireResult; + if (!result.error) { + result.module.init(ts); + extendedDebugModule = result.module; + } + } + } + catch { + // do nothing + } + isDebugInfoEnabled = true; } diff --git a/src/compiler/path.ts b/src/compiler/path.ts new file mode 100644 index 0000000000000..d7d8d964d542b --- /dev/null +++ b/src/compiler/path.ts @@ -0,0 +1,855 @@ +/* @internal */ +namespace ts { + /** + * 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; + + //// Path Tests + + /** + * Determines whether a charCode corresponds to `/` or `\`. + */ + export function isAnyDirectorySeparator(charCode: number): boolean { + return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash; + } + + /** + * Determines whether a path starts with a URL scheme (e.g. starts with `http://`, `ftp://`, `file://`, etc.). + */ + export function isUrl(path: string) { + return getEncodedRootLength(path) < 0; + } + + /** + * Determines whether a path is an absolute disk path (e.g. starts with `/`, or a dos path + * like `c:`, `c:\` or `c:/`). + */ + export function isRootedDiskPath(path: string) { + return getEncodedRootLength(path) > 0; + } + + /** + * Determines whether a path consists only of a path root. + */ + export function isDiskPathRoot(path: string) { + const rootLength = getEncodedRootLength(path); + return rootLength > 0 && rootLength === path.length; + } + + /** + * Determines whether a path starts with an absolute path component (i.e. `/`, `c:/`, `file://`, etc.). + * + * ```ts + * // POSIX + * pathIsAbsolute("/path/to/file.ext") === true + * // DOS + * pathIsAbsolute("c:/path/to/file.ext") === true + * // URL + * pathIsAbsolute("file:///path/to/file.ext") === true + * // Non-absolute + * pathIsAbsolute("path/to/file.ext") === false + * pathIsAbsolute("./path/to/file.ext") === false + * ``` + */ + export function pathIsAbsolute(path: string): boolean { + return getEncodedRootLength(path) !== 0; + } + + /** + * Determines whether a path starts with a relative path component (i.e. `.` or `..`). + */ + export function pathIsRelative(path: string): boolean { + return /^\.\.?($|[\\/])/.test(path); + } + + export function hasExtension(fileName: string): boolean { + return stringContains(getBaseFileName(fileName), "."); + } + + export function fileExtensionIs(path: string, extension: string): boolean { + return path.length > extension.length && endsWith(path, extension); + } + + export function fileExtensionIsOneOf(path: string, extensions: readonly string[]): boolean { + for (const extension of extensions) { + if (fileExtensionIs(path, extension)) { + return true; + } + } + + return false; + } + + /** + * Determines whether a path has a trailing separator (`/` or `\\`). + */ + export function hasTrailingDirectorySeparator(path: string) { + return path.length > 0 && isAnyDirectorySeparator(path.charCodeAt(path.length - 1)); + } + + //// Path Parsing + + 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; + } + + /** + * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` + * except that we support URLs as well. + * + * ```ts + * // POSIX + * getDirectoryPath("/path/to/file.ext") === "/path/to" + * getDirectoryPath("/path/to/") === "/path" + * getDirectoryPath("/") === "/" + * // DOS + * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to" + * getDirectoryPath("c:/path/to/") === "c:/path" + * getDirectoryPath("c:/") === "c:/" + * getDirectoryPath("c:") === "c:" + * // URL + * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to" + * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path" + * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/" + * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org" + * ``` + */ + export function getDirectoryPath(path: Path): Path; + /** + * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname` + * except that we support URLs as well. + * + * ```ts + * // POSIX + * getDirectoryPath("/path/to/file.ext") === "/path/to" + * getDirectoryPath("/path/to/") === "/path" + * getDirectoryPath("/") === "/" + * // DOS + * getDirectoryPath("c:/path/to/file.ext") === "c:/path/to" + * getDirectoryPath("c:/path/to/") === "c:/path" + * getDirectoryPath("c:/") === "c:/" + * getDirectoryPath("c:") === "c:" + * // URL + * getDirectoryPath("http://typescriptlang.org/path/to/file.ext") === "http://typescriptlang.org/path/to" + * getDirectoryPath("http://typescriptlang.org/path/to") === "http://typescriptlang.org/path" + * getDirectoryPath("http://typescriptlang.org/") === "http://typescriptlang.org/" + * getDirectoryPath("http://typescriptlang.org") === "http://typescriptlang.org" + * getDirectoryPath("file://server/path/to/file.ext") === "file://server/path/to" + * getDirectoryPath("file://server/path/to") === "file://server/path" + * getDirectoryPath("file://server/") === "file://server/" + * getDirectoryPath("file://server") === "file://server" + * getDirectoryPath("file:///path/to/file.ext") === "file:///path/to" + * getDirectoryPath("file:///path/to") === "file:///path" + * getDirectoryPath("file:///") === "file:///" + * getDirectoryPath("file://") === "file://" + * ``` + */ + 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))); + } + + /** + * Returns the path except for its containing directory name. + * Semantics align with NodeJS's `path.basename` except that we support URL's as well. + * + * ```ts + * // POSIX + * getBaseFileName("/path/to/file.ext") === "file.ext" + * getBaseFileName("/path/to/") === "to" + * getBaseFileName("/") === "" + * // DOS + * getBaseFileName("c:/path/to/file.ext") === "file.ext" + * getBaseFileName("c:/path/to/") === "to" + * getBaseFileName("c:/") === "" + * getBaseFileName("c:") === "" + * // URL + * getBaseFileName("http://typescriptlang.org/path/to/file.ext") === "file.ext" + * getBaseFileName("http://typescriptlang.org/path/to/") === "to" + * getBaseFileName("http://typescriptlang.org/") === "" + * getBaseFileName("http://typescriptlang.org") === "" + * getBaseFileName("file://server/path/to/file.ext") === "file.ext" + * getBaseFileName("file://server/path/to/") === "to" + * getBaseFileName("file://server/") === "" + * getBaseFileName("file://server") === "" + * getBaseFileName("file:///path/to/file.ext") === "file.ext" + * getBaseFileName("file:///path/to/") === "to" + * getBaseFileName("file:///") === "" + * getBaseFileName("file://") === "" + * ``` + */ + export function getBaseFileName(path: string): string; + /** + * Gets the portion of a path following the last (non-terminal) 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. + * + * ```ts + * getBaseFileName("/path/to/file.ext", ".ext", true) === "file" + * getBaseFileName("/path/to/file.js", ".ext", true) === "file.js" + * getBaseFileName("/path/to/file.js", [".ext", ".js"], true) === "file" + * getBaseFileName("/path/to/file.ext", ".EXT", false) === "file.ext" + * ``` + */ + export function getBaseFileName(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; + export function getBaseFileName(path: string, extensions?: string | readonly string[], 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; + } + + function tryGetExtensionFromPath(path: string, extension: string, stringEqualityComparer: (a: string, b: string) => boolean) { + if (!startsWith(extension, ".")) extension = "." + extension; + if (path.length >= extension.length && path.charCodeAt(path.length - extension.length) === CharacterCodes.dot) { + const pathExtension = path.slice(path.length - extension.length); + if (stringEqualityComparer(pathExtension, extension)) { + return pathExtension; + } + } + } + + function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], stringEqualityComparer: (a: string, b: string) => boolean) { + if (typeof extensions === "string") { + return tryGetExtensionFromPath(path, extensions, stringEqualityComparer) || ""; + } + for (const extension of extensions) { + const result = tryGetExtensionFromPath(path, extension, stringEqualityComparer); + if (result) return result; + } + return ""; + } + + /** + * Gets the file extension for a path. + * + * ```ts + * getAnyExtensionFromPath("/path/to/file.ext") === ".ext" + * getAnyExtensionFromPath("/path/to/file.ext/") === ".ext" + * getAnyExtensionFromPath("/path/to/file") === "" + * getAnyExtensionFromPath("/path/to.ext/file") === "" + * ``` + */ + export function getAnyExtensionFromPath(path: string): string; + /** + * Gets the file extension for a path, provided it is one of the provided extensions. + * + * ```ts + * getAnyExtensionFromPath("/path/to/file.ext", ".ext", true) === ".ext" + * getAnyExtensionFromPath("/path/to/file.js", ".ext", true) === "" + * getAnyExtensionFromPath("/path/to/file.js", [".ext", ".js"], true) === ".js" + * getAnyExtensionFromPath("/path/to/file.ext", ".EXT", false) === "" + */ + export function getAnyExtensionFromPath(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; + export function getAnyExtensionFromPath(path: string, extensions?: string | readonly string[], 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(removeTrailingDirectorySeparator(path), extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive); + } + const baseFileName = getBaseFileName(path); + const extensionIndex = baseFileName.lastIndexOf("."); + if (extensionIndex >= 0) { + return baseFileName.substring(extensionIndex); + } + return ""; + } + + function pathComponents(path: string, rootLength: number) { + const root = path.substring(0, rootLength); + const rest = path.substring(rootLength).split(directorySeparator); + if (rest.length && !lastOrUndefined(rest)) rest.pop(); + return [root, ...rest]; + } + + /** + * Parse a path into an array containing a root component (at index 0) and zero or more path + * components (at indices > 0). The result is not normalized. + * If the path is relative, the root component is `""`. + * If the path is absolute, the root component includes the first path separator (`/`). + * + * ```ts + * // POSIX + * getPathComponents("/path/to/file.ext") === ["/", "path", "to", "file.ext"] + * getPathComponents("/path/to/") === ["/", "path", "to"] + * getPathComponents("/") === ["/"] + * // DOS + * getPathComponents("c:/path/to/file.ext") === ["c:/", "path", "to", "file.ext"] + * getPathComponents("c:/path/to/") === ["c:/", "path", "to"] + * getPathComponents("c:/") === ["c:/"] + * getPathComponents("c:") === ["c:"] + * // URL + * getPathComponents("http://typescriptlang.org/path/to/file.ext") === ["http://typescriptlang.org/", "path", "to", "file.ext"] + * getPathComponents("http://typescriptlang.org/path/to/") === ["http://typescriptlang.org/", "path", "to"] + * getPathComponents("http://typescriptlang.org/") === ["http://typescriptlang.org/"] + * getPathComponents("http://typescriptlang.org") === ["http://typescriptlang.org"] + * getPathComponents("file://server/path/to/file.ext") === ["file://server/", "path", "to", "file.ext"] + * getPathComponents("file://server/path/to/") === ["file://server/", "path", "to"] + * getPathComponents("file://server/") === ["file://server/"] + * getPathComponents("file://server") === ["file://server"] + * getPathComponents("file:///path/to/file.ext") === ["file:///", "path", "to", "file.ext"] + * getPathComponents("file:///path/to/") === ["file:///", "path", "to"] + * getPathComponents("file:///") === ["file:///"] + * getPathComponents("file://") === ["file://"] + */ + export function getPathComponents(path: string, currentDirectory = "") { + path = combinePaths(currentDirectory, path); + return pathComponents(path, getRootLength(path)); + } + + //// Path Formatting + + /** + * Formats a parsed path consisting of a root component (at index 0) and zero or more path + * segments (at indices > 0). + * + * ```ts + * getPathFromPathComponents(["/", "path", "to", "file.ext"]) === "/path/to/file.ext" + * ``` + */ + export function getPathFromPathComponents(pathComponents: readonly string[]) { + if (pathComponents.length === 0) return ""; + + const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]); + return root + pathComponents.slice(1).join(directorySeparator); + } + + //// Path Normalization + + /** + * Normalize path separators, converting `\` into `/`. + */ + export function normalizeSlashes(path: string): string { + return path.replace(backslashRegExp, directorySeparator); + } + + /** + * Reduce an array of path components to a more simplified path by navigating any + * `"."` or `".."` entries in the path. + */ + export function reducePathComponents(components: readonly string[]) { + if (!some(components)) return []; + const reduced = [components[0]]; + for (let i = 1; i < components.length; i++) { + const component = components[i]; + if (!component) continue; + if (component === ".") continue; + if (component === "..") { + if (reduced.length > 1) { + if (reduced[reduced.length - 1] !== "..") { + reduced.pop(); + continue; + } + } + else if (reduced[0]) continue; + } + reduced.push(component); + } + return reduced; + } + + /** + * Combines paths. If a path is absolute, it replaces any previous path. Relative paths are not simplified. + * + * ```ts + * // Non-rooted + * combinePaths("path", "to", "file.ext") === "path/to/file.ext" + * combinePaths("path", "dir", "..", "to", "file.ext") === "path/dir/../to/file.ext" + * // POSIX + * combinePaths("/path", "to", "file.ext") === "/path/to/file.ext" + * combinePaths("/path", "/to", "file.ext") === "/to/file.ext" + * // DOS + * combinePaths("c:/path", "to", "file.ext") === "c:/path/to/file.ext" + * combinePaths("c:/path", "c:/to", "file.ext") === "c:/to/file.ext" + * // URL + * combinePaths("file:///path", "to", "file.ext") === "file:///path/to/file.ext" + * combinePaths("file:///path", "file:///to", "file.ext") === "file:///to/file.ext" + * ``` + */ + export function combinePaths(path: string, ...paths: (string | undefined)[]): 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. Trailing directory separators are preserved. + * + * ```ts + * resolvePath("/path", "to", "file.ext") === "path/to/file.ext" + * resolvePath("/path", "to", "file.ext/") === "path/to/file.ext/" + * resolvePath("/path", "dir", "..", "to", "file.ext") === "path/to/file.ext" + * ``` + */ + export function resolvePath(path: string, ...paths: (string | undefined)[]): string { + return normalizePath(some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path)); + } + + /** + * Parse a path into an array containing a root component (at index 0) and zero or more path + * components (at indices > 0). The result is normalized. + * If the path is relative, the root component is `""`. + * If the path is absolute, the root component includes the first path separator (`/`). + * + * ```ts + * getNormalizedPathComponents("to/dir/../file.ext", "/path/") === ["/", "path", "to", "file.ext"] + */ + export function getNormalizedPathComponents(path: string, currentDirectory: string | undefined) { + return reducePathComponents(getPathComponents(path, currentDirectory)); + } + + export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined) { + return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); + } + + export function normalizePath(path: string): string { + path = normalizeSlashes(path); + const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(path))); + return normalized && hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalized) : normalized; + } + + function getPathWithoutRoot(pathComponents: readonly string[]) { + if (pathComponents.length === 0) return ""; + return pathComponents.slice(1).join(directorySeparator); + } + + export function getNormalizedAbsolutePathWithoutRoot(fileName: string, currentDirectory: string | undefined) { + return getPathWithoutRoot(getNormalizedPathComponents(fileName, currentDirectory)); + } + + export function toPath(fileName: string, basePath: string | undefined, getCanonicalFileName: (path: string) => string): Path { + const nonCanonicalizedPath = isRootedDiskPath(fileName) + ? normalizePath(fileName) + : getNormalizedAbsolutePath(fileName, basePath); + return getCanonicalFileName(nonCanonicalizedPath); + } + + 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 }; + } + } + + //// Path Mutation + + /** + * Removes a trailing directory separator from a path, if it does not already have one. + * + * ```ts + * removeTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext" + * removeTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext" + * ``` + */ + export function removeTrailingDirectorySeparator(path: Path): Path; + export function removeTrailingDirectorySeparator(path: string): string; + export function removeTrailingDirectorySeparator(path: string) { + if (hasTrailingDirectorySeparator(path)) { + return path.substr(0, path.length - 1); + } + + return path; + } + + /** + * Adds a trailing directory separator to a path, if it does not already have one. + * + * ```ts + * ensureTrailingDirectorySeparator("/path/to/file.ext") === "/path/to/file.ext/" + * ensureTrailingDirectorySeparator("/path/to/file.ext/") === "/path/to/file.ext/" + * ``` + */ + export function ensureTrailingDirectorySeparator(path: Path): Path; + export function ensureTrailingDirectorySeparator(path: string): string; + export function ensureTrailingDirectorySeparator(path: string) { + if (!hasTrailingDirectorySeparator(path)) { + return path + directorySeparator; + } + + return 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. + * + * ```ts + * ensurePathIsNonModuleName("/path/to/file.ext") === "/path/to/file.ext" + * ensurePathIsNonModuleName("./path/to/file.ext") === "./path/to/file.ext" + * ensurePathIsNonModuleName("../path/to/file.ext") === "../path/to/file.ext" + * ensurePathIsNonModuleName("path/to/file.ext") === "./path/to/file.ext" + * ``` + */ + export function ensurePathIsNonModuleName(path: string): string { + return !pathIsAbsolute(path) && !pathIsRelative(path) ? "./" + path : path; + } + + /** + * Changes the extension of a path to the provided extension. + * + * ```ts + * changeAnyExtension("/path/to/file.ext", ".js") === "/path/to/file.js" + * ``` + */ + export function changeAnyExtension(path: string, ext: string): string; + /** + * Changes the extension of a path to the provided extension if it has one of the provided extensions. + * + * ```ts + * changeAnyExtension("/path/to/file.ext", ".js", ".ext") === "/path/to/file.js" + * changeAnyExtension("/path/to/file.ext", ".js", ".ts") === "/path/to/file.ext" + * changeAnyExtension("/path/to/file.ext", ".js", [".ext", ".ts"]) === "/path/to/file.js" + * ``` + */ + export function changeAnyExtension(path: string, ext: string, extensions: string | readonly string[], ignoreCase: boolean): string; + export function changeAnyExtension(path: string, ext: string, extensions?: string | readonly string[], ignoreCase?: boolean) { + const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path); + return pathext ? path.slice(0, path.length - pathext.length) + (startsWith(ext, ".") ? ext : "." + ext) : path; + } + + //// Path Comparisons + + // check path for these segments: '', '.'. '..' + const relativePathSegmentRegExp = /(^|\/)\.{0,2}($|\/)/; + + 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; + + // NOTE: Performance optimization - shortcut if the root segments differ as there would be no + // need to perform path reduction. + const aRoot = a.substring(0, getRootLength(a)); + const bRoot = b.substring(0, getRootLength(b)); + const result = compareStringsCaseInsensitive(aRoot, bRoot); + if (result !== Comparison.EqualTo) { + return result; + } + + // NOTE: Performance optimization - shortcut if there are no relative path segments in + // the non-root portion of the path + const aRest = a.substring(aRoot.length); + const bRest = b.substring(bRoot.length); + if (!relativePathSegmentRegExp.test(aRest) && !relativePathSegmentRegExp.test(bRest)) { + return componentComparer(aRest, bRest); + } + + // The path contains a relative path segment. Normalize the paths and perform a slower component + // by component comparison. + const aComponents = reducePathComponents(getPathComponents(a)); + const bComponents = reducePathComponents(getPathComponents(b)); + const sharedLength = Math.min(aComponents.length, bComponents.length); + for (let i = 1; i < sharedLength; i++) { + const result = componentComparer(aComponents[i], bComponents[i]); + if (result !== Comparison.EqualTo) { + return result; + } + } + return compareValues(aComponents.length, bComponents.length); + } + + /** + * Performs a case-sensitive comparison of two paths. Path roots are always compared case-insensitively. + */ + 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); + } + + /** + * Compare two paths using the provided case sensitivity. + */ + 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, getStringComparer(ignoreCase)); + } + + /** + * Determines whether a `parent` path contains a `child` path using the provide case sensitivity. + */ + 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; + const parentComponents = reducePathComponents(getPathComponents(parent)); + const childComponents = reducePathComponents(getPathComponents(child)); + if (childComponents.length < parentComponents.length) { + return false; + } + + 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; + } + } + + return true; + } + + /** + * Determines whether `fileName` starts with the specified `directoryName` using the provided path canonicalization callback. + * Comparison is case-sensitive between the canonical paths. + * + * @deprecated Use `containsPath` if possible. + */ + export function startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: GetCanonicalFileName): boolean { + const canonicalFileName = getCanonicalFileName(fileName); + const canonicalDirectoryName = getCanonicalFileName(directoryName); + return startsWith(canonicalFileName, canonicalDirectoryName + "/") || startsWith(canonicalFileName, canonicalDirectoryName + "\\"); + } + + //// Relative Paths + + export function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) { + const fromComponents = reducePathComponents(getPathComponents(from)); + const toComponents = reducePathComponents(getPathComponents(to)); + + 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; + } + + if (start === 0) { + return toComponents; + } + + const components = toComponents.slice(start); + const relative: string[] = []; + for (; start < fromComponents.length; start++) { + relative.push(".."); + } + return ["", ...relative, ...components]; + } + + /** + * Gets a relative path that can be used to traverse between `from` and `to`. + */ + export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string; + /** + * Gets a relative path that can be used to traverse between `from` and `to`. + */ + export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; // eslint-disable-line @typescript-eslint/unified-signatures + export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { + Debug.assert((getRootLength(fromDirectory) > 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(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); + return getPathFromPathComponents(pathComponents); + } + + export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string { + return !isRootedDiskPath(absoluteOrRelativePath) + ? absoluteOrRelativePath + : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + } + + export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: GetCanonicalFileName) { + return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName)); + } + + export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { + 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; + } + + return getPathFromPathComponents(pathComponents); + } + + //// Path Traversal + + /** + * Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. + */ + export function forEachAncestorDirectory(directory: Path, callback: (directory: Path) => T | undefined): T | undefined; + export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T | undefined): T | undefined; + export function forEachAncestorDirectory(directory: Path, callback: (directory: Path) => T | undefined): T | undefined { + while (true) { + const result = callback(directory); + if (result !== undefined) { + return result; + } + + const parentPath = getDirectoryPath(directory); + if (parentPath === directory) { + return undefined; + } + + directory = parentPath; + } + } +} \ No newline at end of file diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 1304ab1934976..ad866cba13065 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -682,6 +682,7 @@ namespace ts { /*@internal*/ bufferFrom?(input: string, encoding?: string): Buffer; // For testing /*@internal*/ now?(): Date; + /*@internal*/ require?(baseDir: string, moduleName: string): RequireResult; } export interface FileWatcher { @@ -876,6 +877,15 @@ namespace ts { bufferFrom, base64decode: input => bufferFrom(input, "base64").toString("utf8"), base64encode: input => bufferFrom(input).toString("base64"), + require: (baseDir, moduleName) => { + try { + const modulePath = resolveJSModule(moduleName, baseDir, nodeSystem); + return { module: require(modulePath), modulePath, error: undefined }; + } + catch (error) { + return { module: undefined, modulePath: undefined, error }; + } + } }; return nodeSystem; @@ -1022,6 +1032,7 @@ namespace ts { return watchDirectoryUsingFsWatch; } + // defer watchDirectoryRecursively as it depends on `ts.createMap()` which may not be usable yet. const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? createWatchDirectoryUsing(fsWatchFile) : tscWatchDirectory === "RecursiveDirectoryUsingDynamicPriorityPolling" ? diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index c90c6f35b3435..cef7aba0484de 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -4,7 +4,9 @@ "outFile": "../../built/local/compiler.js" }, - "references": [], + "references": [ + { "path": "../shims" }, + ], "files": [ "core.ts", @@ -15,6 +17,7 @@ "types.ts", "sys.ts", + "path.ts", "diagnosticInformationMap.generated.ts", "scanner.ts", "utilities.ts", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 64415c08d2e9f..9053bbc11263e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2681,6 +2681,7 @@ namespace ts { isArrayType?: boolean; } + // NOTE: Ensure this is up-to-date with src/debug/debug.ts export const enum FlowFlags { Unreachable = 1 << 0, // Unreachable code Start = 1 << 1, // Start of flow graph @@ -5123,6 +5124,11 @@ namespace ts { /* @internal */ spec: ConfigFileSpecs; } + /* @internal */ + export type RequireResult = + | { module: T, modulePath?: string, error: undefined } + | { module: undefined, modulePath?: undefined, error: { stack?: string, message?: string } }; + export interface CreateProgramOptions { rootNames: readonly string[]; options: CompilerOptions; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index c9aff842c8c6b..63a8331c3f89b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -36,7 +36,7 @@ namespace ts { /** Create a new escaped identifier map. */ export function createUnderscoreEscapedMap(): UnderscoreEscapedMap { - return new MapCtr() as UnderscoreEscapedMap; + return new Map() as UnderscoreEscapedMap; } export function hasEntries(map: ReadonlyUnderscoreEscapedMap | undefined): map is ReadonlyUnderscoreEscapedMap { @@ -94,13 +94,6 @@ namespace ts { }; } - export function toPath(fileName: string, basePath: string | undefined, getCanonicalFileName: (path: string) => string): Path { - const nonCanonicalizedPath = isRootedDiskPath(fileName) - ? normalizePath(fileName) - : getNormalizedAbsolutePath(fileName, basePath); - return getCanonicalFileName(nonCanonicalizedPath); - } - export function changesAffectModuleResolution(oldOptions: CompilerOptions, newOptions: CompilerOptions): boolean { return oldOptions.configFilePath !== newOptions.configFilePath || optionsHaveModuleResolutionChanges(oldOptions, newOptions); @@ -4733,25 +4726,6 @@ namespace ts { }); } - /** Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. */ - export function forEachAncestorDirectory(directory: Path, callback: (directory: Path) => T | undefined): T | undefined; - export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T | undefined): T | undefined; - export function forEachAncestorDirectory(directory: Path, callback: (directory: Path) => T | undefined): T | undefined { - while (true) { - const result = callback(directory); - if (result !== undefined) { - return result; - } - - const parentPath = getDirectoryPath(directory); - if (parentPath === directory) { - return undefined; - } - - directory = parentPath; - } - } - // Return true if the given type is the constructor type for an abstract class export function isAbstractConstructorType(type: Type): boolean { return !!(getObjectFlags(type) & ObjectFlags.Anonymous) && !!type.symbol && isAbstractConstructorSymbol(type.symbol); @@ -7635,294 +7609,6 @@ namespace ts { return true; } - /** - * 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 URLs as well. - * - * ```ts - * 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 URLs as well. - * - * ```ts - * 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 startsWithDirectory(fileName: string, directoryName: string, getCanonicalFileName: GetCanonicalFileName): boolean { - const canonicalFileName = getCanonicalFileName(fileName); - const canonicalDirectoryName = getCanonicalFileName(directoryName); - return startsWith(canonicalFileName, canonicalDirectoryName + "/") || startsWith(canonicalFileName, canonicalDirectoryName + "\\"); - } - - 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 getEncodedRootLength(path) > 0; - } - - /** - * Determines whether a path consists only of a path root. - */ - export function isDiskPathRoot(path: string) { - const rootLength = getEncodedRootLength(path); - return rootLength > 0 && rootLength === path.length; - } - - export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string { - return !isRootedDiskPath(absoluteOrRelativePath) - ? absoluteOrRelativePath - : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - } - - function pathComponents(path: string, rootLength: number) { - const root = path.substring(0, rootLength); - const rest = path.substring(rootLength).split(directorySeparator); - if (rest.length && !lastOrUndefined(rest)) rest.pop(); - return [root, ...rest]; - } - - /** - * Parse a path into an array containing a root component (at index 0) and zero or more path - * components (at indices > 0). The result is not normalized. - * If the path is relative, the root component is `""`. - * If the path is absolute, the root component includes the first path separator (`/`). - */ - export function getPathComponents(path: string, currentDirectory = "") { - path = combinePaths(currentDirectory, path); - const rootLength = getRootLength(path); - 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: readonly string[]) { - if (!some(components)) return []; - const reduced = [components[0]]; - for (let i = 1; i < components.length; i++) { - const component = components[i]; - if (!component) continue; - if (component === ".") continue; - if (component === "..") { - if (reduced.length > 1) { - if (reduced[reduced.length - 1] !== "..") { - reduced.pop(); - continue; - } - } - else if (reduced[0]) continue; - } - reduced.push(component); - } - return reduced; - } - - /** - * Parse a path into an array containing a root component (at index 0) and zero or more path - * components (at indices > 0). The result is normalized. - * If the path is relative, the root component is `""`. - * If the path is absolute, the root component includes the first path separator (`/`). - */ - export function getNormalizedPathComponents(path: string, currentDirectory: string | undefined) { - return reducePathComponents(getPathComponents(path, currentDirectory)); - } - - export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string | undefined) { - return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); - } - - /** - * Formats a parsed path consisting of a root component (at index 0) and zero or more path - * segments (at indices > 0). - */ - export function getPathFromPathComponents(pathComponents: readonly string[]) { - if (pathComponents.length === 0) return ""; - - const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]); - return root + pathComponents.slice(1).join(directorySeparator); - } - - export function getNormalizedAbsolutePathWithoutRoot(fileName: string, currentDirectory: string | undefined) { - return getPathWithoutRoot(getNormalizedPathComponents(fileName, currentDirectory)); - } - - function getPathWithoutRoot(pathComponents: readonly string[]) { - if (pathComponents.length === 0) return ""; - return pathComponents.slice(1).join(directorySeparator); - } - export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): ReadonlyMap { const result = createMap(); const symlinks = flatten(mapDefined(files, sf => @@ -7956,278 +7642,8 @@ namespace ts { /* @internal */ namespace ts { - export function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) { - const fromComponents = reducePathComponents(getPathComponents(from)); - const toComponents = reducePathComponents(getPathComponents(to)); - - 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; - } - - if (start === 0) { - return toComponents; - } - - const components = toComponents.slice(start); - const relative: string[] = []; - for (; start < fromComponents.length; start++) { - relative.push(".."); - } - return ["", ...relative, ...components]; - } - - export function getRelativePathFromFile(from: string, to: string, getCanonicalFileName: GetCanonicalFileName) { - return ensurePathIsNonModuleName(getRelativePathFromDirectory(getDirectoryPath(from), to, getCanonicalFileName)); - } - - /** - * Gets a relative path that can be used to traverse between `from` and `to`. - */ - export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string; - /** - * Gets a relative path that can be used to traverse between `from` and `to`. - */ - export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileName: GetCanonicalFileName): string; // eslint-disable-line @typescript-eslint/unified-signatures - export function getRelativePathFromDirectory(fromDirectory: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) { - Debug.assert((getRootLength(fromDirectory) > 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(fromDirectory, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName); - return getPathFromPathComponents(pathComponents); - } - - export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) { - 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; - } - - return getPathFromPathComponents(pathComponents); - } - - /** - * 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 ensurePathIsNonModuleName(path: string): string { - return getRootLength(path) === 0 && !pathIsRelative(path) ? "./" + path : path; - } - - /** - * Returns the path except for its containing directory name. - * Semantics align with NodeJS's `path.basename` except that we support URL's as well. - * - * ```ts - * getBaseFileName("/path/to/file.ext") === "file.ext" - * getBaseFileName("/path/to/") === "to" - * getBaseFileName("/") === "" - * ``` - */ - export function getBaseFileName(path: string): string; - /** - * Gets the portion of a path following the last (non-terminal) 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. - * - * ```ts - * getBaseFileName("/path/to/file.ext", ".ext", true) === "file" - * getBaseFileName("/path/to/file.js", ".ext", true) === "file.js" - * ``` - */ - export function getBaseFileName(path: string, extensions: string | readonly string[], ignoreCase: boolean): string; - export function getBaseFileName(path: string, extensions?: string | readonly string[], 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 | undefined)[]): 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 | undefined)[]): string { - const combined = some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path); - const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(combined))); - return normalized && hasTrailingDirectorySeparator(combined) ? ensureTrailingDirectorySeparator(normalized) : normalized; - } - - /** - * Determines whether a path has a trailing separator (`/` or `\\`). - */ - export function hasTrailingDirectorySeparator(path: string) { - if (path.length === 0) return false; - const ch = path.charCodeAt(path.length - 1); - return ch === CharacterCodes.slash || ch === CharacterCodes.backslash; - } - - /** - * Removes a trailing directory separator from a path. - * @param path The path. - */ - export function removeTrailingDirectorySeparator(path: Path): Path; - export function removeTrailingDirectorySeparator(path: string): string; - export function removeTrailingDirectorySeparator(path: string) { - if (hasTrailingDirectorySeparator(path)) { - return path.substr(0, path.length - 1); - } - - return path; - } - - /** - * Adds a trailing directory separator to a path, if it does not already have one. - * @param path The path. - */ - export function ensureTrailingDirectorySeparator(path: Path): Path; - export function ensureTrailingDirectorySeparator(path: string): string; - export function ensureTrailingDirectorySeparator(path: string) { - if (!hasTrailingDirectorySeparator(path)) { - return path + directorySeparator; - } - - return path; - } - - // check path for these segments: '', '.'. '..' - const relativePathSegmentRegExp = /(^|\/)\.{0,2}($|\/)/; - - 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; - - // NOTE: Performance optimization - shortcut if the root segments differ as there would be no - // need to perform path reduction. - const aRoot = a.substring(0, getRootLength(a)); - const bRoot = b.substring(0, getRootLength(b)); - const result = compareStringsCaseInsensitive(aRoot, bRoot); - if (result !== Comparison.EqualTo) { - return result; - } - - // NOTE: Performance optimization - shortcut if there are no relative path segments in - // the non-root portion of the path - const aRest = a.substring(aRoot.length); - const bRest = b.substring(bRoot.length); - if (!relativePathSegmentRegExp.test(aRest) && !relativePathSegmentRegExp.test(bRest)) { - return componentComparer(aRest, bRest); - } - - // The path contains a relative path segment. Normalize the paths and perform a slower component - // by component comparison. - const aComponents = reducePathComponents(getPathComponents(a)); - const bComponents = reducePathComponents(getPathComponents(b)); - const sharedLength = Math.min(aComponents.length, bComponents.length); - for (let i = 1; i < sharedLength; i++) { - const result = componentComparer(aComponents[i], bComponents[i]); - if (result !== Comparison.EqualTo) { - return result; - } - } - return compareValues(aComponents.length, bComponents.length); - } - - /** - * 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, getStringComparer(ignoreCase)); - } - - 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; - const parentComponents = reducePathComponents(getPathComponents(parent)); - const childComponents = reducePathComponents(getPathComponents(child)); - if (childComponents.length < parentComponents.length) { - return false; - } - - 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; - } - } - - return true; - } - - function isDirectorySeparator(charCode: number): boolean { - return charCode === CharacterCodes.slash || charCode === CharacterCodes.backslash; - } - function stripLeadingDirectorySeparator(s: string): string | undefined { - return isDirectorySeparator(s.charCodeAt(0)) ? s.slice(1) : undefined; + return isAnyDirectorySeparator(s.charCodeAt(0)) ? s.slice(1) : undefined; } export function tryRemoveDirectoryPrefix(path: string, dirPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { @@ -8250,10 +7666,6 @@ namespace ts { const wildcardCharCodes = [CharacterCodes.asterisk, CharacterCodes.question]; - export function hasExtension(fileName: string): boolean { - return stringContains(getBaseFileName(fileName), "."); - } - export const commonPackageFolders: readonly string[] = ["node_modules", "bower_components", "jspm_packages"]; const implicitExcludePathRegexPattern = `(?!(${commonPackageFolders.join("|")})(/|$))`; @@ -8718,13 +8130,6 @@ namespace ts { return changeAnyExtension(path, newExtension, extensionsToRemove, /*ignoreCase*/ false); } - export function changeAnyExtension(path: string, ext: string): string; - export function changeAnyExtension(path: string, ext: string, extensions: string | readonly string[], ignoreCase: boolean): string; - export function changeAnyExtension(path: string, ext: string, extensions?: string | readonly string[], ignoreCase?: boolean) { - const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path); - return pathext ? path.slice(0, path.length - pathext.length) + (startsWith(ext, ".") ? ext : "." + ext) : path; - } - export function tryParsePattern(pattern: string): Pattern | undefined { // This should be verified outside of here and a proper error thrown. Debug.assert(hasZeroOrOneAsteriskCharacter(pattern)); @@ -8767,42 +8172,6 @@ namespace ts { return find(extensionsToRemove, e => fileExtensionIs(path, e)); } - function getAnyExtensionFromPathWorker(path: string, extensions: string | readonly string[], 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 | readonly string[], ignoreCase: boolean): string; - export function getAnyExtensionFromPath(path: string, extensions?: string | readonly string[], 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) { return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs; } diff --git a/src/debug/debug.ts b/src/debug/debug.ts new file mode 100644 index 0000000000000..4314be10a49d3 --- /dev/null +++ b/src/debug/debug.ts @@ -0,0 +1,499 @@ +/// + +/* @internal */ +namespace Debug { + interface Node { + kind: number; + } + + type FunctionExpression = Node; + type ArrowFunction = Node; + type MethodDeclaration = Node; + type Expression = Node; + type SourceFile = Node; + + interface SwitchStatement extends Node { + caseBlock: CaseBlock; + } + + interface CaseBlock extends Node { + clauses: (CaseClause | DefaultClause)[]; + } + + interface CaseClause extends Node { + _caseclauseBrand: any; + expression: Expression; + } + + interface DefaultClause extends Node { + _defaultClauseBrand: any; + } + + interface TypeScriptModule { + readonly SyntaxKind: { + readonly CaseClause: number; + readonly DefaultClause: number; + }; + + readonly FlowFlags: { + readonly Unreachable: number, + readonly Start: number, + readonly BranchLabel: number, + readonly LoopLabel: number, + readonly Assignment: number, + readonly TrueCondition: number, + readonly FalseCondition: number, + readonly SwitchClause: number, + readonly ArrayMutation: number, + readonly Call: number, + readonly Referenced: number, + readonly Shared: number, + readonly PreFinally: number, + readonly AfterFinally: number, + readonly Label: number, + readonly Condition: number, + }; + + getSourceFileOfNode(node: Node): SourceFile; + getSourceTextOfNodeFromSourceFile(sourceFile: SourceFile, node: Node, includeTrivia?: boolean): string; + isDefaultClause(node: Node): node is DefaultClause; + } + + type FlowNode = + | AfterFinallyFlow + | PreFinallyFlow + | FlowStart + | FlowLabel + | FlowAssignment + | FlowCall + | FlowCondition + | FlowSwitchClause + | FlowArrayMutation + ; + + interface FlowNodeBase { + flags: FlowFlags; + id?: number; + } + + interface AfterFinallyFlow extends FlowNodeBase { + antecedent: FlowNode; + } + + interface PreFinallyFlow extends FlowNodeBase { + antecedent: FlowNode; + } + + interface FlowStart extends FlowNodeBase { + node?: FunctionExpression | ArrowFunction | MethodDeclaration; + } + + interface FlowLabel extends FlowNodeBase { + antecedents: FlowNode[] | undefined; + } + + interface FlowAssignment extends FlowNodeBase { + node: Expression; + antecedent: FlowNode; + } + + interface FlowCall extends FlowNodeBase { + node: Expression; + antecedent: FlowNode; + } + + interface FlowCondition extends FlowNodeBase { + node: Expression; + antecedent: FlowNode; + } + + interface FlowSwitchClause extends FlowNodeBase { + switchStatement: SwitchStatement; + clauseStart: number; + clauseEnd: number; + antecedent: FlowNode; + } + + interface FlowArrayMutation extends FlowNodeBase { + node: Expression; + antecedent: FlowNode; + } + + type FlowFlags = number; + let FlowFlags: TypeScriptModule["FlowFlags"]; + let getSourceFileOfNode: TypeScriptModule["getSourceFileOfNode"]; + let getSourceTextOfNodeFromSourceFile: TypeScriptModule["getSourceTextOfNodeFromSourceFile"]; + let isDefaultClause: TypeScriptModule["isDefaultClause"]; + + export function init(ts: TypeScriptModule) { + FlowFlags = ts.FlowFlags; + getSourceFileOfNode = ts.getSourceFileOfNode; + getSourceTextOfNodeFromSourceFile = ts.getSourceTextOfNodeFromSourceFile; + isDefaultClause = ts.isDefaultClause; + } + + let nextDebugFlowId = -1; + + function getDebugFlowNodeId(f: FlowNode) { + if (!f.id) { + f.id = nextDebugFlowId; + nextDebugFlowId--; + } + return f.id; + } + + export function formatControlFlowGraph(flowNode: FlowNode) { + const enum BoxCharacter { + lr = "─", + ud = "│", + dr = "╭", + dl = "╮", + ul = "╯", + ur = "╰", + udr = "├", + udl = "┤", + dlr = "┬", + ulr = "┴", + udlr = "╫", + } + + const enum Connection { + Up = 1 << 0, + Down = 1 << 1, + Left = 1 << 2, + Right = 1 << 3, + + UpDown = Up | Down, + LeftRight = Left | Right, + UpLeft = Up | Left, + UpRight = Up | Right, + DownLeft = Down | Left, + DownRight = Down | Right, + UpDownLeft = UpDown | Left, + UpDownRight = UpDown | Right, + UpLeftRight = Up | LeftRight, + DownLeftRight = Down | LeftRight, + UpDownLeftRight = UpDown | LeftRight, + + NoChildren = 1 << 4, + } + + interface FlowGraphNode { + id: number; + flowNode: FlowNode; + edges: FlowGraphEdge[]; + text: string; + lane: number; + endLane: number; + level: number; + } + + interface FlowGraphEdge { + source: FlowGraphNode; + target: FlowGraphNode; + } + + const hasAntecedentFlags = + FlowFlags.Assignment | + FlowFlags.Condition | + FlowFlags.SwitchClause | + FlowFlags.ArrayMutation | + FlowFlags.Call | + FlowFlags.PreFinally | + FlowFlags.AfterFinally; + + const hasNodeFlags = + FlowFlags.Start | + FlowFlags.Assignment | + FlowFlags.Call | + FlowFlags.Condition | + FlowFlags.ArrayMutation; + + const links: Record = Object.create(/*o*/ null); // eslint-disable-line no-null/no-null + const nodes: FlowGraphNode[] = []; + const edges: FlowGraphEdge[] = []; + const root = buildGraphNode(flowNode); + for (const node of nodes) { + computeLevel(node); + } + + const height = computeHeight(root); + const columnWidths = computeColumnWidths(height); + computeLanes(root, 0); + return renderGraph(); + + function isFlowSwitchClause(f: FlowNode): f is FlowSwitchClause { + return !!(f.flags & FlowFlags.SwitchClause); + } + + function hasAntecedents(f: FlowNode): f is FlowLabel & { antecedents: FlowNode[] } { + return !!(f.flags & FlowFlags.Label) && !!(f as FlowLabel).antecedents; + } + + function hasAntecedent(f: FlowNode): f is Extract { + return !!(f.flags & hasAntecedentFlags); + } + + function hasNode(f: FlowNode): f is Extract { + return !!(f.flags & hasNodeFlags); + } + + function getChildren(node: FlowGraphNode) { + const children: FlowGraphNode[] = []; + for (const edge of node.edges) { + if (edge.source === node) { + children.push(edge.target); + } + } + return children; + } + + function getParents(node: FlowGraphNode) { + const parents: FlowGraphNode[] = []; + for (const edge of node.edges) { + if (edge.target === node) { + parents.push(edge.source); + } + } + return parents; + } + + function buildGraphNode(flowNode: FlowNode) { + const id = getDebugFlowNodeId(flowNode); + let graphNode = links[id]; + if (!graphNode) { + links[id] = graphNode = { id, flowNode, edges: [], text: renderFlowNode(flowNode), lane: -1, endLane: -1, level: -1 }; + nodes.push(graphNode); + if (!(flowNode.flags & FlowFlags.PreFinally)) { + if (hasAntecedents(flowNode)) { + + for (const antecedent of flowNode.antecedents) { + buildGraphEdge(graphNode, antecedent); + } + } + else if (hasAntecedent(flowNode)) { + buildGraphEdge(graphNode, flowNode.antecedent); + } + } + } + return graphNode; + } + + function buildGraphEdge(source: FlowGraphNode, antecedent: FlowNode) { + const target = buildGraphNode(antecedent); + const edge: FlowGraphEdge = { source, target }; + edges.push(edge); + source.edges.push(edge); + target.edges.push(edge); + } + + function computeLevel(node: FlowGraphNode): number { + if (node.level !== -1) { + return node.level; + } + let level = 0; + for (const parent of getParents(node)) { + level = Math.max(level, computeLevel(parent) + 1); + } + return node.level = level; + } + + function computeHeight(node: FlowGraphNode): number { + let height = 0; + for (const child of getChildren(node)) { + height = Math.max(height, computeHeight(child)); + } + return height + 1; + } + + function computeColumnWidths(height: number) { + const columns: number[] = fill(Array(height), 0); + for (const node of nodes) { + columns[node.level] = Math.max(columns[node.level], node.text.length); + } + return columns; + } + + function computeLanes(node: FlowGraphNode, lane: number) { + if (node.lane === -1) { + node.lane = lane; + node.endLane = lane; + const children = getChildren(node); + for (let i = 0; i < children.length; i++) { + if (i > 0) lane++; + const child = children[i]; + computeLanes(child, lane); + if (child.endLane > node.endLane) { + lane = child.endLane; + } + } + node.endLane = lane; + } + } + + function getHeader(flags: FlowFlags) { + if (flags & FlowFlags.Start) return "Start"; + if (flags & FlowFlags.BranchLabel) return "Branch"; + if (flags & FlowFlags.LoopLabel) return "Loop"; + if (flags & FlowFlags.Assignment) return "Assignment"; + if (flags & FlowFlags.TrueCondition) return "True"; + if (flags & FlowFlags.FalseCondition) return "False"; + if (flags & FlowFlags.SwitchClause) return "SwitchClause"; + if (flags & FlowFlags.ArrayMutation) return "ArrayMutation"; + if (flags & FlowFlags.Call) return "Call"; + if (flags & FlowFlags.PreFinally) return "PreFinally"; + if (flags & FlowFlags.AfterFinally) return "AfterFinally"; + if (flags & FlowFlags.Unreachable) return "Unreachable"; + throw new Error(); + } + + function getNodeText(node: Node) { + const sourceFile = getSourceFileOfNode(node); + return getSourceTextOfNodeFromSourceFile(sourceFile, node, /*includeTrivia*/ false); + } + + function renderFlowNode(flowNode: FlowNode) { + let text = getHeader(flowNode.flags); + if (hasNode(flowNode)) { + if (flowNode.node) { + text += ` (${getNodeText(flowNode.node)})`; + } + } + else if (isFlowSwitchClause(flowNode)) { + const clauses: string[] = []; + for (let i = flowNode.clauseStart; i < flowNode.clauseEnd; i++) { + const clause = flowNode.switchStatement.caseBlock.clauses[i]; + if (isDefaultClause(clause)) { + clauses.push("default"); + } + else { + clauses.push(getNodeText(clause.expression)); + } + } + text += ` (${clauses.join(", ")})`; + } + return text; + } + + function renderGraph() { + const columnCount = columnWidths.length; + const laneCount = nodes.reduce((x, n) => Math.max(x, n.lane), 0) + 1; + const lanes: string[] = fill(Array(laneCount), ""); + const grid: (FlowGraphNode | undefined)[][] = columnWidths.map(() => Array(laneCount)); + const connectors: Connection[][] = columnWidths.map(() => fill(Array(laneCount), 0)); + + // build connectors + for (const node of nodes) { + grid[node.level][node.lane] = node; + const children = getChildren(node); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + let connector: Connection = Connection.Right; + if (child.lane === node.lane) connector |= Connection.Left; + if (i > 0) connector |= Connection.Up; + if (i < children.length - 1) connector |= Connection.Down; + connectors[node.level][child.lane] |= connector; + } + if (children.length === 0) { + connectors[node.level][node.lane] |= Connection.NoChildren; + } + const parents = getParents(node); + for (let i = 0; i < parents.length; i++) { + const parent = parents[i]; + let connector: Connection = Connection.Left; + if (i > 0) connector |= Connection.Up; + if (i < parents.length - 1) connector |= Connection.Down; + connectors[node.level - 1][parent.lane] |= connector; + } + } + + // fill in missing connectors + for (let column = 0; column < columnCount; column++) { + for (let lane = 0; lane < laneCount; lane++) { + const left = column > 0 ? connectors[column - 1][lane] : 0; + const above = lane > 0 ? connectors[column][lane - 1] : 0; + let connector = connectors[column][lane]; + if (!connector) { + if (left & Connection.Right) connector |= Connection.LeftRight; + if (above & Connection.Down) connector |= Connection.UpDown; + connectors[column][lane] = connector; + } + } + } + + for (let column = 0; column < columnCount; column++) { + for (let lane = 0; lane < lanes.length; lane++) { + const connector = connectors[column][lane]; + const fill = connector & Connection.Left ? BoxCharacter.lr : " "; + const node = grid[column][lane]; + if (!node) { + if (column < columnCount - 1) { + writeLane(lane, repeat(fill, columnWidths[column] + 1)); + } + } + else { + writeLane(lane, node.text); + if (column < columnCount - 1) { + writeLane(lane, " "); + writeLane(lane, repeat(fill, columnWidths[column] - node.text.length)); + } + } + writeLane(lane, getBoxCharacter(connector)); + writeLane(lane, connector & Connection.Right && column < columnCount - 1 && !grid[column + 1][lane] ? BoxCharacter.lr : " "); + } + } + + return `\n${lanes.join("\n")}\n`; + + function writeLane(lane: number, text: string) { + lanes[lane] += text; + } + } + + function getBoxCharacter(connector: Connection) { + switch (connector) { + case Connection.UpDown: return BoxCharacter.ud; + case Connection.LeftRight: return BoxCharacter.lr; + case Connection.UpLeft: return BoxCharacter.ul; + case Connection.UpRight: return BoxCharacter.ur; + case Connection.DownLeft: return BoxCharacter.dl; + case Connection.DownRight: return BoxCharacter.dr; + case Connection.UpDownLeft: return BoxCharacter.udl; + case Connection.UpDownRight: return BoxCharacter.udr; + case Connection.UpLeftRight: return BoxCharacter.ulr; + case Connection.DownLeftRight: return BoxCharacter.dlr; + case Connection.UpDownLeftRight: return BoxCharacter.udlr; + } + return " "; + } + + function fill(array: T[], value: T) { + if (array.fill) { + array.fill(value); + } + else { + for (let i = 0; i < array.length; i++) { + array[i] = value; + } + } + return array; + } + + function repeat(ch: string, length: number) { + if (ch.repeat) { + return length > 0 ? ch.repeat(length) : ""; + } + let s = ""; + while (s.length < length) { + s += ch; + } + return s; + } + } + + // Export as a module. NOTE: Can't use module exports as this is built using --outFile + declare const module: { exports: {} }; + if (typeof module !== "undefined" && module.exports) { + module.exports = Debug; + } +} \ No newline at end of file diff --git a/src/debug/tsconfig.json b/src/debug/tsconfig.json new file mode 100644 index 0000000000000..471ae4ad34a68 --- /dev/null +++ b/src/debug/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig-library-base", + "compilerOptions": { + "target": "es2019", + "lib": ["es2019"], + "outFile": "../../built/local/compiler-debug.js", + "declaration": false, + "sourceMap": true + }, + "files": [ + "debug.ts" + ] +} diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 67fc45c480700..11ae1e60473ee 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -796,7 +796,7 @@ namespace Harness.LanguageService { return mockHash(s); } - require(_initialDir: string, _moduleName: string): ts.server.RequireResult { + require(_initialDir: string, _moduleName: string): ts.RequireResult { switch (_moduleName) { // Adds to the Quick Info a fixed string and a string from the config file // and replaces the first display part diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 0417ad55c99a6..42834d4688046 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -341,7 +341,7 @@ interface Array { length: number; [n: number]: T; }` private readonly currentDirectory: string; private readonly customWatchFile: HostWatchFile | undefined; private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined; - public require: ((initialPath: string, moduleName: string) => server.RequireResult) | undefined; + public require: ((initialPath: string, moduleName: string) => RequireResult) | undefined; constructor( public withSafeList: boolean, diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 5c0e39f9f6564..a75a96c961eae 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -4,6 +4,7 @@ "outFile": "../../built/local/services.js" }, "references": [ + { "path": "../shims" }, { "path": "../compiler" }, { "path": "../jsTyping" } ], diff --git a/src/shims/mapShim.ts b/src/shims/mapShim.ts new file mode 100644 index 0000000000000..e0468ad43f26f --- /dev/null +++ b/src/shims/mapShim.ts @@ -0,0 +1,212 @@ +/* @internal */ +namespace ts { + // NOTE: Due to how the project-reference merging ends up working, `T` isn't considered referenced until `Map` merges with the definition + // in src/compiler/core.ts + // @ts-ignore + export interface Map { + // full type defined in ~/src/compiler/core.ts + } + + export function createMapShim(): new () => Map { + /** Create a MapLike with good performance. */ + function createDictionaryObject(): Record { + const map = Object.create(/*prototype*/ null); // eslint-disable-line no-null/no-null + + // Using 'delete' on an object causes V8 to put the object in dictionary mode. + // This disables creation of hidden classes, which are expensive when an object is + // constantly changing shape. + map.__ = undefined; + delete map.__; + + return map; + } + + interface MapEntry { + readonly key?: string; + value?: T; + + // Linked list references for iterators. + nextEntry?: MapEntry; + previousEntry?: MapEntry; + + /** + * Specifies if iterators should skip the next entry. + * This will be set when an entry is deleted. + * See https://github.com/Microsoft/TypeScript/pull/27292 for more information. + */ + skipNext?: boolean; + } + + class MapIterator { + private currentEntry?: MapEntry; + private selector: (key: string, value: T) => U; + + constructor(currentEntry: MapEntry, selector: (key: string, value: T) => U) { + this.currentEntry = currentEntry; + this.selector = selector; + } + + public next(): { value: U, done: false } | { value: never, done: true } { + // Navigate to the next entry. + while (this.currentEntry) { + const skipNext = !!this.currentEntry.skipNext; + this.currentEntry = this.currentEntry.nextEntry; + + if (!skipNext) { + break; + } + } + + if (this.currentEntry) { + return { value: this.selector(this.currentEntry.key!, this.currentEntry.value!), done: false }; + } + else { + return { value: undefined as never, done: true }; + } + } + } + + return class implements Map { + private data = createDictionaryObject>(); + public size = 0; + + // Linked list references for iterators. + // See https://github.com/Microsoft/TypeScript/pull/27292 + // for more information. + + /** + * The first entry in the linked list. + * Note that this is only a stub that serves as starting point + * for iterators and doesn't contain a key and a value. + */ + private readonly firstEntry: MapEntry; + private lastEntry: MapEntry; + + constructor() { + // Create a first (stub) map entry that will not contain a key + // and value but serves as starting point for iterators. + this.firstEntry = {}; + // When the map is empty, the last entry is the same as the + // first one. + this.lastEntry = this.firstEntry; + } + + get(key: string): T | undefined { + const entry = this.data[key] as MapEntry | undefined; + return entry && entry.value!; + } + + set(key: string, value: T): this { + if (!this.has(key)) { + this.size++; + + // Create a new entry that will be appended at the + // end of the linked list. + const newEntry: MapEntry = { + key, + value + }; + this.data[key] = newEntry; + + // Adjust the references. + const previousLastEntry = this.lastEntry; + previousLastEntry.nextEntry = newEntry; + newEntry.previousEntry = previousLastEntry; + this.lastEntry = newEntry; + } + else { + this.data[key].value = value; + } + + return this; + } + + has(key: string): boolean { + // eslint-disable-next-line no-in-operator + return key in this.data; + } + + delete(key: string): boolean { + if (this.has(key)) { + this.size--; + const entry = this.data[key]; + delete this.data[key]; + + // Adjust the linked list references of the neighbor entries. + const previousEntry = entry.previousEntry!; + previousEntry.nextEntry = entry.nextEntry; + if (entry.nextEntry) { + entry.nextEntry.previousEntry = previousEntry; + } + + // When the deleted entry was the last one, we need to + // adjust the lastEntry reference. + if (this.lastEntry === entry) { + this.lastEntry = previousEntry; + } + + // Adjust the forward reference of the deleted entry + // in case an iterator still references it. This allows us + // to throw away the entry, but when an active iterator + // (which points to the current entry) continues, it will + // navigate to the entry that originally came before the + // current one and skip it. + entry.previousEntry = undefined; + entry.nextEntry = previousEntry; + entry.skipNext = true; + + return true; + } + return false; + } + + clear(): void { + this.data = createDictionaryObject>(); + this.size = 0; + + // Reset the linked list. Note that we must adjust the forward + // references of the deleted entries to ensure iterators stuck + // in the middle of the list don't continue with deleted entries, + // but can continue with new entries added after the clear() + // operation. + const firstEntry = this.firstEntry; + let currentEntry = firstEntry.nextEntry; + while (currentEntry) { + const nextEntry = currentEntry.nextEntry; + currentEntry.previousEntry = undefined; + currentEntry.nextEntry = firstEntry; + currentEntry.skipNext = true; + + currentEntry = nextEntry; + } + firstEntry.nextEntry = undefined; + this.lastEntry = firstEntry; + } + + keys(): Iterator { + return new MapIterator(this.firstEntry, key => key); + } + + values(): Iterator { + return new MapIterator(this.firstEntry, (_key, value) => value); + } + + entries(): Iterator<[string, T]> { + return new MapIterator(this.firstEntry, (key, value) => [key, value] as [string, T]); + } + + forEach(action: (value: T, key: string) => void): void { + const iterator = this.entries(); + while (true) { + const iterResult = iterator.next(); + if (iterResult.done) { + break; + } + + const [key, value] = iterResult.value; + action(value, key); + } + } + }; + } +} \ No newline at end of file diff --git a/src/shims/tsconfig.json b/src/shims/tsconfig.json new file mode 100644 index 0000000000000..cb058762fd616 --- /dev/null +++ b/src/shims/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig-base", + "compilerOptions": { + "outFile": "../../built/local/shims.js" + }, + "files": [ + "mapShim.ts" + ] +} diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 1892c4c481539..27a7712a2b14f 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -14,6 +14,7 @@ ] }, "references": [ + { "path": "../shims", "prepend": true }, { "path": "../compiler", "prepend": true }, { "path": "../services", "prepend": true }, { "path": "../jsTyping", "prepend": true }, @@ -60,7 +61,7 @@ "unittests/publicApi.ts", "unittests/reuseProgramStructure.ts", "unittests/semver.ts", - "unittests/shimMap.ts", + "unittests/createMapShim.ts", "unittests/transform.ts", "unittests/config/commandLineParsing.ts", "unittests/config/configurationExtension.ts", diff --git a/src/testRunner/unittests/shimMap.ts b/src/testRunner/unittests/createMapShim.ts similarity index 91% rename from src/testRunner/unittests/shimMap.ts rename to src/testRunner/unittests/createMapShim.ts index c3e94c0840c05..c49e1cc63ccc1 100644 --- a/src/testRunner/unittests/shimMap.ts +++ b/src/testRunner/unittests/createMapShim.ts @@ -1,5 +1,5 @@ namespace ts { - describe("unittests:: shimMap", () => { + describe("unittests:: createMapShim", () => { function testMapIterationAddedValues(map: Map, useForEach: boolean): string { let resultString = ""; @@ -93,15 +93,15 @@ namespace ts { const nativeMapIteratorResult = testMapIterationAddedValues(nativeMap, /* useForEach */ false); assert.equal(nativeMapIteratorResult, expectedResult, "nativeMap-iterator"); - // Then, test the shimMap. - let localShimMap = new (shimMap())(); + // Then, test the map shim. + const MapShim = createMapShim(); // tslint:disable-line variable-name + let localShimMap = new MapShim(); const shimMapForEachResult = testMapIterationAddedValues(localShimMap, /* useForEach */ true); assert.equal(shimMapForEachResult, expectedResult, "shimMap-forEach"); - localShimMap = new (shimMap())(); + localShimMap = new MapShim(); const shimMapIteratorResult = testMapIterationAddedValues(localShimMap, /* useForEach */ false); assert.equal(shimMapIteratorResult, expectedResult, "shimMap-iterator"); - }); }); } diff --git a/src/tsconfig.json b/src/tsconfig.json index 0c7f0c65da00b..8a0f4dbce0ed8 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -2,10 +2,12 @@ "files": [], "include": [], "references": [ + { "path": "./shims" }, { "path": "./tsc" }, { "path": "./tsserver" }, { "path": "./typingsInstaller" }, { "path": "./watchGuard" }, + { "path": "./debug" }, { "path": "./cancellationToken" }, { "path": "./testRunner" } ] diff --git a/src/tsserverlibrary/tsconfig.json b/src/tsserverlibrary/tsconfig.json index fcd4597dc6d81..f9953772f1fe9 100644 --- a/src/tsserverlibrary/tsconfig.json +++ b/src/tsserverlibrary/tsconfig.json @@ -7,6 +7,7 @@ "tsserverlibrary.ts" ], "references": [ + { "path": "../shims", "prepend": true }, { "path": "../compiler", "prepend": true }, { "path": "../jsTyping", "prepend": true }, { "path": "../services", "prepend": true }, diff --git a/src/typescriptServices/tsconfig.json b/src/typescriptServices/tsconfig.json index 1845d69a47369..33028aad27ce7 100644 --- a/src/typescriptServices/tsconfig.json +++ b/src/typescriptServices/tsconfig.json @@ -7,6 +7,7 @@ "typescriptServices.ts" ], "references": [ + { "path": "../shims", "prepend": true }, { "path": "../compiler", "prepend": true }, { "path": "../jsTyping", "prepend": true }, { "path": "../services", "prepend": true }