From bfba32b71df825279a4408756fb7b5dddd1170b1 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 26 Oct 2017 13:49:32 -0700 Subject: [PATCH] Cleanup, merge #19475 --- src/compiler/core.ts | 147 ++++++++++++++++++++++---------- src/compiler/types.ts | 16 ++++ src/server/project.ts | 3 +- src/server/session.ts | 2 +- src/server/utilities.ts | 5 +- src/services/jsTyping.ts | 5 +- src/services/pathCompletions.ts | 7 +- src/services/services.ts | 2 +- 8 files changed, 132 insertions(+), 55 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 64ab5af67c2..9c52a4481f9 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -159,12 +159,6 @@ namespace ts { return getCanonicalFileName(nonCanonicalizedPath); } - export const enum Comparison { - LessThan = -1, - EqualTo = 0, - GreaterThan = 1 - } - export function length(array: ReadonlyArray) { return array ? array.length : 0; } @@ -301,17 +295,33 @@ namespace ts { Debug.fail(); } - export function contains(array: ReadonlyArray, value: T): boolean { - if (array) { - for (const v of array) { - if (v === value) { - return true; - } + function containsWithoutEqualityComparer(array: ReadonlyArray, value: T) { + for (const v of array) { + if (v === value) { + return true; } } return false; } + function containsWithEqualityComparer(array: ReadonlyArray, value: T, equalityComparer: EqualityComparer) { + for (const v of array) { + if (equalityComparer(v, value)) { + return true; + } + } + return false; + } + + export function contains(array: ReadonlyArray, value: T, equalityComparer?: EqualityComparer): boolean { + if (array) { + return equalityComparer + ? containsWithEqualityComparer(array, value, equalityComparer) + : containsWithoutEqualityComparer(array, value); + } + return false; + } + export function indexOf(array: ReadonlyArray, value: T): number { if (array) { for (let i = 0; i < array.length; i++) { @@ -649,21 +659,36 @@ namespace ts { return [...array1, ...array2]; } - // TODO: fixme (N^2) - add optional comparer so collection can be sorted before deduplication. - export function deduplicate(array: ReadonlyArray, equalityComparer: (a: T, b: T) => boolean = equateValues): T[] { - let result: T[]; - if (array) { - result = []; - loop: for (const item of array) { - for (const res of result) { - if (equalityComparer(res, item)) { - continue loop; - } + /** + * Creates a new array with duplicate entries removed. + * @param equalityComparer An optional `EqualityComparer` used to determine if two values are duplicates. + * @param comparer An optional `Comparer` used to sort entries before comparison. If supplied, + * results are returned in the original order found in `array`. + */ + export function deduplicate(array: ReadonlyArray, equalityComparer?: EqualityComparer, comparer?: Comparer): T[] { + if (!array) return undefined; + if (!comparer) return addRangeIfUnique([], array, equalityComparer); + return deduplicateWorker(array, equalityComparer, comparer); + } + + function deduplicateWorker(array: ReadonlyArray, equalityComparer: EqualityComparer = equateValues, comparer: Comparer) { + // Perform a stable sort of the array. This ensures the first entry in a list of + // duplicates remains the first entry in the result. + const indices = sequence(0, array.length); + stableSortIndices(array, indices, comparer); + + const deduplicated: number[] = []; + loop: for (const sourceIndex of indices) { + for (const targetIndex of deduplicated) { + if (equalityComparer(array[sourceIndex], array[targetIndex])) { + continue loop; } - result.push(item); } + deduplicated.push(sourceIndex); } - return result; + + // return deduplicated items in original order + return deduplicated.sort().map(i => array[i]); } export function arrayIsEqualTo(array1: ReadonlyArray, array2: ReadonlyArray, equalityComparer: (a: T, b: T) => boolean = equateValues): boolean { @@ -731,7 +756,7 @@ namespace ts { * are not present in `arrayA` but are present in `arrayB`. Assumes both arrays are sorted * based on the provided comparer. */ - export function relativeComplement(arrayA: T[] | undefined, arrayB: T[] | undefined, comparer: Comparer = compareValues, offsetA = 0, offsetB = 0): T[] | undefined { + export function relativeComplement(arrayA: T[] | undefined, arrayB: T[] | undefined, comparer: Comparer, offsetA = 0, offsetB = 0): T[] | undefined { if (!arrayB || !arrayA || arrayB.length === 0 || arrayA.length === 0) return arrayB; const result: T[] = []; outer: for (; offsetB < arrayB.length; offsetB++) { @@ -795,19 +820,27 @@ namespace ts { start = start === undefined ? 0 : toOffset(from, start); end = end === undefined ? from.length : toOffset(from, end); for (let i = start; i < end && i < from.length; i++) { - const v = from[i]; - if (v !== undefined) { + if (from[i] !== undefined) { to.push(from[i]); } } return to; } + function addRangeIfUnique(to: T[], from: ReadonlyArray, equalityComparer?: EqualityComparer): T[] | undefined { + for (let i = 0; i < from.length; i++) { + if (from[i] !== undefined) { + pushIfUnique(to, from[i], equalityComparer); + } + } + return to; + } + /** * @return Whether the value was added. */ - export function pushIfUnique(array: T[], toAdd: T): boolean { - if (contains(array, toAdd)) { + export function pushIfUnique(array: T[], toAdd: T, equalityComparer?: EqualityComparer): boolean { + if (contains(array, toAdd, equalityComparer)) { return false; } else { @@ -819,9 +852,9 @@ namespace ts { /** * Unlike `pushIfUnique`, this can take `undefined` as an input, and returns a new array. */ - export function appendIfUnique(array: T[] | undefined, toAdd: T): T[] { + export function appendIfUnique(array: T[] | undefined, toAdd: T, equalityComparer?: EqualityComparer): T[] { if (array) { - pushIfUnique(array, toAdd); + pushIfUnique(array, toAdd, equalityComparer); return array; } else { @@ -829,14 +862,29 @@ namespace ts { } } + /** + * Creates an array of integers starting at `from` (inclusive) and ending at `to` (exclusive). + */ + function sequence(from: number, to: number) { + const numbers: number[] = []; + for (let i = from; i < to; i++) { + numbers.push(i); + } + return numbers; + } + + function stableSortIndices(array: ReadonlyArray, indices: number[], comparer: Comparer) { + // sort indices by value then position + indices.sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y)); + } + /** * Stable sort of an array. Elements equal to each other maintain their relative position in the array. */ - export function stableSort(array: ReadonlyArray, comparer: Comparer = compareValues) { - return array - .map((_, i) => i) // create array of indices - .sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y)) // sort indices by value then position - .map(i => array[i]); // get sorted array + export function stableSort(array: ReadonlyArray, comparer: Comparer) { + const indices = sequence(0, array.length); + stableSortIndices(array, indices, comparer); + return indices.map(i => array[i]); } export function rangeEquals(array1: ReadonlyArray, array2: ReadonlyArray, pos: number, end: number) { @@ -914,9 +962,6 @@ namespace ts { return result; } - export type Comparer = (a: T, b: T) => Comparison; - export type EqualityComparer = (a: T, b: T) => boolean; - /** * Performs a binary search, finding the index at which 'value' occurs in 'array'. * If no such index is found, returns the 2's-complement of first index at which @@ -1483,7 +1528,7 @@ namespace ts { return headChain; } - function equateValues(a: T, b: T) { + export function equateValues(a: T, b: T) { return a === b; } @@ -1512,10 +1557,9 @@ namespace ts { return equateValues(a, b); } - /** - * Compare two values for their order relative to each other. - */ - export function compareValues(a: T, b: T) { + function compareComparableValues(a: string, b: string): Comparison; + function compareComparableValues(a: number, b: number): Comparison; + function compareComparableValues(a: string | number, b: string | number) { return a === b ? Comparison.EqualTo : a === undefined ? Comparison.LessThan : b === undefined ? Comparison.GreaterThan : @@ -1523,6 +1567,13 @@ namespace ts { Comparison.GreaterThan; } + /** + * Compare two values for their order relative to each other. + */ + export function compareValues(a: number, b: number) { + return compareComparableValues(a, b); + } + /** * Compare two strings using a case-insensitive ordinal comparison. * @@ -1555,7 +1606,7 @@ namespace ts { * value of each code-point. */ export function compareStringsCaseSensitive(a: string, b: string) { - return compareValues(a, b); + return compareComparableValues(a, b); } /** @@ -2488,7 +2539,11 @@ namespace ts { if (!extraFileExtensions || extraFileExtensions.length === 0 || !needAllExtensions) { return needAllExtensions ? allSupportedExtensions : supportedTypeScriptExtensions; } - return deduplicate([...allSupportedExtensions, ...extraFileExtensions.map(e => e.extension)]); + return deduplicate( + [...allSupportedExtensions, ...extraFileExtensions.map(e => e.extension)], + equateStringsCaseSensitive, + compareStringsCaseSensitive + ); } export function hasJavaScriptFileExtension(fileName: string) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ff2a33266ed..2f861f2c371 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -36,6 +36,22 @@ namespace ts { push(...values: T[]): void; } + /* @internal */ + export type EqualityComparer = (a: T, b: T) => boolean; + + /* @internal */ + export type Comparer = (a: T, b: T) => Comparison; + + /* @internal */ + export const enum Comparison { + LessThan = -1, + EqualTo = 0, + GreaterThan = 1 + } + + /* @internal */ + export type Selector = (v: T) => U; + // branded string type used to store absolute, normalized and canonicalized paths // arbitrary file name can be converted to Path via toPath function export type Path = string & { __pathBrand: any }; diff --git a/src/server/project.ts b/src/server/project.ts index 7542b30df00..bda4b644e9c 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -860,7 +860,8 @@ namespace ts.server { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.directoryStructureHost); scriptInfo.attachToProject(this); }, - removed => this.detachScriptInfoFromProject(removed) + removed => this.detachScriptInfoFromProject(removed), + compareStringsCaseSensitive ); const elapsed = timestamp() - start; this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`); diff --git a/src/server/session.ts b/src/server/session.ts index d40becbbd16..c8ce00f303c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -958,7 +958,7 @@ namespace ts.server { projects, project => project.getLanguageService().findReferences(file, position), /*comparer*/ undefined, - /*areEqual (TODO: fixme)*/ undefined + equateValues ); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 69399b672b3..0976d3dd1e5 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -288,8 +288,7 @@ namespace ts.server { return index === 0 || value !== array[index - 1]; } - export function enumerateInsertsAndDeletes(newItems: SortedReadonlyArray, oldItems: SortedReadonlyArray, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, compare?: Comparer) { - compare = compare || compareValues; + export function enumerateInsertsAndDeletes(newItems: SortedReadonlyArray, oldItems: SortedReadonlyArray, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, comparer: Comparer) { let newIndex = 0; let oldIndex = 0; const newLen = newItems.length; @@ -297,7 +296,7 @@ namespace ts.server { while (newIndex < newLen && oldIndex < oldLen) { const newItem = newItems[newIndex]; const oldItem = oldItems[oldIndex]; - const compareResult = compare(newItem, oldItem); + const compareResult = comparer(newItem, oldItem); if (compareResult === Comparison.LessThan) { inserted(newItem); newIndex++; diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts index 572858dd2fd..6579e21fb27 100644 --- a/src/services/jsTyping.ts +++ b/src/services/jsTyping.ts @@ -115,7 +115,10 @@ namespace ts.JsTyping { // add typings for unresolved imports if (unresolvedImports) { - const module = deduplicate(unresolvedImports.map(moduleId => nodeCoreModules.has(moduleId) ? "node" : moduleId)); + const module = deduplicate( + unresolvedImports.map(moduleId => nodeCoreModules.has(moduleId) ? "node" : moduleId), + equateStringsCaseSensitive, + compareStringsCaseSensitive); addInferredTypings(module, "Inferred typings from unresolved imports"); } // Add the cached typing locations for inferred typings that are already installed diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts index 780b14db719..79a38c70d3a 100644 --- a/src/services/pathCompletions.ts +++ b/src/services/pathCompletions.ts @@ -44,7 +44,10 @@ namespace ts.Completions.PathCompletions { containsPath(rootDirectory, scriptPath, basePath, ignoreCase) ? scriptPath.substr(rootDirectory.length) : undefined); // Now find a path for each potential directory that is to be merged with the one containing the script - return deduplicate(map(rootDirs, rootDirectory => combinePaths(rootDirectory, relativeDirectory))); + return deduplicate( + map(rootDirs, rootDirectory => combinePaths(rootDirectory, relativeDirectory)), + equateStringsCaseSensitive, + compareStringsCaseSensitive); } function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: ReadonlyArray, includeExtensions: boolean, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude?: string): CompletionEntry[] { @@ -271,7 +274,7 @@ namespace ts.Completions.PathCompletions { } } - return deduplicate(nonRelativeModuleNames); + return deduplicate(nonRelativeModuleNames, equateStringsCaseSensitive, compareStringsCaseSensitive); } export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): CompletionInfo { diff --git a/src/services/services.ts b/src/services/services.ts index 3b90a80a387..a5f81edf79d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1757,7 +1757,7 @@ namespace ts { const newLineCharacter = getNewLineOrDefaultFromHost(host); const rulesProvider = getRuleProvider(formatOptions); - return flatMap(deduplicate(errorCodes), errorCode => { + return flatMap(deduplicate(errorCodes, equateValues, compareValues), errorCode => { cancellationToken.throwIfCancellationRequested(); return codefix.getFixes({ errorCode, sourceFile, span, program, newLineCharacter, host, cancellationToken, rulesProvider }); });