From 2402b090845dc132e1a1d93a08afeb1cd7675d60 Mon Sep 17 00:00:00 2001 From: Matt Phillips Date: Tue, 19 Mar 2019 15:40:38 +0000 Subject: [PATCH] Fix circular references in iterable equality --- packages/expect/src/__tests__/utils.test.js | 52 +++++++++++++++++++++ packages/expect/src/utils.ts | 44 ++++++++++++++--- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/packages/expect/src/__tests__/utils.test.js b/packages/expect/src/__tests__/utils.test.js index 180b514a1a77..3f3bb3980059 100644 --- a/packages/expect/src/__tests__/utils.test.js +++ b/packages/expect/src/__tests__/utils.test.js @@ -15,6 +15,7 @@ const { getPath, hasOwnProperty, subsetEquality, + iterableEquality, } = require('../utils'); describe('getPath()', () => { @@ -202,3 +203,54 @@ describe('subsetEquality()', () => { expect(subsetEquality(undefined, {foo: 'bar'})).not.toBeTruthy(); }); }); + +describe('iterableEquality', () => { + test('returns true when given circular iterators', () => { + class Iter { + *[Symbol.iterator]() { + yield this; + } + } + + const a = new Iter(); + const b = new Iter(); + + expect(iterableEquality(a, b)).toBe(true); + }); + + test('returns true when given circular Set', () => { + const a = new Set(); + a.add(a); + const b = new Set(); + b.add(b); + + expect(iterableEquality(a, b)).toBe(true); + }); + + test('returns true when given circular key in Map', () => { + const a = new Map(); + a.set(a, 'a'); + const b = new Map(); + b.set(b, 'a'); + + expect(iterableEquality(a, b)).toBe(true); + }); + + test('returns true when given circular key and value in Map', () => { + const a = new Map(); + a.set(a, a); + const b = new Map(); + b.set(b, b); + + expect(iterableEquality(a, b)).toBe(true); + }); + + test('returns true when given circular value in Map', () => { + const a = new Map(); + a.set('a', a); + const b = new Map(); + b.set('a', b); + + expect(iterableEquality(a, b)).toBe(true); + }); +}); diff --git a/packages/expect/src/utils.ts b/packages/expect/src/utils.ts index 13047d555ff1..06533fccce69 100644 --- a/packages/expect/src/utils.ts +++ b/packages/expect/src/utils.ts @@ -137,7 +137,13 @@ const IteratorSymbol = Symbol.iterator; const hasIterator = (object: any) => !!(object != null && object[IteratorSymbol]); -export const iterableEquality = (a: any, b: any) => { + +export const iterableEquality = ( + a: any, + b: any, + aStack: Array = [], + bStack: Array = [], +) => { if ( typeof a !== 'object' || typeof b !== 'object' || @@ -152,6 +158,24 @@ export const iterableEquality = (a: any, b: any) => { return false; } + let length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + // circular references at same depth are equal + // circular reference is not equal to non-circular one + if (aStack[length] === a) { + return bStack[length] === b; + } else if (bStack[length] === b) { + return false; + } + } + aStack.push(a); + bStack.push(b); + + const iterableEqualityWithStack = (a: any, b: any) => + iterableEquality(a, b, aStack, bStack); + if (a.size !== undefined) { if (a.size !== b.size) { return false; @@ -161,7 +185,7 @@ export const iterableEquality = (a: any, b: any) => { if (!b.has(aValue)) { let has = false; for (const bValue of b) { - const isEqual = equals(aValue, bValue, [iterableEquality]); + const isEqual = equals(aValue, bValue, [iterableEqualityWithStack]); if (isEqual === true) { has = true; } @@ -181,17 +205,20 @@ export const iterableEquality = (a: any, b: any) => { for (const aEntry of a) { if ( !b.has(aEntry[0]) || - !equals(aEntry[1], b.get(aEntry[0]), [iterableEquality]) + !equals(aEntry[1], b.get(aEntry[0]), [iterableEqualityWithStack]) ) { let has = false; for (const bEntry of b) { - const matchedKey = equals(aEntry[0], bEntry[0], [iterableEquality]); + const matchedKey = equals(aEntry[0], bEntry[0], [ + iterableEqualityWithStack, + ]); let matchedValue = false; if (matchedKey === true) { - matchedValue = equals(aEntry[1], bEntry[1], [iterableEquality]); + matchedValue = equals(aEntry[1], bEntry[1], [ + iterableEqualityWithStack, + ]); } - if (matchedValue === true) { has = true; } @@ -213,7 +240,10 @@ export const iterableEquality = (a: any, b: any) => { for (const aValue of a) { const nextB = bIterator.next(); - if (nextB.done || !equals(aValue, nextB.value, [iterableEquality])) { + if ( + nextB.done || + !equals(aValue, nextB.value, [iterableEqualityWithStack]) + ) { return false; } }