mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-05 16:38:05 -06:00
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:
parent
93c3a30edc
commit
ce9657d5e2
@ -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.
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user