diff options
-rw-r--r-- | doc/api/assert.md | 2 | ||||
-rw-r--r-- | doc/api/util.md | 16 | ||||
-rw-r--r-- | lib/assert.js | 509 | ||||
-rw-r--r-- | lib/internal/util/comparisons.js | 516 | ||||
-rw-r--r-- | lib/util.js | 5 | ||||
-rw-r--r-- | node.gyp | 1 | ||||
-rw-r--r-- | test/parallel/test-util-isDeepStrictEqual.js | 483 |
7 files changed, 1028 insertions, 504 deletions
diff --git a/doc/api/assert.md b/doc/api/assert.md index 6179caa27f..4da3e15125 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -134,7 +134,7 @@ changes: * `expected` {any} * `message` {any} -Similar to `assert.deepEqual()` with the following exceptions: +Identical to [`assert.deepEqual()`][] with the following exceptions: 1. Primitive values besides `NaN` are compared using the [Strict Equality Comparison][] ( `===` ). Set and Map values, Map keys and `NaN` are compared diff --git a/doc/api/util.md b/doc/api/util.md index 6619ee2ad6..b13d02a8c0 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -455,6 +455,21 @@ util.inspect.defaultOptions.maxArrayLength = null; console.log(arr); // logs the full array ``` +## util.isDeepStrictEqual(val1, val2) +<!-- YAML +added: REPLACEME +--> + +* `val1` {any} +* `val2` {any} +* Returns: {string} + +Returns `true` if there is deep strict equality between `val` and `val2`. +Otherwise, returns `false`. + +See [`assert.deepStrictEqual()`][] for more information about deep strict +equality. + ## util.promisify(original) <!-- YAML added: v8.0.0 @@ -1187,6 +1202,7 @@ Deprecated predecessor of `console.log`. [`Buffer.isBuffer()`]: buffer.html#buffer_class_method_buffer_isbuffer_obj [`Error`]: errors.html#errors_class_error [`Object.assign()`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign +[`assert.deepStrictEqual()`]: assert.html#assert_assert_deepstrictequal_actual_expected_message [`console.error()`]: console.html#console_console_error_data_args [`console.log()`]: console.html#console_console_log_data_args [`util.inspect()`]: #util_util_inspect_object_options diff --git a/lib/assert.js b/lib/assert.js index a29408f47e..9c7260a1a8 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -20,12 +20,9 @@ 'use strict'; -const { compare } = process.binding('buffer'); -const { isSet, isMap, isDate, isRegExp } = process.binding('util'); -const { objectToString } = require('internal/util'); -const { isArrayBufferView } = require('internal/util/types'); +const { isDeepEqual, isDeepStrictEqual } = + require('internal/util/comparisons'); const errors = require('internal/errors'); -const { propertyIsEnumerable } = Object.prototype; // The assert module provides functions that throw // AssertionError's when particular conditions are not met. The @@ -100,522 +97,28 @@ assert.notEqual = function notEqual(actual, expected, message) { // The equivalence assertion tests a deep equality relation. assert.deepEqual = function deepEqual(actual, expected, message) { - if (!innerDeepEqual(actual, expected, false)) { + if (!isDeepEqual(actual, expected)) { innerFail(actual, expected, message, 'deepEqual', deepEqual); } }; /* eslint-enable */ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - if (!innerDeepEqual(actual, expected, true)) { + if (!isDeepStrictEqual(actual, expected)) { innerFail(actual, expected, message, 'deepStrictEqual', deepStrictEqual); } }; -// Check if they have the same source and flags -function areSimilarRegExps(a, b) { - return a.source === b.source && a.flags === b.flags; -} - -// For small buffers it's faster to compare the buffer in a loop. The c++ -// barrier including the Uint8Array operation takes the advantage of the faster -// binary compare otherwise. The break even point was at about 300 characters. -function areSimilarTypedArrays(a, b, max) { - const len = a.byteLength; - if (len !== b.byteLength) { - return false; - } - if (len < max) { - for (var offset = 0; offset < len; offset++) { - if (a[offset] !== b[offset]) { - return false; - } - } - return true; - } - return compare(new Uint8Array(a.buffer, a.byteOffset, len), - new Uint8Array(b.buffer, b.byteOffset, b.byteLength)) === 0; -} - -function isFloatTypedArrayTag(tag) { - return tag === '[object Float32Array]' || tag === '[object Float64Array]'; -} - -function isArguments(tag) { - return tag === '[object Arguments]'; -} - -function isObjectOrArrayTag(tag) { - return tag === '[object Array]' || tag === '[object Object]'; -} - -// Notes: Type tags are historical [[Class]] properties that can be set by -// FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS -// and retrieved using Object.prototype.toString.call(obj) in JS -// See https://tc39.github.io/ecma262/#sec-object.prototype.tostring -// for a list of tags pre-defined in the spec. -// There are some unspecified tags in the wild too (e.g. typed array tags). -// Since tags can be altered, they only serve fast failures -// -// Typed arrays and buffers are checked by comparing the content in their -// underlying ArrayBuffer. This optimization requires that it's -// reasonable to interpret their underlying memory in the same way, -// which is checked by comparing their type tags. -// (e.g. a Uint8Array and a Uint16Array with the same memory content -// could still be different because they will be interpreted differently). -// -// For strict comparison, objects should have -// a) The same built-in type tags -// b) The same prototypes. -function strictDeepEqual(actual, expected, memos) { - if (typeof actual !== 'object') { - return typeof actual === 'number' && Number.isNaN(actual) && - Number.isNaN(expected); - } - if (typeof expected !== 'object' || actual === null || expected === null) { - return false; - } - const actualTag = objectToString(actual); - const expectedTag = objectToString(expected); - - if (actualTag !== expectedTag) { - return false; - } - if (Object.getPrototypeOf(actual) !== Object.getPrototypeOf(expected)) { - return false; - } - if (actualTag === '[object Array]') { - // Check for sparse arrays and general fast path - if (actual.length !== expected.length) - return false; - // Skip testing the part below and continue with the keyCheck. - return keyCheck(actual, expected, true, memos); - } - if (actualTag === '[object Object]') { - // Skip testing the part below and continue with the keyCheck. - return keyCheck(actual, expected, true, memos); - } - if (isDate(actual)) { - if (actual.getTime() !== expected.getTime()) { - return false; - } - } else if (isRegExp(actual)) { - if (!areSimilarRegExps(actual, expected)) { - return false; - } - } else if (actualTag === '[object Error]') { - // Do not compare the stack as it might differ even though the error itself - // is otherwise identical. The non-enumerable name should be identical as - // the prototype is also identical. Otherwise this is caught later on. - if (actual.message !== expected.message) { - return false; - } - } else if (isArrayBufferView(actual)) { - if (!areSimilarTypedArrays(actual, expected, - isFloatTypedArrayTag(actualTag) ? 0 : 300)) { - return false; - } - // Buffer.compare returns true, so actual.length === expected.length - // if they both only contain numeric keys, we don't need to exam further - return keyCheck(actual, expected, true, memos, actual.length, - expected.length); - } else if (typeof actual.valueOf === 'function') { - const actualValue = actual.valueOf(); - // Note: Boxed string keys are going to be compared again by Object.keys - if (actualValue !== actual) { - if (!innerDeepEqual(actualValue, expected.valueOf(), true)) - return false; - // Fast path for boxed primitives - var lengthActual = 0; - var lengthExpected = 0; - if (typeof actualValue === 'string') { - lengthActual = actual.length; - lengthExpected = expected.length; - } - return keyCheck(actual, expected, true, memos, lengthActual, - lengthExpected); - } - } - return keyCheck(actual, expected, true, memos); -} - -function looseDeepEqual(actual, expected, memos) { - if (actual === null || typeof actual !== 'object') { - if (expected === null || typeof expected !== 'object') { - // eslint-disable-next-line eqeqeq - return actual == expected; - } - return false; - } - if (expected === null || typeof expected !== 'object') { - return false; - } - if (isDate(actual) && isDate(expected)) { - return actual.getTime() === expected.getTime(); - } - if (isRegExp(actual) && isRegExp(expected)) { - return areSimilarRegExps(actual, expected); - } - if (actual instanceof Error && expected instanceof Error) { - if (actual.message !== expected.message || actual.name !== expected.name) - return false; - } - const actualTag = objectToString(actual); - const expectedTag = objectToString(expected); - if (actualTag === expectedTag) { - if (!isObjectOrArrayTag(actualTag) && isArrayBufferView(actual)) { - return areSimilarTypedArrays(actual, expected, - isFloatTypedArrayTag(actualTag) ? - Infinity : 300); - } - // Ensure reflexivity of deepEqual with `arguments` objects. - // See https://github.com/nodejs/node-v0.x-archive/pull/7178 - } else if (isArguments(actualTag) || isArguments(expectedTag)) { - return false; - } - return keyCheck(actual, expected, false, memos); -} - -function keyCheck(actual, expected, strict, memos, lengthA, lengthB) { - // For all remaining Object pairs, including Array, objects and Maps, - // equivalence is determined by having: - // a) The same number of owned enumerable properties - // b) The same set of keys/indexes (although not necessarily the same order) - // c) Equivalent values for every corresponding key/index - // d) For Sets and Maps, equal contents - // Note: this accounts for both named and indexed properties on Arrays. - var aKeys = Object.keys(actual); - var bKeys = Object.keys(expected); - var i; - - // The pair must have the same number of owned properties. - if (aKeys.length !== bKeys.length) - return false; - - if (strict) { - var symbolKeysA = Object.getOwnPropertySymbols(actual); - var symbolKeysB = Object.getOwnPropertySymbols(expected); - if (symbolKeysA.length !== 0) { - symbolKeysA = symbolKeysA.filter((k) => - propertyIsEnumerable.call(actual, k)); - symbolKeysB = symbolKeysB.filter((k) => - propertyIsEnumerable.call(expected, k)); - if (symbolKeysA.length !== symbolKeysB.length) - return false; - } else if (symbolKeysB.length !== 0 && symbolKeysB.filter((k) => - propertyIsEnumerable.call(expected, k)).length !== 0) { - return false; - } - if (lengthA !== undefined) { - if (aKeys.length !== lengthA || bKeys.length !== lengthB) - return false; - if (symbolKeysA.length === 0) - return true; - aKeys = []; - bKeys = []; - } - if (symbolKeysA.length !== 0) { - aKeys.push(...symbolKeysA); - bKeys.push(...symbolKeysB); - } - } - - // Cheap key test: - const keys = {}; - for (i = 0; i < aKeys.length; i++) { - keys[aKeys[i]] = true; - } - for (i = 0; i < aKeys.length; i++) { - if (keys[bKeys[i]] === undefined) - return false; - } - - // Use memos to handle cycles. - if (memos === undefined) { - memos = { - actual: new Map(), - expected: new Map(), - position: 0 - }; - } else { - // We prevent up to two map.has(x) calls by directly retrieving the value - // and checking for undefined. The map can only contain numbers, so it is - // safe to check for undefined only. - const expectedMemoA = memos.actual.get(actual); - if (expectedMemoA !== undefined) { - const expectedMemoB = memos.expected.get(expected); - if (expectedMemoB !== undefined) { - return expectedMemoA === expectedMemoB; - } - } - memos.position++; - } - - memos.actual.set(actual, memos.position); - memos.expected.set(expected, memos.position); - - const areEq = objEquiv(actual, expected, strict, aKeys, memos); - - memos.actual.delete(actual); - memos.expected.delete(expected); - - return areEq; -} - -function innerDeepEqual(actual, expected, strict, memos) { - // All identical values are equivalent, as determined by ===. - if (actual === expected) { - if (actual !== 0) - return true; - return strict ? Object.is(actual, expected) : true; - } - - // Check more closely if actual and expected are equal. - if (strict === true) - return strictDeepEqual(actual, expected, memos); - - return looseDeepEqual(actual, expected, memos); -} - -function setHasEqualElement(set, val1, strict, memo) { - // Go looking. - for (const val2 of set) { - if (innerDeepEqual(val1, val2, strict, memo)) { - // Remove the matching element to make sure we do not check that again. - set.delete(val2); - return true; - } - } - - return false; -} - -// Note: we actually run this multiple times for each loose key! -// This is done to prevent slowing down the average case. -function setHasLoosePrim(a, b, val) { - const altValues = findLooseMatchingPrimitives(val); - if (altValues === undefined) - return false; - - var matches = 1; - for (var i = 0; i < altValues.length; i++) { - if (b.has(altValues[i])) { - matches--; - } - if (a.has(altValues[i])) { - matches++; - } - } - return matches === 0; -} - -function setEquiv(a, b, strict, memo) { - // This code currently returns false for this pair of sets: - // assert.deepEqual(new Set(['1', 1]), new Set([1])) - // - // In theory, all the items in the first set have a corresponding == value in - // the second set, but the sets have different sizes. Its a silly case, - // and more evidence that deepStrictEqual should always be preferred over - // deepEqual. - if (a.size !== b.size) - return false; - - // This is a lazily initiated Set of entries which have to be compared - // pairwise. - var set = null; - for (const val of a) { - // Note: Checking for the objects first improves the performance for object - // heavy sets but it is a minor slow down for primitives. As they are fast - // to check this improves the worst case scenario instead. - if (typeof val === 'object' && val !== null) { - if (set === null) { - set = new Set(); - } - // If the specified value doesn't exist in the second set its an not null - // object (or non strict only: a not matching primitive) we'll need to go - // hunting for something thats deep-(strict-)equal to it. To make this - // O(n log n) complexity we have to copy these values in a new set first. - set.add(val); - } else if (!b.has(val) && (strict || !setHasLoosePrim(a, b, val))) { - return false; - } - } - - if (set !== null) { - for (const val of b) { - // We have to check if a primitive value is already - // matching and only if it's not, go hunting for it. - if (typeof val === 'object' && val !== null) { - if (!setHasEqualElement(set, val, strict, memo)) - return false; - } else if (!a.has(val) && (strict || !setHasLoosePrim(b, a, val))) { - return false; - } - } - } - - return true; -} - -function findLooseMatchingPrimitives(prim) { - var values, number; - switch (typeof prim) { - case 'number': - values = ['' + prim]; - if (prim === 1 || prim === 0) - values.push(Boolean(prim)); - return values; - case 'string': - number = +prim; - if ('' + number === prim) { - values = [number]; - if (number === 1 || number === 0) - values.push(Boolean(number)); - } - return values; - case 'undefined': - return [null]; - case 'object': // Only pass in null as object! - return [undefined]; - case 'boolean': - number = +prim; - return [number, '' + number]; - } -} - -// This is a ugly but relatively fast way to determine if a loose equal entry -// actually has a correspondent matching entry. Otherwise checking for such -// values would be way more expensive (O(n^2)). -// Note: we actually run this multiple times for each loose key! -// This is done to prevent slowing down the average case. -function mapHasLoosePrim(a, b, key1, memo, item1, item2) { - const altKeys = findLooseMatchingPrimitives(key1); - if (altKeys === undefined) - return false; - - const setA = new Set(); - const setB = new Set(); - - var keyCount = 1; - - setA.add(item1); - if (b.has(key1)) { - keyCount--; - setB.add(item2); - } - - for (var i = 0; i < altKeys.length; i++) { - const key2 = altKeys[i]; - if (a.has(key2)) { - keyCount++; - setA.add(a.get(key2)); - } - if (b.has(key2)) { - keyCount--; - setB.add(b.get(key2)); - } - } - if (keyCount !== 0 || setA.size !== setB.size) - return false; - - for (const val of setA) { - if (typeof val === 'object' && val !== null) { - if (!setHasEqualElement(setB, val, false, memo)) - return false; - } else if (!setB.has(val) && !setHasLoosePrim(setA, setB, val)) { - return false; - } - } - return true; -} - -function mapHasEqualEntry(set, map, key1, item1, strict, memo) { - // To be able to handle cases like: - // Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']]) - // ... we need to consider *all* matching keys, not just the first we find. - for (const key2 of set) { - if (innerDeepEqual(key1, key2, strict, memo) && - innerDeepEqual(item1, map.get(key2), strict, memo)) { - set.delete(key2); - return true; - } - } - - return false; -} - -function mapEquiv(a, b, strict, memo) { - if (a.size !== b.size) - return false; - - var set = null; - - for (const [key, item1] of a) { - if (typeof key === 'object' && key !== null) { - if (set === null) { - set = new Set(); - } - set.add(key); - } else { - // By directly retrieving the value we prevent another b.has(key) check in - // almost all possible cases. - const item2 = b.get(key); - if ((item2 === undefined && !b.has(key) || - !innerDeepEqual(item1, item2, strict, memo)) && - (strict || !mapHasLoosePrim(a, b, key, memo, item1, item2))) { - return false; - } - } - } - - if (set !== null) { - for (const [key, item] of b) { - if (typeof key === 'object' && key !== null) { - if (!mapHasEqualEntry(set, a, key, item, strict, memo)) - return false; - } else if (!a.has(key) && - (strict || !mapHasLoosePrim(b, a, key, memo, item))) { - return false; - } - } - } - - return true; -} - -function objEquiv(a, b, strict, keys, memos) { - // Sets and maps don't have their entries accessible via normal object - // properties. - if (isSet(a)) { - if (!isSet(b) || !setEquiv(a, b, strict, memos)) - return false; - } else if (isMap(a)) { - if (!isMap(b) || !mapEquiv(a, b, strict, memos)) - return false; - } else if (isSet(b) || isMap(b)) { - return false; - } - - // The pair must have equivalent values for every corresponding key. - // Possibly expensive deep test: - for (var i = 0; i < keys.length; i++) { - const key = keys[i]; - if (!innerDeepEqual(a[key], b[key], strict, memos)) - return false; - } - return true; -} - // The non-equivalence assertion tests for any deep inequality. assert.notDeepEqual = function notDeepEqual(actual, expected, message) { - if (innerDeepEqual(actual, expected, false)) { + if (isDeepEqual(actual, expected)) { innerFail(actual, expected, message, 'notDeepEqual', notDeepEqual); } }; assert.notDeepStrictEqual = notDeepStrictEqual; function notDeepStrictEqual(actual, expected, message) { - if (innerDeepEqual(actual, expected, true)) { + if (isDeepStrictEqual(actual, expected)) { innerFail(actual, expected, message, 'notDeepStrictEqual', notDeepStrictEqual); } diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js new file mode 100644 index 0000000000..1145bc7d99 --- /dev/null +++ b/lib/internal/util/comparisons.js @@ -0,0 +1,516 @@ +'use strict'; + +const { compare } = process.binding('buffer'); +const { isArrayBufferView } = require('internal/util/types'); +const { isDate, isMap, isRegExp, isSet } = process.binding('util'); + +function objectToString(o) { + return Object.prototype.toString.call(o); +} + +// Check if they have the same source and flags +function areSimilarRegExps(a, b) { + return a.source === b.source && a.flags === b.flags; +} + +// For small buffers it's faster to compare the buffer in a loop. The c++ +// barrier including the Uint8Array operation takes the advantage of the faster +// binary compare otherwise. The break even point was at about 300 characters. +function areSimilarTypedArrays(a, b, max) { + const len = a.byteLength; + if (len !== b.byteLength) { + return false; + } + if (len < max) { + for (var offset = 0; offset < len; offset++) { + if (a[offset] !== b[offset]) { + return false; + } + } + return true; + } + return compare(new Uint8Array(a.buffer, a.byteOffset, len), + new Uint8Array(b.buffer, b.byteOffset, b.byteLength)) === 0; +} + +function isFloatTypedArrayTag(tag) { + return tag === '[object Float32Array]' || tag === '[object Float64Array]'; +} + +function isArguments(tag) { + return tag === '[object Arguments]'; +} + +function isObjectOrArrayTag(tag) { + return tag === '[object Array]' || tag === '[object Object]'; +} + +// Notes: Type tags are historical [[Class]] properties that can be set by +// FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS +// and retrieved using Object.prototype.toString.call(obj) in JS +// See https://tc39.github.io/ecma262/#sec-object.prototype.tostring +// for a list of tags pre-defined in the spec. +// There are some unspecified tags in the wild too (e.g. typed array tags). +// Since tags can be altered, they only serve fast failures +// +// Typed arrays and buffers are checked by comparing the content in their +// underlying ArrayBuffer. This optimization requires that it's +// reasonable to interpret their underlying memory in the same way, +// which is checked by comparing their type tags. +// (e.g. a Uint8Array and a Uint16Array with the same memory content +// could still be different because they will be interpreted differently). +// +// For strict comparison, objects should have +// a) The same built-in type tags +// b) The same prototypes. +function strictDeepEqual(val1, val2, memos) { + if (typeof val1 !== 'object') { + return typeof val1 === 'number' && Number.isNaN(val1) && + Number.isNaN(val2); + } + if (typeof val2 !== 'object' || val1 === null || val2 === null) { + return false; + } + const val1Tag = objectToString(val1); + const val2Tag = objectToString(val2); + + if (val1Tag !== val2Tag) { + return false; + } + if (Object.getPrototypeOf(val1) !== Object.getPrototypeOf(val2)) { + return false; + } + if (val1Tag === '[object Array]') { + // Check for sparse arrays and general fast path + if (val1.length !== val2.length) + return false; + // Skip testing the part below and continue with the keyCheck. + return keyCheck(val1, val2, true, memos); + } + if (val1Tag === '[object Object]') { + // Skip testing the part below and continue with the keyCheck. + return keyCheck(val1, val2, true, memos); + } + if (isDate(val1)) { + if (val1.getTime() !== val2.getTime()) { + return false; + } + } else if (isRegExp(val1)) { + if (!areSimilarRegExps(val1, val2)) { + return false; + } + } else if (val1Tag === '[object Error]') { + // Do not compare the stack as it might differ even though the error itself + // is otherwise identical. The non-enumerable name should be identical as + // the prototype is also identical. Otherwise this is caught later on. + if (val1.message !== val2.message) { + return false; + } + } else if (isArrayBufferView(val1)) { + if (!areSimilarTypedArrays(val1, val2, + isFloatTypedArrayTag(val1Tag) ? 0 : 300)) { + return false; + } + // Buffer.compare returns true, so val1.length === val2.length + // if they both only contain numeric keys, we don't need to exam further + return keyCheck(val1, val2, true, memos, val1.length, + val2.length); + } else if (typeof val1.valueOf === 'function') { + const val1Value = val1.valueOf(); + // Note: Boxed string keys are going to be compared again by Object.keys + if (val1Value !== val1) { + if (!innerDeepEqual(val1Value, val2.valueOf(), true)) + return false; + // Fast path for boxed primitives + var lengthval1 = 0; + var lengthval2 = 0; + if (typeof val1Value === 'string') { + lengthval1 = val1.length; + lengthval2 = val2.length; + } + return keyCheck(val1, val2, true, memos, lengthval1, + lengthval2); + } + } + return keyCheck(val1, val2, true, memos); +} + +function looseDeepEqual(val1, val2, memos) { + if (val1 === null || typeof val1 !== 'object') { + if (val2 === null || typeof val2 !== 'object') { + // eslint-disable-next-line eqeqeq + return val1 == val2; + } + return false; + } + if (val2 === null || typeof val2 !== 'object') { + return false; + } + if (isDate(val1) && isDate(val2)) { + return val1.getTime() === val2.getTime(); + } + if (isRegExp(val1) && isRegExp(val2)) { + return areSimilarRegExps(val1, val2); + } + if (val1 instanceof Error && val2 instanceof Error) { + if (val1.message !== val2.message || val1.name !== val2.name) + return false; + } + const val1Tag = objectToString(val1); + const val2Tag = objectToString(val2); + if (val1Tag === val2Tag) { + if (!isObjectOrArrayTag(val1Tag) && isArrayBufferView(val1)) { + return areSimilarTypedArrays(val1, val2, + isFloatTypedArrayTag(val1Tag) ? + Infinity : 300); + } + // Ensure reflexivity of deepEqual with `arguments` objects. + // See https://github.com/nodejs/node-v0.x-archive/pull/7178 + } else if (isArguments(val1Tag) || isArguments(val2Tag)) { + return false; + } + return keyCheck(val1, val2, false, memos); +} + +function keyCheck(val1, val2, strict, memos, lengthA, lengthB) { + // For all remaining Object pairs, including Array, objects and Maps, + // equivalence is determined by having: + // a) The same number of owned enumerable properties + // b) The same set of keys/indexes (although not necessarily the same order) + // c) Equivalent values for every corresponding key/index + // d) For Sets and Maps, equal contents + // Note: this accounts for both named and indexed properties on Arrays. + var aKeys = Object.keys(val1); + var bKeys = Object.keys(val2); + var i; + + // The pair must have the same number of owned properties. + if (aKeys.length !== bKeys.length) + return false; + + if (strict) { + var symbolKeysA = Object.getOwnPropertySymbols(val1); + var symbolKeysB = Object.getOwnPropertySymbols(val2); + if (symbolKeysA.length !== 0) { + symbolKeysA = symbolKeysA.filter((k) => + propertyIsEnumerable.call(val1, k)); + symbolKeysB = symbolKeysB.filter((k) => + propertyIsEnumerable.call(val2, k)); + if (symbolKeysA.length !== symbolKeysB.length) + return false; + } else if (symbolKeysB.length !== 0 && symbolKeysB.filter((k) => + propertyIsEnumerable.call(val2, k)).length !== 0) { + return false; + } + if (lengthA !== undefined) { + if (aKeys.length !== lengthA || bKeys.length !== lengthB) + return false; + if (symbolKeysA.length === 0) + return true; + aKeys = []; + bKeys = []; + } + if (symbolKeysA.length !== 0) { + aKeys.push(...symbolKeysA); + bKeys.push(...symbolKeysB); + } + } + + // Cheap key test: + const keys = {}; + for (i = 0; i < aKeys.length; i++) { + keys[aKeys[i]] = true; + } + for (i = 0; i < aKeys.length; i++) { + if (keys[bKeys[i]] === undefined) + return false; + } + + // Use memos to handle cycles. + if (memos === undefined) { + memos = { + val1: new Map(), + val2: new Map(), + position: 0 + }; + } else { + // We prevent up to two map.has(x) calls by directly retrieving the value + // and checking for undefined. The map can only contain numbers, so it is + // safe to check for undefined only. + const val2MemoA = memos.val1.get(val1); + if (val2MemoA !== undefined) { + const val2MemoB = memos.val2.get(val2); + if (val2MemoB !== undefined) { + return val2MemoA === val2MemoB; + } + } + memos.position++; + } + + memos.val1.set(val1, memos.position); + memos.val2.set(val2, memos.position); + + const areEq = objEquiv(val1, val2, strict, aKeys, memos); + + memos.val1.delete(val1); + memos.val2.delete(val2); + + return areEq; +} + +function innerDeepEqual(val1, val2, strict, memos) { + // All identical values are equivalent, as determined by ===. + if (val1 === val2) { + if (val1 !== 0) + return true; + return strict ? Object.is(val1, val2) : true; + } + + // Check more closely if val1 and val2 are equal. + if (strict === true) + return strictDeepEqual(val1, val2, memos); + + return looseDeepEqual(val1, val2, memos); +} + +function setHasEqualElement(set, val1, strict, memo) { + // Go looking. + for (const val2 of set) { + if (innerDeepEqual(val1, val2, strict, memo)) { + // Remove the matching element to make sure we do not check that again. + set.delete(val2); + return true; + } + } + + return false; +} + +// Note: we val1ly run this multiple times for each loose key! +// This is done to prevent slowing down the average case. +function setHasLoosePrim(a, b, val) { + const altValues = findLooseMatchingPrimitives(val); + if (altValues === undefined) + return false; + + var matches = 1; + for (var i = 0; i < altValues.length; i++) { + if (b.has(altValues[i])) { + matches--; + } + if (a.has(altValues[i])) { + matches++; + } + } + return matches === 0; +} + +function setEquiv(a, b, strict, memo) { + // This code currently returns false for this pair of sets: + // assert.deepEqual(new Set(['1', 1]), new Set([1])) + // + // In theory, all the items in the first set have a corresponding == value in + // the second set, but the sets have different sizes. Its a silly case, + // and more evidence that deepStrictEqual should always be preferred over + // deepEqual. + if (a.size !== b.size) + return false; + + // This is a lazily initiated Set of entries which have to be compared + // pairwise. + var set = null; + for (const val of a) { + // Note: Checking for the objects first improves the performance for object + // heavy sets but it is a minor slow down for primitives. As they are fast + // to check this improves the worst case scenario instead. + if (typeof val === 'object' && val !== null) { + if (set === null) { + set = new Set(); + } + // If the specified value doesn't exist in the second set its an not null + // object (or non strict only: a not matching primitive) we'll need to go + // hunting for something thats deep-(strict-)equal to it. To make this + // O(n log n) complexity we have to copy these values in a new set first. + set.add(val); + } else if (!b.has(val) && (strict || !setHasLoosePrim(a, b, val))) { + return false; + } + } + + if (set !== null) { + for (const val of b) { + // We have to check if a primitive value is already + // matching and only if it's not, go hunting for it. + if (typeof val === 'object' && val !== null) { + if (!setHasEqualElement(set, val, strict, memo)) + return false; + } else if (!a.has(val) && (strict || !setHasLoosePrim(b, a, val))) { + return false; + } + } + } + + return true; +} + +function findLooseMatchingPrimitives(prim) { + var values, number; + switch (typeof prim) { + case 'number': + values = ['' + prim]; + if (prim === 1 || prim === 0) + values.push(Boolean(prim)); + return values; + case 'string': + number = +prim; + if ('' + number === prim) { + values = [number]; + if (number === 1 || number === 0) + values.push(Boolean(number)); + } + return values; + case 'undefined': + return [null]; + case 'object': // Only pass in null as object! + return [undefined]; + case 'boolean': + number = +prim; + return [number, '' + number]; + } +} + +// This is a ugly but relatively fast way to determine if a loose equal entry +// val1ly has a correspondent matching entry. Otherwise checking for such +// values would be way more expensive (O(n^2)). +// Note: we val1ly run this multiple times for each loose key! +// This is done to prevent slowing down the average case. +function mapHasLoosePrim(a, b, key1, memo, item1, item2) { + const altKeys = findLooseMatchingPrimitives(key1); + if (altKeys === undefined) + return false; + + const setA = new Set(); + const setB = new Set(); + + var keyCount = 1; + + setA.add(item1); + if (b.has(key1)) { + keyCount--; + setB.add(item2); + } + + for (var i = 0; i < altKeys.length; i++) { + const key2 = altKeys[i]; + if (a.has(key2)) { + keyCount++; + setA.add(a.get(key2)); + } + if (b.has(key2)) { + keyCount--; + setB.add(b.get(key2)); + } + } + if (keyCount !== 0 || setA.size !== setB.size) + return false; + + for (const val of setA) { + if (typeof val === 'object' && val !== null) { + if (!setHasEqualElement(setB, val, false, memo)) + return false; + } else if (!setB.has(val) && !setHasLoosePrim(setA, setB, val)) { + return false; + } + } + return true; +} + +function mapHasEqualEntry(set, map, key1, item1, strict, memo) { + // To be able to handle cases like: + // Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']]) + // ... we need to consider *all* matching keys, not just the first we find. + for (const key2 of set) { + if (innerDeepEqual(key1, key2, strict, memo) && + innerDeepEqual(item1, map.get(key2), strict, memo)) { + set.delete(key2); + return true; + } + } + + return false; +} + +function mapEquiv(a, b, strict, memo) { + if (a.size !== b.size) + return false; + + var set = null; + + for (const [key, item1] of a) { + if (typeof key === 'object' && key !== null) { + if (set === null) { + set = new Set(); + } + set.add(key); + } else { + // By directly retrieving the value we prevent another b.has(key) check in + // almost all possible cases. + const item2 = b.get(key); + if ((item2 === undefined && !b.has(key) || + !innerDeepEqual(item1, item2, strict, memo)) && + (strict || !mapHasLoosePrim(a, b, key, memo, item1, item2))) { + return false; + } + } + } + + if (set !== null) { + for (const [key, item] of b) { + if (typeof key === 'object' && key !== null) { + if (!mapHasEqualEntry(set, a, key, item, strict, memo)) + return false; + } else if (!a.has(key) && + (strict || !mapHasLoosePrim(b, a, key, memo, item))) { + return false; + } + } + } + + return true; +} + +function objEquiv(a, b, strict, keys, memos) { + // Sets and maps don't have their entries accessible via normal object + // properties. + if (isSet(a)) { + if (!isSet(b) || !setEquiv(a, b, strict, memos)) + return false; + } else if (isMap(a)) { + if (!isMap(b) || !mapEquiv(a, b, strict, memos)) + return false; + } else if (isSet(b) || isMap(b)) { + return false; + } + + // The pair must have equivalent values for every corresponding key. + // Possibly expensive deep test: + for (var i = 0; i < keys.length; i++) { + const key = keys[i]; + if (!innerDeepEqual(a[key], b[key], strict, memos)) + return false; + } + return true; +} + +function isDeepEqual(val1, val2) { + return innerDeepEqual(val1, val2, false); +} + +function isDeepStrictEqual(val1, val2) { + return innerDeepEqual(val1, val2, true); +} + +module.exports = { + isDeepEqual, + isDeepStrictEqual +}; diff --git a/lib/util.js b/lib/util.js index b97b1ede0c..00da489b88 100644 --- a/lib/util.js +++ b/lib/util.js @@ -49,6 +49,10 @@ const { } = require('internal/util/types'); const { + isDeepStrictEqual +} = require('internal/util/comparisons'); + +const { customInspectSymbol, deprecate, getConstructorOf, @@ -1118,6 +1122,7 @@ module.exports = exports = { isArray: Array.isArray, isBoolean, isBuffer, + isDeepStrictEqual, isNull, isNullOrUndefined, isNumber, @@ -123,6 +123,7 @@ 'lib/internal/tls.js', 'lib/internal/url.js', 'lib/internal/util.js', + 'lib/internal/util/comparisons.js', 'lib/internal/util/types.js', 'lib/internal/http2/core.js', 'lib/internal/http2/compat.js', diff --git a/test/parallel/test-util-isDeepStrictEqual.js b/test/parallel/test-util-isDeepStrictEqual.js new file mode 100644 index 0000000000..356a9a7132 --- /dev/null +++ b/test/parallel/test-util-isDeepStrictEqual.js @@ -0,0 +1,483 @@ +'use strict'; + +// Confirm functionality of `util.isDeepStrictEqual()`. + +require('../common'); + +const assert = require('assert'); +const util = require('util'); + +class MyDate extends Date { + constructor(...args) { + super(...args); + this[0] = '1'; + } +} + +class MyRegExp extends RegExp { + constructor(...args) { + super(...args); + this[0] = '1'; + } +} + +{ + const arr = new Uint8Array([120, 121, 122, 10]); + const buf = Buffer.from(arr); + // They have different [[Prototype]] + assert.strictEqual(util.isDeepStrictEqual(arr, buf), false); + + const buf2 = Buffer.from(arr); + buf2.prop = 1; + + assert.strictEqual(util.isDeepStrictEqual(buf2, buf), false); + + const arr2 = new Uint8Array([120, 121, 122, 10]); + arr2.prop = 5; + assert.strictEqual(util.isDeepStrictEqual(arr, arr2), false); +} + +{ + const date = new Date('2016'); + + const date2 = new MyDate('2016'); + + // deepStrictEqual checks own properties + assert.strictEqual(util.isDeepStrictEqual(date, date2), false); + assert.strictEqual(util.isDeepStrictEqual(date2, date), false); +} + +{ + const re1 = new RegExp('test'); + const re2 = new MyRegExp('test'); + + // deepStrictEqual checks all properties + assert.strictEqual(util.isDeepStrictEqual(re1, re2), false); +} + +{ + // For these cases, deepStrictEqual should throw. + const similar = new Set([ + { 0: '1' }, // Object + { 0: 1 }, // Object + new String('1'), // Object + ['1'], // Array + [1], // Array + new MyDate('2016'), // Date with this[0] = '1' + new MyRegExp('test'), // RegExp with this[0] = '1' + new Int8Array([1]), // Int8Array + new Uint8Array([1]), // Uint8Array + new Int16Array([1]), // Int16Array + new Uint16Array([1]), // Uint16Array + new Int32Array([1]), // Int32Array + new Uint32Array([1]), // Uint32Array + Buffer.from([1]), // Buffer + ]); + + for (const a of similar) { + for (const b of similar) { + if (a !== b) { + assert.strictEqual(util.isDeepStrictEqual(a, b), false); + } + } + } +} + +function utilIsDeepStrict(a, b) { + assert.strictEqual(util.isDeepStrictEqual(a, b), true); + assert.strictEqual(util.isDeepStrictEqual(b, a), true); +} + +function notUtilIsDeepStrict(a, b) { + assert.strictEqual(util.isDeepStrictEqual(a, b), false); + assert.strictEqual(util.isDeepStrictEqual(b, a), false); +} + +// es6 Maps and Sets +utilIsDeepStrict(new Set(), new Set()); +utilIsDeepStrict(new Map(), new Map()); + +utilIsDeepStrict(new Set([1, 2, 3]), new Set([1, 2, 3])); +notUtilIsDeepStrict(new Set([1, 2, 3]), new Set([1, 2, 3, 4])); +notUtilIsDeepStrict(new Set([1, 2, 3, 4]), new Set([1, 2, 3])); +utilIsDeepStrict(new Set(['1', '2', '3']), new Set(['1', '2', '3'])); +utilIsDeepStrict(new Set([[1, 2], [3, 4]]), new Set([[3, 4], [1, 2]])); + +{ + const a = [ 1, 2 ]; + const b = [ 3, 4 ]; + const c = [ 1, 2 ]; + const d = [ 3, 4 ]; + + utilIsDeepStrict( + { a: a, b: b, s: new Set([a, b]) }, + { a: c, b: d, s: new Set([d, c]) } + ); +} + +utilIsDeepStrict(new Map([[1, 1], [2, 2]]), new Map([[1, 1], [2, 2]])); +utilIsDeepStrict(new Map([[1, 1], [2, 2]]), new Map([[2, 2], [1, 1]])); +notUtilIsDeepStrict(new Map([[1, 1], [2, 2]]), new Map([[1, 2], [2, 1]])); +notUtilIsDeepStrict( + new Map([[[1], 1], [{}, 2]]), + new Map([[[1], 2], [{}, 1]]) +); + +notUtilIsDeepStrict(new Set([1]), [1]); +notUtilIsDeepStrict(new Set(), []); +notUtilIsDeepStrict(new Set(), {}); + +notUtilIsDeepStrict(new Map([['a', 1]]), { a: 1 }); +notUtilIsDeepStrict(new Map(), []); +notUtilIsDeepStrict(new Map(), {}); + +notUtilIsDeepStrict(new Set(['1']), new Set([1])); + +notUtilIsDeepStrict(new Map([['1', 'a']]), new Map([[1, 'a']])); +notUtilIsDeepStrict(new Map([['a', '1']]), new Map([['a', 1]])); +notUtilIsDeepStrict(new Map([['a', '1']]), new Map([['a', 2]])); + +utilIsDeepStrict(new Set([{}]), new Set([{}])); + +// Ref: https://github.com/nodejs/node/issues/13347 +notUtilIsDeepStrict( + new Set([{ a: 1 }, { a: 1 }]), + new Set([{ a: 1 }, { a: 2 }]) +); +notUtilIsDeepStrict( + new Set([{ a: 1 }, { a: 1 }, { a: 2 }]), + new Set([{ a: 1 }, { a: 2 }, { a: 2 }]) +); +notUtilIsDeepStrict( + new Map([[{ x: 1 }, 5], [{ x: 1 }, 5]]), + new Map([[{ x: 1 }, 5], [{ x: 2 }, 5]]) +); + +notUtilIsDeepStrict(new Set([3, '3']), new Set([3, 4])); +notUtilIsDeepStrict(new Map([[3, 0], ['3', 0]]), new Map([[3, 0], [4, 0]])); + +notUtilIsDeepStrict( + new Set([{ a: 1 }, { a: 1 }, { a: 2 }]), + new Set([{ a: 1 }, { a: 2 }, { a: 2 }]) +); + +// Mixed primitive and object keys +utilIsDeepStrict( + new Map([[1, 'a'], [{}, 'a']]), + new Map([[1, 'a'], [{}, 'a']]) +); +utilIsDeepStrict( + new Set([1, 'a', [{}, 'a']]), + new Set([1, 'a', [{}, 'a']]) +); + +// This is an awful case, where a map contains multiple equivalent keys: +notUtilIsDeepStrict( + new Map([[1, 'a'], ['1', 'b']]), + new Map([['1', 'a'], [true, 'b']]) +); +notUtilIsDeepStrict( + new Set(['a']), + new Set(['b']) +); +utilIsDeepStrict( + new Map([[{}, 'a'], [{}, 'b']]), + new Map([[{}, 'b'], [{}, 'a']]) +); +notUtilIsDeepStrict( + new Map([[true, 'a'], ['1', 'b'], [1, 'a']]), + new Map([['1', 'a'], [1, 'b'], [true, 'a']]) +); +notUtilIsDeepStrict( + new Map([[true, 'a'], ['1', 'b'], [1, 'c']]), + new Map([['1', 'a'], [1, 'b'], [true, 'a']]) +); + +// Similar object keys +notUtilIsDeepStrict( + new Set([{}, {}]), + new Set([{}, 1]) +); +notUtilIsDeepStrict( + new Set([[{}, 1], [{}, 1]]), + new Set([[{}, 1], [1, 1]]) +); +notUtilIsDeepStrict( + new Map([[{}, 1], [{}, 1]]), + new Map([[{}, 1], [1, 1]]) +); +notUtilIsDeepStrict( + new Map([[{}, 1], [true, 1]]), + new Map([[{}, 1], [1, 1]]) +); + +// Similar primitive key / values +notUtilIsDeepStrict( + new Set([1, true, false]), + new Set(['1', 0, '0']) +); +notUtilIsDeepStrict( + new Map([[1, 5], [true, 5], [false, 5]]), + new Map([['1', 5], [0, 5], ['0', 5]]) +); + +// undefined value in Map +utilIsDeepStrict( + new Map([[1, undefined]]), + new Map([[1, undefined]]) +); +notUtilIsDeepStrict( + new Map([[1, null]]), + new Map([['1', undefined]]) +); +notUtilIsDeepStrict( + new Map([[1, undefined]]), + new Map([[2, undefined]]) +); + +// null as key +utilIsDeepStrict( + new Map([[null, 3]]), + new Map([[null, 3]]) +); +notUtilIsDeepStrict( + new Map([[null, undefined]]), + new Map([[undefined, null]]) +); +notUtilIsDeepStrict( + new Set([null]), + new Set([undefined]) +); + +// GH-6416. Make sure circular refs don't throw. +{ + const b = {}; + b.b = b; + const c = {}; + c.b = c; + + utilIsDeepStrict(b, c); + + const d = {}; + d.a = 1; + d.b = d; + const e = {}; + e.a = 1; + e.b = {}; + + notUtilIsDeepStrict(d, e); +} + +// GH-14441. Circular structures should be consistent +{ + const a = {}; + const b = {}; + a.a = a; + b.a = {}; + b.a.a = a; + utilIsDeepStrict(a, b); +} + +{ + const a = new Set(); + const b = new Set(); + const c = new Set(); + a.add(a); + b.add(b); + c.add(a); + utilIsDeepStrict(b, c); +} + +// GH-7178. Ensure reflexivity of deepEqual with `arguments` objects. +{ + const args = (function() { return arguments; })(); + notUtilIsDeepStrict([], args); +} + +// More checking that arguments objects are handled correctly +{ + // eslint-disable-next-line func-style + const returnArguments = function() { return arguments; }; + + const someArgs = returnArguments('a'); + const sameArgs = returnArguments('a'); + const diffArgs = returnArguments('b'); + + notUtilIsDeepStrict(someArgs, ['a']); + notUtilIsDeepStrict(someArgs, { '0': 'a' }); + notUtilIsDeepStrict(someArgs, diffArgs); + utilIsDeepStrict(someArgs, sameArgs); +} + +{ + const values = [ + 123, + Infinity, + 0, + null, + undefined, + false, + true, + {}, + [], + () => {}, + ]; + utilIsDeepStrict(new Set(values), new Set(values)); + utilIsDeepStrict(new Set(values), new Set(values.reverse())); + + const mapValues = values.map((v) => [v, { a: 5 }]); + utilIsDeepStrict(new Map(mapValues), new Map(mapValues)); + utilIsDeepStrict(new Map(mapValues), new Map(mapValues.reverse())); +} + +{ + const s1 = new Set(); + const s2 = new Set(); + s1.add(1); + s1.add(2); + s2.add(2); + s2.add(1); + utilIsDeepStrict(s1, s2); +} + +{ + const m1 = new Map(); + const m2 = new Map(); + const obj = { a: 5, b: 6 }; + m1.set(1, obj); + m1.set(2, 'hi'); + m1.set(3, [1, 2, 3]); + + m2.set(2, 'hi'); // different order + m2.set(1, obj); + m2.set(3, [1, 2, 3]); // deep equal, but not reference equal. + + utilIsDeepStrict(m1, m2); +} + +{ + const m1 = new Map(); + const m2 = new Map(); + + // m1 contains itself. + m1.set(1, m1); + m2.set(1, new Map()); + + notUtilIsDeepStrict(m1, m2); +} + +{ + const map1 = new Map([[1, 1]]); + const map2 = new Map([[1, '1']]); + assert.strictEqual(util.isDeepStrictEqual(map1, map2), false); +} + +{ + // Two equivalent sets / maps with different key/values applied shouldn't be + // the same. This is a terrible idea to do in practice, but deepEqual should + // still check for it. + const s1 = new Set(); + const s2 = new Set(); + s1.x = 5; + notUtilIsDeepStrict(s1, s2); + + const m1 = new Map(); + const m2 = new Map(); + m1.x = 5; + notUtilIsDeepStrict(m1, m2); +} + +{ + // Circular references. + const s1 = new Set(); + s1.add(s1); + const s2 = new Set(); + s2.add(s2); + utilIsDeepStrict(s1, s2); + + const m1 = new Map(); + m1.set(2, m1); + const m2 = new Map(); + m2.set(2, m2); + utilIsDeepStrict(m1, m2); + + const m3 = new Map(); + m3.set(m3, 2); + const m4 = new Map(); + m4.set(m4, 2); + utilIsDeepStrict(m3, m4); +} + +// Handle sparse arrays +utilIsDeepStrict([1, , , 3], [1, , , 3]); +notUtilIsDeepStrict([1, , , 3], [1, , , 3, , , ]); + +// Handle different error messages +{ + const err1 = new Error('foo1'); + const err2 = new Error('foo2'); + const err3 = new TypeError('foo1'); + notUtilIsDeepStrict(err1, err2, assert.AssertionError); + notUtilIsDeepStrict(err1, err3, assert.AssertionError); + // TODO: evaluate if this should throw or not. The same applies for RegExp + // Date and any object that has the same keys but not the same prototype. + notUtilIsDeepStrict(err1, {}, assert.AssertionError); +} + +// Handle NaN +assert.strictEqual(util.isDeepStrictEqual(NaN, NaN), true); +assert.strictEqual(util.isDeepStrictEqual({ a: NaN }, { a: NaN }), true); +assert.strictEqual( + util.isDeepStrictEqual([ 1, 2, NaN, 4 ], [ 1, 2, NaN, 4 ]), + true +); + +// Handle boxed primitives +{ + const boxedString = new String('test'); + const boxedSymbol = Object(Symbol()); + notUtilIsDeepStrict(new Boolean(true), Object(false)); + notUtilIsDeepStrict(Object(true), new Number(1)); + notUtilIsDeepStrict(new Number(2), new Number(1)); + notUtilIsDeepStrict(boxedSymbol, Object(Symbol())); + notUtilIsDeepStrict(boxedSymbol, {}); + utilIsDeepStrict(boxedSymbol, boxedSymbol); + utilIsDeepStrict(Object(true), Object(true)); + utilIsDeepStrict(Object(2), Object(2)); + utilIsDeepStrict(boxedString, Object('test')); + boxedString.slow = true; + notUtilIsDeepStrict(boxedString, Object('test')); + boxedSymbol.slow = true; + notUtilIsDeepStrict(boxedSymbol, {}); +} + +// Minus zero +notUtilIsDeepStrict(0, -0); +utilIsDeepStrict(-0, -0); + +// Handle symbols (enumerable only) +{ + const symbol1 = Symbol(); + const obj1 = { [symbol1]: 1 }; + const obj2 = { [symbol1]: 1 }; + const obj3 = { [Symbol()]: 1 }; + // Add a non enumerable symbol as well. It is going to be ignored! + Object.defineProperty(obj2, Symbol(), { value: 1 }); + notUtilIsDeepStrict(obj1, obj3); + utilIsDeepStrict(obj1, obj2); + // TypedArrays have a fast path. Test for this as well. + const a = new Uint8Array(4); + const b = new Uint8Array(4); + a[symbol1] = true; + b[symbol1] = false; + notUtilIsDeepStrict(a, b); + b[symbol1] = true; + utilIsDeepStrict(a, b); + // The same as TypedArrays is valid for boxed primitives + const boxedStringA = new String('test'); + const boxedStringB = new String('test'); + boxedStringA[symbol1] = true; + notUtilIsDeepStrict(boxedStringA, boxedStringB); + boxedStringA[symbol1] = true; + utilIsDeepStrict(a, b); +} |