'use strict'; var assign = require('object.assign'); var callBound = require('call-bind/callBound'); var flags = require('regexp.prototype.flags'); var GetIntrinsic = require('get-intrinsic'); var getIterator = require('es-get-iterator'); var getSideChannel = require('side-channel'); var is = require('object-is'); var isArguments = require('is-arguments'); var isArray = require('isarray'); var isArrayBuffer = require('is-array-buffer'); var isDate = require('is-date-object'); var isRegex = require('is-regex'); var isSharedArrayBuffer = require('is-shared-array-buffer'); var objectKeys = require('object-keys'); var whichBoxedPrimitive = require('which-boxed-primitive'); var whichCollection = require('which-collection'); var whichTypedArray = require('which-typed-array'); var byteLength = callBound('ArrayBuffer.prototype.byteLength', true) || function byteLength(ab) { return ab.byteLength; }; // in node < 0.11, byteLength is an own nonconfigurable property var sabByteLength = callBound('SharedArrayBuffer.prototype.byteLength', true); var $getTime = callBound('Date.prototype.getTime'); var gPO = Object.getPrototypeOf; var $objToString = callBound('Object.prototype.toString'); var $Set = GetIntrinsic('%Set%', true); var $mapHas = callBound('Map.prototype.has', true); var $mapGet = callBound('Map.prototype.get', true); var $mapSize = callBound('Map.prototype.size', true); var $setAdd = callBound('Set.prototype.add', true); var $setDelete = callBound('Set.prototype.delete', true); var $setHas = callBound('Set.prototype.has', true); var $setSize = callBound('Set.prototype.size', true); // taken from https://github.com/browserify/commonjs-assert/blob/bba838e9ba9e28edf3127ce6974624208502f6bc/internal/util/comparisons.js#L401-L414 function setHasEqualElement(set, val1, opts, channel) { var i = getIterator(set); var result; while ((result = i.next()) && !result.done) { if (internalDeepEqual(val1, result.value, opts, channel)) { // eslint-disable-line no-use-before-define // Remove the matching element to make sure we do not check that again. $setDelete(set, result.value); return true; } } return false; } // taken from https://github.com/browserify/commonjs-assert/blob/bba838e9ba9e28edf3127ce6974624208502f6bc/internal/util/comparisons.js#L416-L439 function findLooseMatchingPrimitives(prim) { if (typeof prim === 'undefined') { return null; } if (typeof prim === 'object') { // Only pass in null as object! return void 0; } if (typeof prim === 'symbol') { return false; } if (typeof prim === 'string' || typeof prim === 'number') { // Loose equal entries exist only if the string is possible to convert to a regular number and not NaN. return +prim === +prim; // eslint-disable-line no-implicit-coercion } return true; } // taken from https://github.com/browserify/commonjs-assert/blob/bba838e9ba9e28edf3127ce6974624208502f6bc/internal/util/comparisons.js#L449-L460 function mapMightHaveLoosePrim(a, b, prim, item, opts, channel) { var altValue = findLooseMatchingPrimitives(prim); if (altValue != null) { return altValue; } var curB = $mapGet(b, altValue); var looseOpts = assign({}, opts, { strict: false }); if ( (typeof curB === 'undefined' && !$mapHas(b, altValue)) // eslint-disable-next-line no-use-before-define || !internalDeepEqual(item, curB, looseOpts, channel) ) { return false; } // eslint-disable-next-line no-use-before-define return !$mapHas(a, altValue) && internalDeepEqual(item, curB, looseOpts, channel); } // taken from https://github.com/browserify/commonjs-assert/blob/bba838e9ba9e28edf3127ce6974624208502f6bc/internal/util/comparisons.js#L441-L447 function setMightHaveLoosePrim(a, b, prim) { var altValue = findLooseMatchingPrimitives(prim); if (altValue != null) { return altValue; } return $setHas(b, altValue) && !$setHas(a, altValue); } // taken from https://github.com/browserify/commonjs-assert/blob/bba838e9ba9e28edf3127ce6974624208502f6bc/internal/util/comparisons.js#L518-L533 function mapHasEqualEntry(set, map, key1, item1, opts, channel) { var i = getIterator(set); var result; var key2; while ((result = i.next()) && !result.done) { key2 = result.value; if ( // eslint-disable-next-line no-use-before-define internalDeepEqual(key1, key2, opts, channel) // eslint-disable-next-line no-use-before-define && internalDeepEqual(item1, $mapGet(map, key2), opts, channel) ) { $setDelete(set, key2); return true; } } return false; } function internalDeepEqual(actual, expected, options, channel) { var opts = options || {}; // 7.1. All identical values are equivalent, as determined by ===. if (opts.strict ? is(actual, expected) : actual === expected) { return true; } var actualBoxed = whichBoxedPrimitive(actual); var expectedBoxed = whichBoxedPrimitive(expected); if (actualBoxed !== expectedBoxed) { return false; } // 7.3. Other pairs that do not both pass typeof value == 'object', equivalence is determined by ==. if (!actual || !expected || (typeof actual !== 'object' && typeof expected !== 'object')) { return opts.strict ? is(actual, expected) : actual == expected; // eslint-disable-line eqeqeq } /* * 7.4. 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. */ // see https://github.com/nodejs/node/commit/d3aafd02efd3a403d646a3044adcf14e63a88d32 for memos/channel inspiration var hasActual = channel.has(actual); var hasExpected = channel.has(expected); var sentinel; if (hasActual && hasExpected) { if (channel.get(actual) === channel.get(expected)) { return true; } } else { sentinel = {}; } if (!hasActual) { channel.set(actual, sentinel); } if (!hasExpected) { channel.set(expected, sentinel); } // eslint-disable-next-line no-use-before-define return objEquiv(actual, expected, opts, channel); } function isBuffer(x) { if (!x || typeof x !== 'object' || typeof x.length !== 'number') { return false; } if (typeof x.copy !== 'function' || typeof x.slice !== 'function') { return false; } if (x.length > 0 && typeof x[0] !== 'number') { return false; } return !!(x.constructor && x.constructor.isBuffer && x.constructor.isBuffer(x)); } function setEquiv(a, b, opts, channel) { if ($setSize(a) !== $setSize(b)) { return false; } var iA = getIterator(a); var iB = getIterator(b); var resultA; var resultB; var set; while ((resultA = iA.next()) && !resultA.done) { if (resultA.value && typeof resultA.value === 'object') { if (!set) { set = new $Set(); } $setAdd(set, resultA.value); } else if (!$setHas(b, resultA.value)) { if (opts.strict) { return false; } if (!setMightHaveLoosePrim(a, b, resultA.value)) { return false; } if (!set) { set = new $Set(); } $setAdd(set, resultA.value); } } if (set) { while ((resultB = iB.next()) && !resultB.done) { // We have to check if a primitive value is already matching and only if it's not, go hunting for it. if (resultB.value && typeof resultB.value === 'object') { if (!setHasEqualElement(set, resultB.value, opts.strict, channel)) { return false; } } else if ( !opts.strict && !$setHas(a, resultB.value) && !setHasEqualElement(set, resultB.value, opts.strict, channel) ) { return false; } } return $setSize(set) === 0; } return true; } function mapEquiv(a, b, opts, channel) { if ($mapSize(a) !== $mapSize(b)) { return false; } var iA = getIterator(a); var iB = getIterator(b); var resultA; var resultB; var set; var key; var item1; var item2; while ((resultA = iA.next()) && !resultA.done) { key = resultA.value[0]; item1 = resultA.value[1]; if (key && typeof key === 'object') { if (!set) { set = new $Set(); } $setAdd(set, key); } else { item2 = $mapGet(b, key); if ((typeof item2 === 'undefined' && !$mapHas(b, key)) || !internalDeepEqual(item1, item2, opts, channel)) { if (opts.strict) { return false; } if (!mapMightHaveLoosePrim(a, b, key, item1, opts, channel)) { return false; } if (!set) { set = new $Set(); } $setAdd(set, key); } } } if (set) { while ((resultB = iB.next()) && !resultB.done) { key = resultB.value[0]; item2 = resultB.value[1]; if (key && typeof key === 'object') { if (!mapHasEqualEntry(set, a, key, item2, opts, channel)) { return false; } } else if ( !opts.strict && (!a.has(key) || !internalDeepEqual($mapGet(a, key), item2, opts, channel)) && !mapHasEqualEntry(set, a, key, item2, assign({}, opts, { strict: false }), channel) ) { return false; } } return $setSize(set) === 0; } return true; } function objEquiv(a, b, opts, channel) { /* eslint max-statements: [2, 100], max-lines-per-function: [2, 120], max-depth: [2, 5], max-lines: [2, 400] */ var i, key; if (typeof a !== typeof b) { return false; } if (a == null || b == null) { return false; } if ($objToString(a) !== $objToString(b)) { return false; } if (isArguments(a) !== isArguments(b)) { return false; } var aIsArray = isArray(a); var bIsArray = isArray(b); if (aIsArray !== bIsArray) { return false; } // TODO: replace when a cross-realm brand check is available var aIsError = a instanceof Error; var bIsError = b instanceof Error; if (aIsError !== bIsError) { return false; } if (aIsError || bIsError) { if (a.name !== b.name || a.message !== b.message) { return false; } } var aIsRegex = isRegex(a); var bIsRegex = isRegex(b); if (aIsRegex !== bIsRegex) { return false; } if ((aIsRegex || bIsRegex) && (a.source !== b.source || flags(a) !== flags(b))) { return false; } var aIsDate = isDate(a); var bIsDate = isDate(b); if (aIsDate !== bIsDate) { return false; } if (aIsDate || bIsDate) { // && would work too, because both are true or both false here if ($getTime(a) !== $getTime(b)) { return false; } } if (opts.strict && gPO && gPO(a) !== gPO(b)) { return false; } var aWhich = whichTypedArray(a); var bWhich = whichTypedArray(b); if ((aWhich || bWhich) && aWhich !== bWhich) { return false; } var aIsBuffer = isBuffer(a); var bIsBuffer = isBuffer(b); if (aIsBuffer !== bIsBuffer) { return false; } if (aIsBuffer || bIsBuffer) { // && would work too, because both are true or both false here if (a.length !== b.length) { return false; } for (i = 0; i < a.length; i++) { if (a[i] !== b[i]) { return false; } } return true; } var aIsArrayBuffer = isArrayBuffer(a); var bIsArrayBuffer = isArrayBuffer(b); if (aIsArrayBuffer !== bIsArrayBuffer) { return false; } if (aIsArrayBuffer || bIsArrayBuffer) { // && would work too, because both are true or both false here if (byteLength(a) !== byteLength(b)) { return false; } return typeof Uint8Array === 'function' && internalDeepEqual(new Uint8Array(a), new Uint8Array(b), opts, channel); } var aIsSAB = isSharedArrayBuffer(a); var bIsSAB = isSharedArrayBuffer(b); if (aIsSAB !== bIsSAB) { return false; } if (aIsSAB || bIsSAB) { // && would work too, because both are true or both false here if (sabByteLength(a) !== sabByteLength(b)) { return false; } return typeof Uint8Array === 'function' && internalDeepEqual(new Uint8Array(a), new Uint8Array(b), opts, channel); } if (typeof a !== typeof b) { return false; } var ka = objectKeys(a); var kb = objectKeys(b); // having the same number of owned properties (keys incorporates hasOwnProperty) if (ka.length !== kb.length) { return false; } // the same set of keys (although not necessarily the same order), ka.sort(); kb.sort(); // ~~~cheap key test for (i = ka.length - 1; i >= 0; i--) { if (ka[i] != kb[i]) { return false; } // eslint-disable-line eqeqeq } // equivalent values for every corresponding key, and ~~~possibly expensive deep test for (i = ka.length - 1; i >= 0; i--) { key = ka[i]; if (!internalDeepEqual(a[key], b[key], opts, channel)) { return false; } } var aCollection = whichCollection(a); var bCollection = whichCollection(b); if (aCollection !== bCollection) { return false; } if (aCollection === 'Set' || bCollection === 'Set') { // aCollection === bCollection return setEquiv(a, b, opts, channel); } if (aCollection === 'Map') { // aCollection === bCollection return mapEquiv(a, b, opts, channel); } return true; } module.exports = function deepEqual(a, b, opts) { return internalDeepEqual(a, b, opts, getSideChannel()); };