Introduce set with custom equals and getHashCode (#48169)

* Implement set with custom equals and getHashCode

* Adopt custom set in session

* Add doc comment

* Initially store buckets as non-arrays
This commit is contained in:
Andrew Casey 2022-03-11 16:17:54 -08:00 committed by GitHub
parent 93c3a30edc
commit ce9657d5e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 325 additions and 2 deletions

View File

@ -1488,6 +1488,159 @@ namespace ts {
return createMultiMap() as UnderscoreEscapedMultiMap<T>;
}
/**
* Creates a Set with custom equality and hash code functionality. This is useful when you
* want to use something looser than object identity - e.g. "has the same span".
*
* If `equals(a, b)`, it must be the case that `getHashCode(a) === getHashCode(b)`.
* The converse is not required.
*
* To facilitate a perf optimization (lazy allocation of bucket arrays), `TElement` is
* assumed not to be an array type.
*/
export function createSet<TElement, THash = number>(getHashCode: (element: TElement) => THash, equals: EqualityComparer<TElement>): Set<TElement> {
const multiMap = new Map<THash, TElement | TElement[]>();
let size = 0;
function getElementIterator(): Iterator<TElement> {
const valueIt = multiMap.values();
let arrayIt: Iterator<TElement> | undefined;
return {
next: () => {
while (true) {
if (arrayIt) {
const n = arrayIt.next();
if (!n.done) {
return { value: n.value };
}
arrayIt = undefined;
}
else {
const n = valueIt.next();
if (n.done) {
return { value: undefined, done: true };
}
if (!isArray(n.value)) {
return { value: n.value };
}
arrayIt = arrayIterator(n.value);
}
}
}
};
}
const set: Set<TElement> = {
has(element: TElement): boolean {
const hash = getHashCode(element);
if (!multiMap.has(hash)) return false;
const candidates = multiMap.get(hash)!;
if (!isArray(candidates)) return equals(candidates, element);
for (const candidate of candidates) {
if (equals(candidate, element)) {
return true;
}
}
return false;
},
add(element: TElement): Set<TElement> {
const hash = getHashCode(element);
if (multiMap.has(hash)) {
const values = multiMap.get(hash)!;
if (isArray(values)) {
if (!contains(values, element, equals)) {
values.push(element);
size++;
}
}
else {
const value = values;
if (!equals(value, element)) {
multiMap.set(hash, [ value, element ]);
size++;
}
}
}
else {
multiMap.set(hash, element);
size++;
}
return this;
},
delete(element: TElement): boolean {
const hash = getHashCode(element);
if (!multiMap.has(hash)) return false;
const candidates = multiMap.get(hash)!;
if (isArray(candidates)) {
for (let i = 0; i < candidates.length; i++) {
if (equals(candidates[i], element)) {
if (candidates.length === 1) {
multiMap.delete(hash);
}
else if (candidates.length === 2) {
multiMap.set(hash, candidates[1 - i]);
}
else {
unorderedRemoveItemAt(candidates, i);
}
size--;
return true;
}
}
}
else {
const candidate = candidates;
if (equals(candidate, element)) {
multiMap.delete(hash);
size--;
return true;
}
}
return false;
},
clear(): void {
multiMap.clear();
size = 0;
},
get size() {
return size;
},
forEach(action: (value: TElement, key: TElement) => void): void {
for (const elements of arrayFrom(multiMap.values())) {
if (isArray(elements)) {
for (const element of elements) {
action(element, element);
}
}
else {
const element = elements;
action(element, element);
}
}
},
keys(): Iterator<TElement> {
return getElementIterator();
},
values(): Iterator<TElement> {
return getElementIterator();
},
entries(): Iterator<[TElement, TElement]> {
const it = getElementIterator();
return {
next: () => {
const n = it.next();
return n.done ? n : { value: [ n.value, n.value ] };
}
};
},
};
return set;
}
/**
* Tests whether a value is an array.
*/

View File

@ -299,6 +299,10 @@ namespace ts.server {
navigateToItems: readonly NavigateToItem[];
};
function createDocumentSpanSet(): Set<DocumentSpan> {
return createSet(({textSpan}) => textSpan.start + 100003 * textSpan.length, documentSpansEqual);
}
function combineProjectOutputForRenameLocations(
projects: Projects,
defaultProject: Project,
@ -308,6 +312,7 @@ namespace ts.server {
{ providePrefixAndSuffixTextForRename }: UserPreferences
): readonly RenameLocation[] {
const outputs: RenameLocation[] = [];
const seen = createDocumentSpanSet();
combineProjectOutputWorker(
projects,
defaultProject,
@ -316,7 +321,8 @@ namespace ts.server {
const projectOutputs = project.getLanguageService().findRenameLocations(location.fileName, location.pos, findInStrings, findInComments, providePrefixAndSuffixTextForRename);
if (projectOutputs) {
for (const output of projectOutputs) {
if (!contains(outputs, output, documentSpansEqual) && !tryAddToTodo(project, documentSpanLocation(output))) {
if (!seen.has(output) && !tryAddToTodo(project, documentSpanLocation(output))) {
seen.add(output);
outputs.push(output);
}
}
@ -1558,6 +1564,7 @@ namespace ts.server {
const fileName = args.file;
const references: ReferenceEntry[] = [];
const seen = createDocumentSpanSet();
forEachProjectInProjects(projects, /*path*/ undefined, project => {
if (project.getCancellationToken().isCancellationRequested()) return;
@ -1565,8 +1572,9 @@ namespace ts.server {
const projectOutputs = project.getLanguageService().getFileReferences(fileName);
if (projectOutputs) {
for (const referenceEntry of projectOutputs) {
if (!contains(references, referenceEntry, documentSpansEqual)) {
if (!seen.has(referenceEntry)) {
references.push(referenceEntry);
seen.add(referenceEntry);
}
}
}

View File

@ -29,5 +29,167 @@ namespace ts {
assert.isTrue(equalOwnProperties({ a: 1 }, { a: 2 }, () => true), "valid equality");
});
});
describe("customSet", () => {
it("mutation", () => {
const set = createSet<number, number>(x => x % 2, (x, y) => (x % 4) === (y % 4));
assert.equal(set.size, 0);
const newSet = set.add(0);
assert.strictEqual(newSet, set);
assert.equal(set.size, 1);
set.add(1);
assert.equal(set.size, 2);
set.add(2); // Collision with 0
assert.equal(set.size, 3);
set.add(3); // Collision with 1
assert.equal(set.size, 4);
set.add(4); // Already present as 0
assert.equal(set.size, 4);
set.add(5); // Already present as 1
assert.equal(set.size, 4);
assert.isTrue(set.has(6));
assert.isTrue(set.has(7));
assert.isTrue(set.delete(8));
assert.equal(set.size, 3);
assert.isFalse(set.has(8));
assert.isFalse(set.delete(8));
assert.isTrue(set.delete(9));
assert.equal(set.size, 2);
assert.isTrue(set.delete(10));
assert.equal(set.size, 1);
assert.isTrue(set.delete(11));
assert.equal(set.size, 0);
});
it("resizing", () => {
const set = createSet<number, number>(x => x % 2, (x, y) => x === y);
const elementCount = 100;
for (let i = 0; i < elementCount; i++) {
assert.isFalse(set.has(i));
set.add(i);
assert.isTrue(set.has(i));
assert.equal(set.size, i + 1);
}
for (let i = 0; i < elementCount; i++) {
assert.isTrue(set.has(i));
set.delete(i);
assert.isFalse(set.has(i));
assert.equal(set.size, elementCount - (i + 1));
}
});
it("clear", () => {
const set = createSet<number, number>(x => x % 2, (x, y) => (x % 4) === (y % 4));
for (let j = 0; j < 2; j++) {
for (let i = 0; i < 100; i++) {
set.add(i);
}
assert.equal(set.size, 4);
set.clear();
assert.equal(set.size, 0);
assert.isFalse(set.has(0));
}
});
it("forEach", () => {
const set = createSet<number, number>(x => x % 2, (x, y) => (x % 4) === (y % 4));
for (let i = 0; i < 100; i++) {
set.add(i);
}
const values: number[] = [];
const keys: number[] = [];
set.forEach((value, key) => {
values.push(value);
keys.push(key);
});
assert.equal(values.length, 4);
values.sort();
keys.sort();
// NB: first equal value wins (i.e. not [96, 97, 98, 99])
const expected = [0, 1, 2, 3];
assert.deepEqual(values, expected);
assert.deepEqual(keys, expected);
});
it("iteration", () => {
const set = createSet<number, number>(x => x % 2, (x, y) => (x % 4) === (y % 4));
for (let i = 0; i < 4; i++) {
set.add(i);
}
const expected = [0, 1, 2, 3];
let actual: number[];
actual = arrayFrom(set.keys());
actual.sort();
assert.deepEqual(actual, expected);
actual = arrayFrom(set.values());
actual.sort();
assert.deepEqual(actual, expected);
const actualTuple = arrayFrom(set.entries());
assert.isFalse(actualTuple.some(([v, k]) => v !== k));
actual = actualTuple.map(([v, _]) => v);
actual.sort();
assert.deepEqual(actual, expected);
});
it("string hash code", () => {
interface Thing {
x: number;
y: string;
}
const set = createSet<Thing, string>(t => t.y, (t, u) => t.x === u.x && t.y === u.y);
const thing1: Thing = {
x: 1,
y: "a",
};
const thing2: Thing = {
x: 2,
y: "b",
};
const thing3: Thing = {
x: 3,
y: "a", // Collides with thing1
};
set.add(thing1);
set.add(thing2);
set.add(thing3);
assert.equal(set.size, 3);
assert.isTrue(set.has(thing1));
assert.isTrue(set.has(thing2));
assert.isTrue(set.has(thing3));
assert.isFalse(set.has({
x: 4,
y: "a", // Collides with thing1
}));
assert.isFalse(set.has({
x: 5,
y: "c", // No collision
}));
});
});
});
}