diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index aaaa0ed50b8..abedf405cde 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7341,24 +7341,12 @@ namespace ts { unionIndex?: number; } + function getTypeId(type: Type) { + return type.id; + } + function binarySearchTypes(types: Type[], type: Type): number { - let low = 0; - let high = types.length - 1; - const typeId = type.id; - while (low <= high) { - const middle = low + ((high - low) >> 1); - const id = types[middle].id; - if (id === typeId) { - return middle; - } - else if (id > typeId) { - high = middle - 1; - } - else { - low = middle + 1; - } - } - return ~low; + return binarySearch(types, type, getTypeId, compareValues); } function containsType(types: Type[], type: Type): boolean { diff --git a/src/compiler/core.ts b/src/compiler/core.ts index bb0aca3e700..605cde05178 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -660,35 +660,77 @@ namespace ts { } /** - * Creates a new array with duplicate entries removed. + * Deduplicates an array that has already been sorted. + */ + export function deduplicateSorted(array: SortedReadonlyArray, comparer: EqualityComparer | Comparer) { + if (!array) return undefined; + if (array.length === 0) return []; + + let last = array[0]; + const deduplicated: T[] = [last]; + for (let i = 1; i < array.length; i++) { + switch (comparer(last, array[i])) { + // equality comparison + case true: + + // relational comparison + case Comparison.LessThan: + case Comparison.EqualTo: + continue; + } + + deduplicated.push(last = array[i]); + } + + return deduplicated; + } + + /** + * Deduplicates an unsorted array. * @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); + export function deduplicate(array: ReadonlyArray, equalityComparer: EqualityComparer, comparer?: Comparer): T[] { + return !array ? undefined : + array.length === 0 ? [] : + array.length === 1 ? array.slice() : + comparer ? deduplicateRelational(array, equalityComparer, comparer) : + deduplicateEquality(array, equalityComparer); } - function deduplicateWorker(array: ReadonlyArray, equalityComparer: EqualityComparer = equateValues, comparer: Comparer) { + function deduplicateRelational(array: ReadonlyArray, equalityComparer: EqualityComparer, 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; - } + let last = array[indices[0]]; + const deduplicated: number[] = [indices[0]]; + for (let i = 1; i < indices.length; i++) { + const index = indices[i]; + const item = array[index]; + if (!equalityComparer(last, item)) { + deduplicated.push(index); + last = item; } - deduplicated.push(sourceIndex); } - // return deduplicated items in original order - return deduplicated.sort().map(i => array[i]); + // restore original order + deduplicated.sort(); + return deduplicated.map(i => array[i]); + } + + function deduplicateEquality(array: ReadonlyArray, equalityComparer: EqualityComparer) { + const result: T[] = []; + for (const item of array) { + pushIfUnique(result, item, equalityComparer); + } + return result; + } + + export function sortAndDeduplicate(array: ReadonlyArray, comparer: Comparer, equalityComparer?: EqualityComparer) { + return deduplicateSorted(sort(array, comparer), equalityComparer || comparer); } export function arrayIsEqualTo(array1: ReadonlyArray, array2: ReadonlyArray, equalityComparer: (a: T, b: T) => boolean = equateValues): boolean { @@ -827,15 +869,6 @@ namespace ts { 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. */ @@ -878,13 +911,20 @@ namespace ts { indices.sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y)); } + /** + * Returns a new sorted array. + */ + export function sort(array: ReadonlyArray, comparer: Comparer) { + return array.slice().sort(comparer) as ReadonlyArray as SortedReadonlyArray; + } + /** * Stable sort of an array. Elements equal to each other maintain their relative position in the array. */ export function stableSort(array: ReadonlyArray, comparer: Comparer) { const indices = sequence(0, array.length); stableSortIndices(array, indices, comparer); - return indices.map(i => array[i]); + return indices.map(i => array[i]) as ReadonlyArray as SortedReadonlyArray; } export function rangeEquals(array1: ReadonlyArray, array2: ReadonlyArray, pos: number, end: number) { @@ -969,25 +1009,22 @@ namespace ts { * @param array A sorted array whose first element must be no larger than number * @param number The value to be searched for in the array. */ - export function binarySearch(array: ReadonlyArray, value: T, comparer?: Comparer, offset?: number): number { + export function binarySearch(array: ReadonlyArray, value: T, keySelector: Selector, keyComparer: Comparer, offset?: number): number { if (!array || array.length === 0) { return -1; } let low = offset || 0; let high = array.length - 1; - comparer = comparer !== undefined - ? comparer - : (v1, v2) => (v1 < v2 ? -1 : (v1 > v2 ? 1 : 0)); - + const key = keySelector(value); while (low <= high) { const middle = low + ((high - low) >> 1); - const midValue = array[middle]; + const midKey = keySelector(array[middle]); - if (comparer(midValue, value) === 0) { + if (keyComparer(midKey, key) === 0) { return middle; } - else if (comparer(midValue, value) > 0) { + else if (keyComparer(midKey, key) > 0) { high = middle - 1; } else { @@ -2452,8 +2489,8 @@ namespace ts { return flatten(results); function visitDirectory(path: string, absolutePath: string, depth: number | undefined) { - let { files, directories } = getFileSystemEntries(path); - files = files.slice().sort(comparer); + const entries = getFileSystemEntries(path); + const files = sort(entries.files, comparer); for (const current of files) { const name = combinePaths(path, current); @@ -2478,7 +2515,7 @@ namespace ts { } } - directories = directories.slice().sort(comparer); + const directories = sort(entries.directories, comparer); for (const current of directories) { const name = combinePaths(path, current); const absoluteName = combinePaths(absolutePath, current); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 6dab127ca33..5a11cccc8b6 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -352,7 +352,7 @@ namespace ts { * We assume the first line starts at position 0 and 'position' is non-negative. */ export function computeLineAndCharacterOfPosition(lineStarts: ReadonlyArray, position: number): LineAndCharacter { - let lineNumber = binarySearch(lineStarts, position); + let lineNumber = binarySearch(lineStarts, position, identity, compareValues); if (lineNumber < 0) { // If the actual position was not found, // the binary search returns the 2's-complement of the next line start diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index cc3c256827a..e7a73b6d09c 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -306,7 +306,7 @@ namespace ts { // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") const optsList = showAllOptions ? - optionDeclarations.slice().sort((a, b) => compareStringsCaseInsensitive(a.name, b.name)) : + sort(optionDeclarations, (a, b) => compareStringsCaseInsensitive(a.name, b.name)) : filter(optionDeclarations.slice(), v => v.showInSimplifiedHelpView); // We want our descriptions to align at the same column in our output, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 2f861f2c371..3af3b6e022a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -36,6 +36,11 @@ namespace ts { push(...values: T[]): void; } + /* @internal */ + export interface SortedReadonlyArray extends ReadonlyArray { + " __sortedArrayBrand": any; + } + /* @internal */ export type EqualityComparer = (a: T, b: T) => boolean; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index a3a8ca3afc7..1ade3cc6374 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -321,16 +321,16 @@ namespace ts { return getSourceTextOfNodeFromSourceFile(getSourceFileOfNode(node), node, includeTrivia); } + function getPos(range: Node) { + return range.pos; + } + /** * Note: it is expected that the `nodeArray` and the `node` are within the same file. * For example, searching for a `SourceFile` in a `SourceFile[]` wouldn't work. */ export function indexOfNode(nodeArray: ReadonlyArray, node: Node) { - return binarySearch(nodeArray, node, compareNodePos); - } - - function compareNodePos({ pos: aPos }: Node, { pos: bPos}: Node) { - return aPos < bPos ? Comparison.LessThan : bPos < aPos ? Comparison.GreaterThan : Comparison.EqualTo; + return binarySearch(nodeArray, node, getPos, compareValues); } /** diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 728d18c59cc..4826c30bbfb 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -1313,7 +1313,7 @@ namespace Harness { export const diagnosticSummaryMarker = "__diagnosticSummary"; export const globalErrorsMarker = "__globalErrors"; export function *iterateErrorBaseline(inputFiles: ReadonlyArray, diagnostics: ReadonlyArray, pretty?: boolean): IterableIterator<[string, string, number]> { - diagnostics = diagnostics.slice().sort(ts.compareDiagnostics); + diagnostics = ts.sort(diagnostics, ts.compareDiagnostics); let outputLines = ""; // Count up all errors that were found in files other than lib.d.ts so we don't miss any let totalErrorsReportedInNonLibraryFiles = 0; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f749c27cbbc..1e3104b08d1 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -203,8 +203,10 @@ namespace ts.server { * This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project. */ export function combineProjectOutput(projects: ReadonlyArray, action: (project: Project) => ReadonlyArray, comparer?: (a: T, b: T) => number, areEqual?: (a: T, b: T) => boolean) { - const result = flatMap(projects, action).sort(comparer); - return projects.length > 1 ? deduplicate(result, areEqual) : result; + const outputs = flatMap(projects, action); + return comparer + ? sortAndDeduplicate(outputs, comparer, areEqual) + : deduplicate(outputs, areEqual); } export interface HostConfiguration { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 0976d3dd1e5..2d458c09b82 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -250,7 +250,7 @@ namespace ts.server { return; } - const insertIndex = binarySearch(array, insert, compare); + const insertIndex = binarySearch(array, insert, identity, compare); if (insertIndex < 0) { array.splice(~insertIndex, 0, insert); } @@ -266,7 +266,7 @@ namespace ts.server { return; } - const removeIndex = binarySearch(array, remove, compare); + const removeIndex = binarySearch(array, remove, identity, compare); if (removeIndex >= 0) { array.splice(removeIndex, 1); } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index bc3a8e27ef0..f84292c5faf 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -581,7 +581,7 @@ namespace ts.textChanges { return applyFormatting(nonformattedText, sourceFile, initialIndentation, delta, this.rulesProvider); } - private static normalize(changes: Change[]): Change[] { + private static normalize(changes: Change[]) { // order changes by start position const normalized = stableSort(changes, (a, b) => a.range.pos - b.range.pos); // verify that change intervals do not overlap, except possibly at end points.