Skip to content

Commit b70586f

Browse files
committed
Fix circular references in iterable equality
1 parent ce65aac commit b70586f

File tree

2 files changed

+131
-13
lines changed

2 files changed

+131
-13
lines changed

packages/expect/src/__tests__/utils.test.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
getPath,
1616
hasOwnProperty,
1717
subsetEquality,
18+
iterableEquality,
1819
} = require('../utils');
1920

2021
describe('getPath()', () => {
@@ -202,3 +203,83 @@ describe('subsetEquality()', () => {
202203
expect(subsetEquality(undefined, {foo: 'bar'})).not.toBeTruthy();
203204
});
204205
});
206+
207+
describe('iterableEquality', () => {
208+
test('returns true when given circular iterators', () => {
209+
class Iter {
210+
*[Symbol.iterator]() {
211+
yield this;
212+
}
213+
}
214+
215+
const a = new Iter();
216+
const b = new Iter();
217+
218+
expect(iterableEquality(a, b)).toBe(true);
219+
});
220+
221+
test('returns true when given circular Set', () => {
222+
const a = new Set();
223+
a.add(a);
224+
const b = new Set();
225+
b.add(b);
226+
expect(iterableEquality(a, b)).toBe(true);
227+
});
228+
229+
test('returns true when given nested Sets', () => {
230+
expect(
231+
iterableEquality(
232+
new Set([new Set([[1]]), new Set([[2]])]),
233+
new Set([new Set([[2]]), new Set([[1]])]),
234+
),
235+
).toBe(true);
236+
expect(
237+
iterableEquality(
238+
new Set([new Set([[1]]), new Set([[2]])]),
239+
new Set([new Set([[3]]), new Set([[1]])]),
240+
),
241+
).toBe(false);
242+
});
243+
244+
test('returns true when given circular key in Map', () => {
245+
const a = new Map();
246+
a.set(a, 'a');
247+
const b = new Map();
248+
b.set(b, 'a');
249+
250+
expect(iterableEquality(a, b)).toBe(true);
251+
});
252+
253+
test('returns true when given nested Maps', () => {
254+
expect(
255+
iterableEquality(
256+
new Map([['hello', new Map([['world', 'foobar']])]]),
257+
new Map([['hello', new Map([['world', 'qux']])]]),
258+
),
259+
).toBe(false);
260+
expect(
261+
iterableEquality(
262+
new Map([['hello', new Map([['world', 'foobar']])]]),
263+
new Map([['hello', new Map([['world', 'foobar']])]]),
264+
),
265+
).toBe(true);
266+
});
267+
268+
test('returns true when given circular key and value in Map', () => {
269+
const a = new Map();
270+
a.set(a, a);
271+
const b = new Map();
272+
b.set(b, b);
273+
274+
expect(iterableEquality(a, b)).toBe(true);
275+
});
276+
277+
test('returns true when given circular value in Map', () => {
278+
const a = new Map();
279+
a.set('a', a);
280+
const b = new Map();
281+
b.set('a', b);
282+
283+
expect(iterableEquality(a, b)).toBe(true);
284+
});
285+
});

packages/expect/src/utils.ts

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,13 @@ const IteratorSymbol = Symbol.iterator;
137137

138138
const hasIterator = (object: any) =>
139139
!!(object != null && object[IteratorSymbol]);
140-
export const iterableEquality = (a: any, b: any) => {
140+
141+
export const iterableEquality = (
142+
a: any,
143+
b: any,
144+
aStack: Array<any> = [],
145+
bStack: Array<any> = [],
146+
) => {
141147
if (
142148
typeof a !== 'object' ||
143149
typeof b !== 'object' ||
@@ -152,6 +158,24 @@ export const iterableEquality = (a: any, b: any) => {
152158
return false;
153159
}
154160

161+
let length = aStack.length;
162+
while (length--) {
163+
// Linear search. Performance is inversely proportional to the number of
164+
// unique nested structures.
165+
// circular references at same depth are equal
166+
// circular reference is not equal to non-circular one
167+
if (aStack[length] === a) {
168+
return bStack[length] === b;
169+
} else if (bStack[length] === b) {
170+
return false;
171+
}
172+
}
173+
aStack.push(a);
174+
bStack.push(b);
175+
176+
const iterableEqualityWithStack = (a: any, b: any) =>
177+
iterableEquality(a, b, aStack, bStack);
178+
155179
if (a.size !== undefined) {
156180
if (a.size !== b.size) {
157181
return false;
@@ -161,7 +185,7 @@ export const iterableEquality = (a: any, b: any) => {
161185
if (!b.has(aValue)) {
162186
let has = false;
163187
for (const bValue of b) {
164-
const isEqual = equals(aValue, bValue, [iterableEquality]);
188+
const isEqual = equals(aValue, bValue, [iterableEqualityWithStack]);
165189
if (isEqual === true) {
166190
has = true;
167191
}
@@ -173,25 +197,30 @@ export const iterableEquality = (a: any, b: any) => {
173197
}
174198
}
175199
}
176-
if (allFound) {
177-
return true;
178-
}
200+
201+
aStack.pop();
202+
bStack.pop();
203+
204+
return allFound;
179205
} else if (isA('Map', a) || isImmutableUnorderedKeyed(a)) {
180206
let allFound = true;
181207
for (const aEntry of a) {
182208
if (
183209
!b.has(aEntry[0]) ||
184-
!equals(aEntry[1], b.get(aEntry[0]), [iterableEquality])
210+
!equals(aEntry[1], b.get(aEntry[0]), [iterableEqualityWithStack])
185211
) {
186212
let has = false;
187213
for (const bEntry of b) {
188-
const matchedKey = equals(aEntry[0], bEntry[0], [iterableEquality]);
214+
const matchedKey = equals(aEntry[0], bEntry[0], [
215+
iterableEqualityWithStack,
216+
]);
189217

190218
let matchedValue = false;
191219
if (matchedKey === true) {
192-
matchedValue = equals(aEntry[1], bEntry[1], [iterableEquality]);
220+
matchedValue = equals(aEntry[1], bEntry[1], [
221+
iterableEqualityWithStack,
222+
]);
193223
}
194-
195224
if (matchedValue === true) {
196225
has = true;
197226
}
@@ -203,23 +232,31 @@ export const iterableEquality = (a: any, b: any) => {
203232
}
204233
}
205234
}
206-
if (allFound) {
207-
return true;
208-
}
235+
aStack.pop();
236+
bStack.pop();
237+
return allFound;
209238
}
210239
}
211240

212241
const bIterator = b[IteratorSymbol]();
213242

214243
for (const aValue of a) {
215244
const nextB = bIterator.next();
216-
if (nextB.done || !equals(aValue, nextB.value, [iterableEquality])) {
245+
if (
246+
nextB.done ||
247+
!equals(aValue, nextB.value, [
248+
(a: any, b: any) => iterableEquality(a, b, aStack, bStack),
249+
])
250+
) {
217251
return false;
218252
}
219253
}
220254
if (!bIterator.next().done) {
221255
return false;
222256
}
257+
258+
aStack.pop();
259+
bStack.pop();
223260
return true;
224261
};
225262

0 commit comments

Comments
 (0)