UNPKG

deep-equal-x

Version:

node's deepEqual and deepStrictEqual algorithm.

303 lines (264 loc) 10.1 kB
import isDate from 'is-date-object'; import isArguments from 'is-arguments'; import isPrimitive from 'is-primitive-x'; import isObject from 'is-object'; import isBuffer from 'is-buffer'; import isString from 'is-string'; import isError from 'is-error-x'; import isMap from 'is-map-x'; import isSet from 'is-set-x'; import isNil from 'is-nil-x'; import isRegExp from 'is-regexp-x'; import indexOf from 'index-of-x'; import slice from 'array-slice-x'; import some from 'array-some-x'; import filter from 'array-filter-x'; import sort from 'stable'; import $keys from 'object-keys-x'; import $getPrototypeOf from 'get-prototype-of-x'; import hasBoxedString from 'has-boxed-string-x'; import toBoolean from 'to-boolean-x'; import methodize from 'simple-methodize-x'; import toNumber from 'to-number-x'; // Check failure of by-index access of string characters (IE < 9) // and failure of `0 in boxedString` (Rhino) const hasBoxedStringBug = hasBoxedString === false; // Used to detect unsigned integer values. const reIsUint = /^(?:0|[1-9]\d*)$/; const methodizedTest = methodize(reIsUint.test); const methodizedRxToString = methodize(reIsUint.toString); const getTime = methodize(Date.prototype.getTime); const charAt = methodize(''.charAt); const tempArray = []; const push = methodize(tempArray.push); const pop = methodize(tempArray.pop); /* eslint-disable-next-line compat/compat */ const hasMapEnumerables = typeof Map === 'function' ? $keys(new Map()) : []; /* eslint-disable-next-line compat/compat */ const hasSetEnumerables = typeof Set === 'function' ? $keys(new Set()) : []; let hasErrorEnumerables; try { // noinspection ExceptionCaughtLocallyJS throw new Error('a'); } catch (e) { hasErrorEnumerables = $keys(e); } const indexNotFound = -1; const maxSafeIndex = 4294967295; // (2^32)-1 /** * Checks if `value` is a valid string index. Specifically for boxed string * bug fix and not general purpose. * * @private * @param {*} value - The value to check. * @returns {boolean} Returns `true` if `value` is valid index, else `false`. */ const isIndex = function isIndex(value) { let num = indexNotFound; if (methodizedTest(reIsUint, value)) { num = toNumber(value); } return num > indexNotFound && num % 1 === 0 && num < maxSafeIndex; }; // eslint-disable jsdoc/require-param // noinspection JSCommentMatchesSignature /** * Get an object's key avoiding boxed string bug. Specifically for boxed * string bug fix and not general purpose. * * @private * @param {Array|string|object} object - The object to get the `value` from. * @param {string|number} key - The `key` reference to the `value`. * @param {boolean} isStr - Is the object a string. * @param {boolean} isIdx - Is the `key` a character index. * @returns {*} Returns the `value` referenced by the `key`. */ // eslint-enable jsdoc/require-param const getItem = function getItem(args) { const [object, key, isStr, isIdx] = args; return isStr && isIdx ? charAt(key, object) : object[key]; }; /** * Filter `keys` of unwanted Error enumerables. Specifically for Error has * unwanted enumerables fix and not general purpose. * * @private * @param {Array} keys - The Error object's keys. * @param {Array} unwanted - The unwanted keys. * @returns {Array} Returns the filtered keys. */ const filterUnwanted = function filterUnwanted(keys, unwanted) { return unwanted.length ? filter(keys, function predicate(key) { return indexOf(unwanted, key) === indexNotFound; }) : keys; }; // eslint-disable jsdoc/require-param // noinspection JSCommentMatchesSignature /** * Tests for deep equality. Primitive values are compared with the equal * comparison operator ( == ). This only considers enumerable properties. * It does not test object prototypes, attached symbols, or non-enumerable * properties. This can lead to some potentially surprising results. If * `strict` is `true` then Primitive values are compared with the strict * equal comparison operator ( === ). * * @private * @param {*} actual - First comparison object. * @param {*} expected - Second comparison object. * @param {boolean} [strict] - Comparison mode. If set to `true` use `===`. * @param {object} previousStack - The circular stack. * @returns {boolean} `true` if `actual` and `expected` are deemed equal, * otherwise `false`. */ // eslint-enable jsdoc/require-param const baseDeepEqual = function baseDeepEqual(args) { const [actual, expected, strict, previousStack] = args; // 7.1. All identical values are equivalent, as determined by ===. if (actual === expected) { return true; } if (isBuffer(actual) && isBuffer(expected)) { return ( actual.length === expected.length && some(actual, function predicate(item, index) { return item !== expected[index]; }) === false ); } // 7.2. If the expected value is a Date object, the actual value is // equivalent if it is also a Date object that refers to the same time. if (isDate(actual) && isDate(expected)) { return getTime(actual) === getTime(expected); } // 7.3 If the expected value is a RegExp object, the actual value is // equivalent if it is also a RegExp object with the same `source` and // properties (`global`, `multiline`, `lastIndex`, `ignoreCase` & `sticky`). if (isRegExp(actual) && isRegExp(expected)) { return methodizedRxToString(actual) === methodizedRxToString(expected) && actual.lastIndex === expected.lastIndex; } // 7.4. Other pairs that do not both pass typeof value == 'object', // equivalence is determined by == or strict ===. if (isObject(actual) === false && isObject(expected) === false) { if (strict) { return actual === expected; } // noinspection EqualityComparisonWithCoercionJS return actual == expected; /* eslint-disable-line eqeqeq */ } // 7.5 For all other Object pairs, including Array objects, equivalence is // determined by having the same number of owned properties (as verified // with Object.prototype.hasOwnProperty.call), the same set of keys // (although not necessarily the same order), equivalent values for every // corresponding key, and an identical 'prototype' property. Note: this // accounts for both named and indexed properties on Arrays. if (isNil(actual) || isNil(expected)) { return false; } /* jshint eqnull:false */ // This only considers enumerable properties. It does not test object // prototypes, attached symbols, or non-enumerable properties. This can // lead to some potentially surprising results. if (strict && $getPrototypeOf(actual) !== $getPrototypeOf(expected)) { return false; } // if one is actual primitive, the other must be same if (isPrimitive(actual) || isPrimitive(expected)) { return actual === expected; } let ka = isArguments(actual); let kb = isArguments(expected); const aNotB = ka && kb === false; const bNotA = ka === false && kb; if (aNotB || bNotA) { return false; } if (ka) { if (ka.length !== kb.length) { return false; } return baseDeepEqual([slice(actual), slice(expected), strict, null]); } ka = $keys(actual); kb = $keys(expected); // having the same number of owned properties (keys incorporates hasOwnProperty) if (ka.length !== kb.length) { return false; } if (isObject(actual)) { if (isError(actual)) { ka = filterUnwanted(ka, hasErrorEnumerables); } else if (isMap(actual)) { ka = filterUnwanted(ka, hasMapEnumerables); } else if (isSet(actual)) { ka = filterUnwanted(ka, hasSetEnumerables); } } if (isObject(expected)) { if (isError(expected)) { kb = filterUnwanted(kb, hasErrorEnumerables); } else if (isMap(expected)) { kb = filterUnwanted(kb, hasMapEnumerables); } else if (isSet(expected)) { kb = filterUnwanted(kb, hasSetEnumerables); } } // the same set of keys (although not necessarily the same order), sort.inplace(ka); sort.inplace(kb); let aIsString; let bIsString; if (hasBoxedStringBug) { aIsString = isString(actual); bIsString = isString(expected); } // ~~~cheap key test // equivalent values for every corresponding key, and // ~~~possibly expensive deep test return ( some(ka, function predicate(key, index) { if (key !== kb[index]) { return true; } const isIdx = (aIsString || bIsString) && isIndex(key); const stack = previousStack || [actual]; const item = getItem([actual, key, aIsString, isIdx]); const isPrim = isPrimitive(item); if (isPrim === false) { if (indexOf(stack, item) !== indexNotFound) { throw new RangeError('Circular object'); } push(stack, item); } const result = baseDeepEqual([item, getItem([expected, key, bIsString, isIdx]), strict, stack]) === false; if (isPrim === false) { pop(stack); } return result; }) === false ); }; // eslint-enable jsdoc/require-param // noinspection JSCommentMatchesSignature /** * Tests for deep equality. Primitive values are compared with the equal * comparison operator ( == ). This only considers enumerable properties. * It does not test object prototypes, attached symbols, or non-enumerable * properties. This can lead to some potentially surprising results. If * `strict` is `true` then Primitive values are compared with the strict * equal comparison operator ( === ). * * @param {*} actual - First comparison object. * @param {*} expected - Second comparison object. * @param {boolean} [strict] - Comparison mode. If set to `true` use `===`. * @returns {boolean} `true` if `actual` and `expected` are deemed equal, * otherwise `false`. * @see https://nodejs.org/api/assert.html */ // eslint-disable jsdoc/require-param const deepEqual = function deepEqual(actual, expected) { /* eslint-disable-next-line prefer-rest-params */ return baseDeepEqual([actual, expected, toBoolean(arguments[2])]); }; export default deepEqual;