Skip to content

Commit ce9657d

Browse files
authoredMar 12, 2022
Introduce set with custom equals and getHashCode (microsoft#48169)
* Implement set with custom equals and getHashCode * Adopt custom set in session * Add doc comment * Initially store buckets as non-arrays
1 parent 93c3a30 commit ce9657d

File tree

3 files changed

+325
-2
lines changed

3 files changed

+325
-2
lines changed
 

‎src/compiler/core.ts

+153
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,159 @@ namespace ts {
14881488
return createMultiMap() as UnderscoreEscapedMultiMap<T>;
14891489
}
14901490

1491+
/**
1492+
* Creates a Set with custom equality and hash code functionality. This is useful when you
1493+
* want to use something looser than object identity - e.g. "has the same span".
1494+
*
1495+
* If `equals(a, b)`, it must be the case that `getHashCode(a) === getHashCode(b)`.
1496+
* The converse is not required.
1497+
*
1498+
* To facilitate a perf optimization (lazy allocation of bucket arrays), `TElement` is
1499+
* assumed not to be an array type.
1500+
*/
1501+
export function createSet<TElement, THash = number>(getHashCode: (element: TElement) => THash, equals: EqualityComparer<TElement>): Set<TElement> {
1502+
const multiMap = new Map<THash, TElement | TElement[]>();
1503+
let size = 0;
1504+
1505+
function getElementIterator(): Iterator<TElement> {
1506+
const valueIt = multiMap.values();
1507+
let arrayIt: Iterator<TElement> | undefined;
1508+
return {
1509+
next: () => {
1510+
while (true) {
1511+
if (arrayIt) {
1512+
const n = arrayIt.next();
1513+
if (!n.done) {
1514+
return { value: n.value };
1515+
}
1516+
arrayIt = undefined;
1517+
}
1518+
else {
1519+
const n = valueIt.next();
1520+
if (n.done) {
1521+
return { value: undefined, done: true };
1522+
}
1523+
if (!isArray(n.value)) {
1524+
return { value: n.value };
1525+
}
1526+
arrayIt = arrayIterator(n.value);
1527+
}
1528+
}
1529+
}
1530+
};
1531+
}
1532+
1533+
const set: Set<TElement> = {
1534+
has(element: TElement): boolean {
1535+
const hash = getHashCode(element);
1536+
if (!multiMap.has(hash)) return false;
1537+
const candidates = multiMap.get(hash)!;
1538+
if (!isArray(candidates)) return equals(candidates, element);
1539+
1540+
for (const candidate of candidates) {
1541+
if (equals(candidate, element)) {
1542+
return true;
1543+
}
1544+
}
1545+
return false;
1546+
},
1547+
add(element: TElement): Set<TElement> {
1548+
const hash = getHashCode(element);
1549+
if (multiMap.has(hash)) {
1550+
const values = multiMap.get(hash)!;
1551+
if (isArray(values)) {
1552+
if (!contains(values, element, equals)) {
1553+
values.push(element);
1554+
size++;
1555+
}
1556+
}
1557+
else {
1558+
const value = values;
1559+
if (!equals(value, element)) {
1560+
multiMap.set(hash, [ value, element ]);
1561+
size++;
1562+
}
1563+
}
1564+
}
1565+
else {
1566+
multiMap.set(hash, element);
1567+
size++;
1568+
}
1569+
1570+
return this;
1571+
},
1572+
delete(element: TElement): boolean {
1573+
const hash = getHashCode(element);
1574+
if (!multiMap.has(hash)) return false;
1575+
const candidates = multiMap.get(hash)!;
1576+
if (isArray(candidates)) {
1577+
for (let i = 0; i < candidates.length; i++) {
1578+
if (equals(candidates[i], element)) {
1579+
if (candidates.length === 1) {
1580+
multiMap.delete(hash);
1581+
}
1582+
else if (candidates.length === 2) {
1583+
multiMap.set(hash, candidates[1 - i]);
1584+
}
1585+
else {
1586+
unorderedRemoveItemAt(candidates, i);
1587+
}
1588+
size--;
1589+
return true;
1590+
}
1591+
}
1592+
}
1593+
else {
1594+
const candidate = candidates;
1595+
if (equals(candidate, element)) {
1596+
multiMap.delete(hash);
1597+
size--;
1598+
return true;
1599+
}
1600+
}
1601+
1602+
return false;
1603+
},
1604+
clear(): void {
1605+
multiMap.clear();
1606+
size = 0;
1607+
},
1608+
get size() {
1609+
return size;
1610+
},
1611+
forEach(action: (value: TElement, key: TElement) => void): void {
1612+
for (const elements of arrayFrom(multiMap.values())) {
1613+
if (isArray(elements)) {
1614+
for (const element of elements) {
1615+
action(element, element);
1616+
}
1617+
}
1618+
else {
1619+
const element = elements;
1620+
action(element, element);
1621+
}
1622+
}
1623+
},
1624+
keys(): Iterator<TElement> {
1625+
return getElementIterator();
1626+
},
1627+
values(): Iterator<TElement> {
1628+
return getElementIterator();
1629+
},
1630+
entries(): Iterator<[TElement, TElement]> {
1631+
const it = getElementIterator();
1632+
return {
1633+
next: () => {
1634+
const n = it.next();
1635+
return n.done ? n : { value: [ n.value, n.value ] };
1636+
}
1637+
};
1638+
},
1639+
};
1640+
1641+
return set;
1642+
}
1643+
14911644
/**
14921645
* Tests whether a value is an array.
14931646
*/

‎src/server/session.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ namespace ts.server {
299299
navigateToItems: readonly NavigateToItem[];
300300
};
301301

302+
function createDocumentSpanSet(): Set<DocumentSpan> {
303+
return createSet(({textSpan}) => textSpan.start + 100003 * textSpan.length, documentSpansEqual);
304+
}
305+
302306
function combineProjectOutputForRenameLocations(
303307
projects: Projects,
304308
defaultProject: Project,
@@ -308,6 +312,7 @@ namespace ts.server {
308312
{ providePrefixAndSuffixTextForRename }: UserPreferences
309313
): readonly RenameLocation[] {
310314
const outputs: RenameLocation[] = [];
315+
const seen = createDocumentSpanSet();
311316
combineProjectOutputWorker(
312317
projects,
313318
defaultProject,
@@ -316,7 +321,8 @@ namespace ts.server {
316321
const projectOutputs = project.getLanguageService().findRenameLocations(location.fileName, location.pos, findInStrings, findInComments, providePrefixAndSuffixTextForRename);
317322
if (projectOutputs) {
318323
for (const output of projectOutputs) {
319-
if (!contains(outputs, output, documentSpansEqual) && !tryAddToTodo(project, documentSpanLocation(output))) {
324+
if (!seen.has(output) && !tryAddToTodo(project, documentSpanLocation(output))) {
325+
seen.add(output);
320326
outputs.push(output);
321327
}
322328
}
@@ -1558,15 +1564,17 @@ namespace ts.server {
15581564
const fileName = args.file;
15591565

15601566
const references: ReferenceEntry[] = [];
1567+
const seen = createDocumentSpanSet();
15611568

15621569
forEachProjectInProjects(projects, /*path*/ undefined, project => {
15631570
if (project.getCancellationToken().isCancellationRequested()) return;
15641571

15651572
const projectOutputs = project.getLanguageService().getFileReferences(fileName);
15661573
if (projectOutputs) {
15671574
for (const referenceEntry of projectOutputs) {
1568-
if (!contains(references, referenceEntry, documentSpansEqual)) {
1575+
if (!seen.has(referenceEntry)) {
15691576
references.push(referenceEntry);
1577+
seen.add(referenceEntry);
15701578
}
15711579
}
15721580
}

‎src/testRunner/unittests/compilerCore.ts

+162
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,167 @@ namespace ts {
2929
assert.isTrue(equalOwnProperties({ a: 1 }, { a: 2 }, () => true), "valid equality");
3030
});
3131
});
32+
describe("customSet", () => {
33+
it("mutation", () => {
34+
const set = createSet<number, number>(x => x % 2, (x, y) => (x % 4) === (y % 4));
35+
assert.equal(set.size, 0);
36+
37+
const newSet = set.add(0);
38+
assert.strictEqual(newSet, set);
39+
assert.equal(set.size, 1);
40+
41+
set.add(1);
42+
assert.equal(set.size, 2);
43+
44+
set.add(2); // Collision with 0
45+
assert.equal(set.size, 3);
46+
47+
set.add(3); // Collision with 1
48+
assert.equal(set.size, 4);
49+
50+
set.add(4); // Already present as 0
51+
assert.equal(set.size, 4);
52+
53+
set.add(5); // Already present as 1
54+
assert.equal(set.size, 4);
55+
56+
assert.isTrue(set.has(6));
57+
assert.isTrue(set.has(7));
58+
59+
assert.isTrue(set.delete(8));
60+
assert.equal(set.size, 3);
61+
assert.isFalse(set.has(8));
62+
assert.isFalse(set.delete(8));
63+
64+
assert.isTrue(set.delete(9));
65+
assert.equal(set.size, 2);
66+
67+
assert.isTrue(set.delete(10));
68+
assert.equal(set.size, 1);
69+
70+
assert.isTrue(set.delete(11));
71+
assert.equal(set.size, 0);
72+
});
73+
it("resizing", () => {
74+
const set = createSet<number, number>(x => x % 2, (x, y) => x === y);
75+
const elementCount = 100;
76+
77+
for (let i = 0; i < elementCount; i++) {
78+
assert.isFalse(set.has(i));
79+
set.add(i);
80+
assert.isTrue(set.has(i));
81+
assert.equal(set.size, i + 1);
82+
}
83+
84+
for (let i = 0; i < elementCount; i++) {
85+
assert.isTrue(set.has(i));
86+
set.delete(i);
87+
assert.isFalse(set.has(i));
88+
assert.equal(set.size, elementCount - (i + 1));
89+
}
90+
});
91+
it("clear", () => {
92+
const set = createSet<number, number>(x => x % 2, (x, y) => (x % 4) === (y % 4));
93+
for (let j = 0; j < 2; j++) {
94+
for (let i = 0; i < 100; i++) {
95+
set.add(i);
96+
}
97+
assert.equal(set.size, 4);
98+
99+
set.clear();
100+
assert.equal(set.size, 0);
101+
assert.isFalse(set.has(0));
102+
}
103+
});
104+
it("forEach", () => {
105+
const set = createSet<number, number>(x => x % 2, (x, y) => (x % 4) === (y % 4));
106+
for (let i = 0; i < 100; i++) {
107+
set.add(i);
108+
}
109+
110+
const values: number[] = [];
111+
const keys: number[] = [];
112+
set.forEach((value, key) => {
113+
values.push(value);
114+
keys.push(key);
115+
});
116+
117+
assert.equal(values.length, 4);
118+
119+
values.sort();
120+
keys.sort();
121+
122+
// NB: first equal value wins (i.e. not [96, 97, 98, 99])
123+
const expected = [0, 1, 2, 3];
124+
assert.deepEqual(values, expected);
125+
assert.deepEqual(keys, expected);
126+
});
127+
it("iteration", () => {
128+
const set = createSet<number, number>(x => x % 2, (x, y) => (x % 4) === (y % 4));
129+
for (let i = 0; i < 4; i++) {
130+
set.add(i);
131+
}
132+
133+
const expected = [0, 1, 2, 3];
134+
let actual: number[];
135+
136+
actual = arrayFrom(set.keys());
137+
actual.sort();
138+
assert.deepEqual(actual, expected);
139+
140+
actual = arrayFrom(set.values());
141+
actual.sort();
142+
assert.deepEqual(actual, expected);
143+
144+
const actualTuple = arrayFrom(set.entries());
145+
assert.isFalse(actualTuple.some(([v, k]) => v !== k));
146+
actual = actualTuple.map(([v, _]) => v);
147+
actual.sort();
148+
assert.deepEqual(actual, expected);
149+
});
150+
it("string hash code", () => {
151+
interface Thing {
152+
x: number;
153+
y: string;
154+
}
155+
156+
const set = createSet<Thing, string>(t => t.y, (t, u) => t.x === u.x && t.y === u.y);
157+
158+
const thing1: Thing = {
159+
x: 1,
160+
y: "a",
161+
};
162+
163+
const thing2: Thing = {
164+
x: 2,
165+
y: "b",
166+
};
167+
168+
const thing3: Thing = {
169+
x: 3,
170+
y: "a", // Collides with thing1
171+
};
172+
173+
set.add(thing1);
174+
set.add(thing2);
175+
set.add(thing3);
176+
177+
assert.equal(set.size, 3);
178+
179+
assert.isTrue(set.has(thing1));
180+
assert.isTrue(set.has(thing2));
181+
assert.isTrue(set.has(thing3));
182+
183+
assert.isFalse(set.has({
184+
x: 4,
185+
y: "a", // Collides with thing1
186+
}));
187+
188+
assert.isFalse(set.has({
189+
x: 5,
190+
y: "c", // No collision
191+
}));
192+
});
193+
});
32194
});
33195
}

0 commit comments

Comments
 (0)
Please sign in to comment.