Skip to content

Commit f3b5ea1

Browse files
committed
fix: Fix WeakMap set error, arg order irregularities
Contains a minor speed boost as well and helps comparators override all behavior, including simple tests.
1 parent 718922d commit f3b5ea1

2 files changed

Lines changed: 82 additions & 23 deletions

File tree

index.js

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ if (typeof WeakMap === 'function') {
5959
* @returns {Boolean|null} result
6060
*/
6161
function memoizeCompare(leftHandOperand, rightHandOperand, memoizeMap) {
62-
if (!memoizeMap) {
62+
// Technically, WeakMap keys can *only* be objects, not primitives.
63+
if (!memoizeMap || isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
6364
return null;
6465
}
6566
var leftHandMap = memoizeMap.get(leftHandOperand);
@@ -81,7 +82,8 @@ function memoizeCompare(leftHandOperand, rightHandOperand, memoizeMap) {
8182
* @param {Boolean} result
8283
*/
8384
function memoizeSet(leftHandOperand, rightHandOperand, memoizeMap, result) {
84-
if (!memoizeMap || typeof leftHandOperand !== 'object' || typeof rightHandOperand !== 'object') {
85+
// Technically, WeakMap keys can *only* be objects, not primitives.
86+
if (!memoizeMap || isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
8587
return;
8688
}
8789
var leftHandMap = memoizeMap.get(leftHandOperand);
@@ -114,6 +116,27 @@ module.exports.MemoizeMap = MemoizeMap;
114116
* @return {Boolean} equal match
115117
*/
116118
function deepEqual(leftHandOperand, rightHandOperand, options) {
119+
// If we have a comparator, we can't assume anything; so bail to its check first.
120+
if (options && options.comparator) {
121+
return extensiveDeepEqual(leftHandOperand, rightHandOperand, options);
122+
}
123+
124+
var simpleResult = simpleEqual(leftHandOperand, rightHandOperand);
125+
if (simpleResult !== null) {
126+
return simpleResult;
127+
}
128+
129+
// Deeper comparisons are pushed through to a larger function
130+
return extensiveDeepEqual(leftHandOperand, rightHandOperand, options);
131+
}
132+
133+
/**
134+
* Many comparisons can be canceled out early via simple equality or primitive checks.
135+
* @param {Mixed} leftHandOperand
136+
* @param {Mixed} rightHandOperand
137+
* @return {Boolean|null} equal match
138+
*/
139+
function simpleEqual(leftHandOperand, rightHandOperand) {
117140
// Equal references (except for Numbers) can be returned early
118141
if (leftHandOperand === rightHandOperand) {
119142
// Handle +-0 cases
@@ -128,17 +151,13 @@ function deepEqual(leftHandOperand, rightHandOperand, options) {
128151
return true;
129152
}
130153

131-
// Primitives/null/undefined can be checked for referential equality; returning early
132-
if (
133-
typeof leftHandOperand !== 'object' ||
134-
leftHandOperand === null ||
135-
leftHandOperand === undefined // eslint-disable-line no-undefined
136-
) {
137-
return leftHandOperand === rightHandOperand;
154+
// Anything that is not an 'object', i.e. symbols, functions, booleans, numbers,
155+
// strings, and undefined, can be compared by reference.
156+
if (isPrimitive(leftHandOperand) || isPrimitive(rightHandOperand)) {
157+
// Easy out b/c it would have passed the first equality check
158+
return false;
138159
}
139-
140-
// Deeper comparisons are pushed through to a larger function
141-
return extensiveDeepEqual(leftHandOperand, rightHandOperand, options);
160+
return null;
142161
}
143162

144163
/*!
@@ -158,32 +177,44 @@ function extensiveDeepEqual(leftHandOperand, rightHandOperand, options) {
158177
options.memoize = options.memoize === false ? false : options.memoize || new MemoizeMap();
159178
var comparator = options && options.comparator;
160179

161-
var memoizeResult = memoizeCompare(leftHandOperand, rightHandOperand, options.memoize);
162-
if (memoizeResult !== null) {
163-
return memoizeResult;
180+
// Check if a memoized result exists.
181+
var memoizeResultLeft = memoizeCompare(leftHandOperand, rightHandOperand, options.memoize);
182+
if (memoizeResultLeft !== null) {
183+
return memoizeResultLeft;
184+
}
185+
var memoizeResultRight = memoizeCompare(rightHandOperand, leftHandOperand, options.memoize);
186+
if (memoizeResultRight !== null) {
187+
return memoizeResultRight;
164188
}
165189

166-
var comparatorResult = comparator && comparator(leftHandOperand, rightHandOperand);
167-
if (comparatorResult === false || comparatorResult === true) {
168-
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, comparatorResult);
169-
memoizeSet(rightHandOperand, leftHandOperand, options.memoize, comparatorResult);
170-
return comparatorResult;
190+
// If a comparator is present, use it.
191+
if (comparator) {
192+
var comparatorResult = comparator(leftHandOperand, rightHandOperand);
193+
// Comparators may return null, in which case we want to go back to default behavior.
194+
if (comparatorResult === false || comparatorResult === true) {
195+
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, comparatorResult);
196+
return comparatorResult;
197+
}
198+
// To allow comparators to override *any* behavior, we ran them first. Since it didn't decide
199+
// what to do, we need to make sure to return the basic tests first before we move on.
200+
var simpleResult = simpleEqual(leftHandOperand, rightHandOperand);
201+
if (simpleResult !== null) {
202+
// Don't memoize this, it takes longer to set/retrieve than to just compare.
203+
return simpleResult;
204+
}
171205
}
172206

173207
var leftHandType = type(leftHandOperand);
174208
if (leftHandType !== type(rightHandOperand)) {
175209
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, false);
176-
memoizeSet(rightHandOperand, leftHandOperand, options.memoize, false);
177210
return false;
178211
}
179212

180213
// Temporarily set the operands in the memoize object to prevent blowing the stack
181214
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, true);
182-
memoizeSet(rightHandOperand, leftHandOperand, options.memoize, true);
183215

184216
var result = extensiveDeepEqualByType(leftHandOperand, rightHandOperand, leftHandType, options);
185217
memoizeSet(leftHandOperand, rightHandOperand, options.memoize, result);
186-
memoizeSet(rightHandOperand, leftHandOperand, options.memoize, result);
187218
return result;
188219
}
189220

@@ -433,3 +464,16 @@ function objectEqual(leftHandOperand, rightHandOperand, options) {
433464

434465
return false;
435466
}
467+
468+
/*!
469+
* Returns true if the argument is a primitive.
470+
*
471+
* This intentionally returns true for all objects that can be compared by reference,
472+
* including functions and symbols.
473+
*
474+
* @param {Mixed} value
475+
* @return {Boolean} result
476+
*/
477+
function isPrimitive(value) {
478+
return value === null || typeof value !== 'object';
479+
}

test/index.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ describe('Generic', function () {
7676
assert(eql(null, undefined) === false, 'eql(null, undefined) === false');
7777
});
7878

79+
it('doesn\'t crash on weakmap key error (#33)', function () {
80+
assert(eql({}, null) === false, 'eql({}, null) === false');
81+
});
82+
7983
});
8084

8185
describe('undefined', function () {
@@ -471,6 +475,17 @@ describe('Comparator', function () {
471475
'eql({a:value => typeof value === "number"}, {a:1}, <comparator>) === true');
472476
});
473477

478+
it('returns true if Comparator says so even on primitives (switch arg order)', function () {
479+
var valueA = { a: 1 };
480+
var valueB = {
481+
a: new Matcher(function (value) {
482+
return typeof value === 'number';
483+
}),
484+
};
485+
assert(eql(valueA, valueB, { comparator: matcherComparator }) === true,
486+
'eql({a:1}, {a:value => typeof value === "number"}, <comparator>) === true');
487+
});
488+
474489
it('returns true if Comparator says so (deep-equality)', function () {
475490
var valueA = { a: { '@@specialValue': 1, a: 1 }, b: 1 };
476491
var valueB = { a: { '@@specialValue': 1, a: 2 }, b: 1 };

0 commit comments

Comments
 (0)