diff --git a/benchmark/assert/deepequal-buffer.js b/benchmark/assert/deepequal-buffer.js index fe3f6f9754c3a0..baf3e1766c15f5 100644 --- a/benchmark/assert/deepequal-buffer.js +++ b/benchmark/assert/deepequal-buffer.js @@ -3,17 +3,16 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [1e5], - len: [1e2, 1e4], + n: [2e4], + len: [1e2, 1e3], + strict: [0, 1], method: [ 'deepEqual', - 'deepStrictEqual', - 'notDeepEqual', - 'notDeepStrictEqual' + 'notDeepEqual' ] }); -function main({ len, n, method }) { +function main({ len, n, method, strict }) { if (!method) method = 'deepEqual'; const data = Buffer.allocUnsafe(len + 1); @@ -24,6 +23,9 @@ function main({ len, n, method }) { data.copy(expected); data.copy(expectedWrong); + if (strict) { + method = method.replace('eep', 'eepStrict'); + } const fn = assert[method]; const value2 = method.includes('not') ? expectedWrong : expected; diff --git a/benchmark/assert/deepequal-map.js b/benchmark/assert/deepequal-map.js index c6c7173fe8ed6d..c15e1c9a360c3b 100644 --- a/benchmark/assert/deepequal-map.js +++ b/benchmark/assert/deepequal-map.js @@ -7,21 +7,14 @@ const { deepEqual, deepStrictEqual, notDeepEqual, notDeepStrictEqual } = const bench = common.createBenchmark(main, { n: [5e2], len: [5e2], + strict: [0, 1], method: [ 'deepEqual_primitiveOnly', - 'deepStrictEqual_primitiveOnly', 'deepEqual_objectOnly', - 'deepStrictEqual_objectOnly', 'deepEqual_mixed', - 'deepStrictEqual_mixed', - 'deepEqual_looseMatches', 'notDeepEqual_primitiveOnly', - 'notDeepStrictEqual_primitiveOnly', 'notDeepEqual_objectOnly', - 'notDeepStrictEqual_objectOnly', - 'notDeepEqual_mixed', - 'notDeepStrictEqual_mixed', - 'notDeepEqual_looseMatches', + 'notDeepEqual_mixed' ] }); @@ -37,7 +30,7 @@ function benchmark(method, n, values, values2) { bench.end(n); } -function main({ n, len, method }) { +function main({ n, len, method, strict }) { const array = Array(len).fill(1); var values, values2; @@ -46,74 +39,33 @@ function main({ n, len, method }) { // Empty string falls through to next line as default, mostly for tests. case 'deepEqual_primitiveOnly': values = array.map((_, i) => [`str_${i}`, 123]); - benchmark(deepEqual, n, values); - break; - case 'deepStrictEqual_primitiveOnly': - values = array.map((_, i) => [`str_${i}`, 123]); - benchmark(deepStrictEqual, n, values); + benchmark(strict ? deepStrictEqual : deepEqual, n, values); break; case 'deepEqual_objectOnly': values = array.map((_, i) => [[`str_${i}`, 1], 123]); - benchmark(deepEqual, n, values); - break; - case 'deepStrictEqual_objectOnly': - values = array.map((_, i) => [[`str_${i}`, 1], 123]); - benchmark(deepStrictEqual, n, values); + benchmark(strict ? deepStrictEqual : deepEqual, n, values); break; case 'deepEqual_mixed': values = array.map((_, i) => [i % 2 ? [`str_${i}`, 1] : `str_${i}`, 123]); - benchmark(deepEqual, n, values); - break; - case 'deepStrictEqual_mixed': - values = array.map((_, i) => [i % 2 ? [`str_${i}`, 1] : `str_${i}`, 123]); - benchmark(deepStrictEqual, n, values); - break; - case 'deepEqual_looseMatches': - values = array.map((_, i) => [i, i]); - values2 = values.slice().map((v) => [String(v[0]), String(v[1])]); - benchmark(deepEqual, n, values, values2); + benchmark(strict ? deepStrictEqual : deepEqual, n, values); break; case 'notDeepEqual_primitiveOnly': values = array.map((_, i) => [`str_${i}`, 123]); values2 = values.slice(0); values2[Math.floor(len / 2)] = ['w00t', 123]; - benchmark(notDeepEqual, n, values, values2); - break; - case 'notDeepStrictEqual_primitiveOnly': - values = array.map((_, i) => [`str_${i}`, 123]); - values2 = values.slice(0); - values2[Math.floor(len / 2)] = ['w00t', 123]; - benchmark(notDeepStrictEqual, n, values, values2); + benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2); break; case 'notDeepEqual_objectOnly': values = array.map((_, i) => [[`str_${i}`, 1], 123]); values2 = values.slice(0); values2[Math.floor(len / 2)] = [['w00t'], 123]; - benchmark(notDeepEqual, n, values, values2); - break; - case 'notDeepStrictEqual_objectOnly': - values = array.map((_, i) => [[`str_${i}`, 1], 123]); - values2 = values.slice(0); - values2[Math.floor(len / 2)] = [['w00t'], 123]; - benchmark(notDeepStrictEqual, n, values, values2); + benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2); break; case 'notDeepEqual_mixed': values = array.map((_, i) => [i % 2 ? [`str_${i}`, 1] : `str_${i}`, 123]); values2 = values.slice(0); values2[0] = ['w00t', 123]; - benchmark(notDeepEqual, n, values, values2); - break; - case 'notDeepStrictEqual_mixed': - values = array.map((_, i) => [i % 2 ? [`str_${i}`, 1] : `str_${i}`, 123]); - values2 = values.slice(0); - values2[0] = ['w00t', 123]; - benchmark(notDeepStrictEqual, n, values, values2); - break; - case 'notDeepEqual_looseMatches': - values = array.map((_, i) => [i, i]); - values2 = values.slice().map((v) => [String(v[0]), String(v[1])]); - values2[len - 1] = [String(len + 1), String(len + 1)]; - benchmark(notDeepEqual, n, values, values2); + benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2); break; default: throw new Error(`Unsupported method ${method}`); diff --git a/benchmark/assert/deepequal-object.js b/benchmark/assert/deepequal-object.js index 124943b19ec761..1bf0cfc9104a70 100644 --- a/benchmark/assert/deepequal-object.js +++ b/benchmark/assert/deepequal-object.js @@ -4,13 +4,12 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [1e6], - size: [1e2, 1e3, 1e4], + n: [5e3], + size: [1e2, 1e3, 5e4], + strict: [0, 1], method: [ 'deepEqual', - 'deepStrictEqual', - 'notDeepEqual', - 'notDeepStrictEqual' + 'notDeepEqual' ] }); @@ -20,12 +19,14 @@ function createObj(source, add = '') { nope: { bar: `123${add}`, a: [1, 2, 3], - baz: n + baz: n, + c: {}, + b: [] } })); } -function main({ size, n, method }) { +function main({ size, n, method, strict }) { // TODO: Fix this "hack". `n` should not be manipulated. n = n / size; @@ -37,6 +38,9 @@ function main({ size, n, method }) { const expected = createObj(source); const expectedWrong = createObj(source, '4'); + if (strict) { + method = method.replace('eep', 'eepStrict'); + } const fn = assert[method]; const value2 = method.includes('not') ? expectedWrong : expected; diff --git a/benchmark/assert/deepequal-prims-and-objs-big-array-set.js b/benchmark/assert/deepequal-prims-and-objs-big-array-set.js index 4578e5a250f392..71637c28cea999 100644 --- a/benchmark/assert/deepequal-prims-and-objs-big-array-set.js +++ b/benchmark/assert/deepequal-prims-and-objs-big-array-set.js @@ -5,29 +5,22 @@ const { deepEqual, deepStrictEqual, notDeepEqual, notDeepStrictEqual } = require('assert'); const primValues = { - 'null': null, - 'undefined': undefined, 'string': 'a', 'number': 1, - 'boolean': true, 'object': { 0: 'a' }, - 'array': [1, 2, 3], - 'new-array': new Array([1, 2, 3]) + 'array': [1, 2, 3] }; const bench = common.createBenchmark(main, { primitive: Object.keys(primValues), n: [25], - len: [1e5], + len: [2e4], + strict: [0, 1], method: [ 'deepEqual_Array', - 'deepStrictEqual_Array', 'notDeepEqual_Array', - 'notDeepStrictEqual_Array', 'deepEqual_Set', - 'deepStrictEqual_Set', - 'notDeepEqual_Set', - 'notDeepStrictEqual_Set' + 'notDeepEqual_Set' ] }); @@ -39,7 +32,7 @@ function run(fn, n, actual, expected) { bench.end(n); } -function main({ n, len, primitive, method }) { +function main({ n, len, primitive, method, strict }) { const prim = primValues[primitive]; const actual = []; const expected = []; @@ -62,28 +55,17 @@ function main({ n, len, primitive, method }) { // Empty string falls through to next line as default, mostly for tests. case '': case 'deepEqual_Array': - run(deepEqual, n, actual, expected); - break; - case 'deepStrictEqual_Array': - run(deepStrictEqual, n, actual, expected); + run(strict ? deepStrictEqual : deepEqual, n, actual, expected); break; case 'notDeepEqual_Array': - run(notDeepEqual, n, actual, expectedWrong); - break; - case 'notDeepStrictEqual_Array': - run(notDeepStrictEqual, n, actual, expectedWrong); + run(strict ? notDeepStrictEqual : notDeepEqual, n, actual, expectedWrong); break; case 'deepEqual_Set': - run(deepEqual, n, actualSet, expectedSet); - break; - case 'deepStrictEqual_Set': - run(deepStrictEqual, n, actualSet, expectedSet); + run(strict ? deepStrictEqual : deepEqual, n, actualSet, expectedSet); break; case 'notDeepEqual_Set': - run(notDeepEqual, n, actualSet, expectedWrongSet); - break; - case 'notDeepStrictEqual_Set': - run(notDeepStrictEqual, n, actualSet, expectedWrongSet); + run(strict ? notDeepStrictEqual : notDeepEqual, + n, actualSet, expectedWrongSet); break; default: throw new Error(`Unsupported method "${method}"`); diff --git a/benchmark/assert/deepequal-prims-and-objs-big-loop.js b/benchmark/assert/deepequal-prims-and-objs-big-loop.js index f1183dab32a3fb..13abaf107fdfb9 100644 --- a/benchmark/assert/deepequal-prims-and-objs-big-loop.js +++ b/benchmark/assert/deepequal-prims-and-objs-big-loop.js @@ -3,28 +3,23 @@ const common = require('../common.js'); const assert = require('assert'); const primValues = { - 'null': null, - 'undefined': undefined, 'string': 'a', 'number': 1, - 'boolean': true, 'object': { 0: 'a' }, - 'array': [1, 2, 3], - 'new-array': new Array([1, 2, 3]) + 'array': [1, 2, 3] }; const bench = common.createBenchmark(main, { primitive: Object.keys(primValues), - n: [1e6], + n: [2e4], + strict: [0, 1], method: [ 'deepEqual', - 'deepStrictEqual', 'notDeepEqual', - 'notDeepStrictEqual' ] }); -function main({ n, primitive, method }) { +function main({ n, primitive, method, strict }) { if (!method) method = 'deepEqual'; const prim = primValues[primitive]; @@ -32,6 +27,9 @@ function main({ n, primitive, method }) { const expected = prim; const expectedWrong = 'b'; + if (strict) { + method = method.replace('eep', 'eepStrict'); + } const fn = assert[method]; const value2 = method.includes('not') ? expectedWrong : expected; diff --git a/benchmark/assert/deepequal-set.js b/benchmark/assert/deepequal-set.js index 6769e5e37fafb7..1f4061afb2a300 100644 --- a/benchmark/assert/deepequal-set.js +++ b/benchmark/assert/deepequal-set.js @@ -7,21 +7,14 @@ const { deepEqual, deepStrictEqual, notDeepEqual, notDeepStrictEqual } = const bench = common.createBenchmark(main, { n: [5e2], len: [5e2], + strict: [0, 1], method: [ 'deepEqual_primitiveOnly', - 'deepStrictEqual_primitiveOnly', 'deepEqual_objectOnly', - 'deepStrictEqual_objectOnly', 'deepEqual_mixed', - 'deepStrictEqual_mixed', - 'deepEqual_looseMatches', 'notDeepEqual_primitiveOnly', - 'notDeepStrictEqual_primitiveOnly', 'notDeepEqual_objectOnly', - 'notDeepStrictEqual_objectOnly', - 'notDeepEqual_mixed', - 'notDeepStrictEqual_mixed', - 'notDeepEqual_looseMatches', + 'notDeepEqual_mixed' ] }); @@ -37,7 +30,7 @@ function benchmark(method, n, values, values2) { bench.end(n); } -function main({ n, len, method }) { +function main({ n, len, method, strict }) { const array = Array(len).fill(1); var values, values2; @@ -47,60 +40,29 @@ function main({ n, len, method }) { // Empty string falls through to next line as default, mostly for tests. case 'deepEqual_primitiveOnly': values = array.map((_, i) => `str_${i}`); - benchmark(deepEqual, n, values); - break; - case 'deepStrictEqual_primitiveOnly': - values = array.map((_, i) => `str_${i}`); - benchmark(deepStrictEqual, n, values); + benchmark(strict ? deepStrictEqual : deepEqual, n, values); break; case 'deepEqual_objectOnly': values = array.map((_, i) => [`str_${i}`, null]); - benchmark(deepEqual, n, values); - break; - case 'deepStrictEqual_objectOnly': - values = array.map((_, i) => [`str_${i}`, null]); - benchmark(deepStrictEqual, n, values); + benchmark(strict ? deepStrictEqual : deepEqual, n, values); break; case 'deepEqual_mixed': values = array.map((_, i) => { return i % 2 ? [`str_${i}`, null] : `str_${i}`; }); - benchmark(deepEqual, n, values); - break; - case 'deepStrictEqual_mixed': - values = array.map((_, i) => { - return i % 2 ? [`str_${i}`, null] : `str_${i}`; - }); - benchmark(deepStrictEqual, n, values); - break; - case 'deepEqual_looseMatches': - values = array.map((_, i) => i); - values2 = values.slice().map((v) => String(v)); - benchmark(deepEqual, n, values, values2); + benchmark(strict ? deepStrictEqual : deepEqual, n, values); break; case 'notDeepEqual_primitiveOnly': values = array.map((_, i) => `str_${i}`); values2 = values.slice(0); values2[Math.floor(len / 2)] = 'w00t'; - benchmark(notDeepEqual, n, values, values2); - break; - case 'notDeepStrictEqual_primitiveOnly': - values = array.map((_, i) => `str_${i}`); - values2 = values.slice(0); - values2[Math.floor(len / 2)] = 'w00t'; - benchmark(notDeepStrictEqual, n, values, values2); + benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2); break; case 'notDeepEqual_objectOnly': values = array.map((_, i) => [`str_${i}`, null]); values2 = values.slice(0); values2[Math.floor(len / 2)] = ['w00t']; - benchmark(notDeepEqual, n, values, values2); - break; - case 'notDeepStrictEqual_objectOnly': - values = array.map((_, i) => [`str_${i}`, null]); - values2 = values.slice(0); - values2[Math.floor(len / 2)] = ['w00t']; - benchmark(notDeepStrictEqual, n, values, values2); + benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2); break; case 'notDeepEqual_mixed': values = array.map((_, i) => { @@ -108,21 +70,7 @@ function main({ n, len, method }) { }); values2 = values.slice(); values2[0] = 'w00t'; - benchmark(notDeepEqual, n, values, values2); - break; - case 'notDeepStrictEqual_mixed': - values = array.map((_, i) => { - return i % 2 ? [`str_${i}`, null] : `str_${i}`; - }); - values2 = values.slice(); - values2[0] = 'w00t'; - benchmark(notDeepStrictEqual, n, values, values2); - break; - case 'notDeepEqual_looseMatches': - values = array.map((_, i) => i); - values2 = values.slice().map((v) => String(v)); - values2[len - 1] = String(len + 1); - benchmark(notDeepEqual, n, values, values2); + benchmark(strict ? notDeepStrictEqual : notDeepEqual, n, values, values2); break; default: throw new Error(`Unsupported method "${method}"`); diff --git a/benchmark/assert/deepequal-typedarrays.js b/benchmark/assert/deepequal-typedarrays.js index c4d8f434bf4e8a..9f9c68a6731254 100644 --- a/benchmark/assert/deepequal-typedarrays.js +++ b/benchmark/assert/deepequal-typedarrays.js @@ -6,39 +6,39 @@ const bench = common.createBenchmark(main, { type: [ 'Int8Array', 'Uint8Array', - 'Int16Array', - 'Uint16Array', - 'Int32Array', - 'Uint32Array', 'Float32Array', 'Float64Array', 'Uint8ClampedArray', ], - n: [1], + n: [5e2], + strict: [0, 1], method: [ 'deepEqual', - 'deepStrictEqual', 'notDeepEqual', - 'notDeepStrictEqual' ], - len: [1e6] + len: [1e2, 5e3] }); -function main({ type, n, len, method }) { +function main({ type, n, len, method, strict }) { if (!method) method = 'deepEqual'; const clazz = global[type]; const actual = new clazz(len); const expected = new clazz(len); - const expectedWrong = Buffer.alloc(len); + const expectedWrong = new clazz(len); const wrongIndex = Math.floor(len / 2); expectedWrong[wrongIndex] = 123; + if (strict) { + method = method.replace('eep', 'eepStrict'); + } const fn = assert[method]; const value2 = method.includes('not') ? expectedWrong : expected; bench.start(); for (var i = 0; i < n; ++i) { + actual[0] = i; + value2[0] = i; fn(actual, value2); } bench.end(n); diff --git a/benchmark/assert/ok.js b/benchmark/assert/ok.js index 849be65539aef7..4869a9de69c319 100644 --- a/benchmark/assert/ok.js +++ b/benchmark/assert/ok.js @@ -4,7 +4,7 @@ const common = require('../common.js'); const assert = require('assert'); const bench = common.createBenchmark(main, { - n: [1e9] + n: [1e5] }); function main({ n }) { diff --git a/benchmark/assert/throws.js b/benchmark/assert/throws.js index a8a7bd4509e1ca..1788630cb2f992 100644 --- a/benchmark/assert/throws.js +++ b/benchmark/assert/throws.js @@ -4,10 +4,9 @@ const common = require('../common.js'); const { throws, doesNotThrow } = require('assert'); const bench = common.createBenchmark(main, { - n: [1e6], + n: [1e4], method: [ 'doesNotThrow', - 'throws', 'throws_TypeError', 'throws_RegExp' ] @@ -30,13 +29,6 @@ function main({ n, method }) { } bench.end(n); break; - case 'throws': - bench.start(); - for (i = 0; i < n; ++i) { - throws(throwError); - } - bench.end(n); - break; case 'throws_TypeError': bench.start(); for (i = 0; i < n; ++i) { diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index f4491f070f18d6..e538410c2b5535 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -5,10 +5,33 @@ const { isArrayBufferView } = require('internal/util/types'); const { internalBinding } = require('internal/bootstrap/loaders'); const { isDate, isMap, isRegExp, isSet } = internalBinding('types'); -function objectToString(o) { - return Object.prototype.toString.call(o); +const ReflectApply = Reflect.apply; + +function uncurryThis(func) { + return (thisArg, ...args) => ReflectApply(func, thisArg, args); } +const kStrict = true; +const kLoose = false; + +const kNoIterator = 0; +const kIsArray = 1; +const kIsSet = 2; +const kIsMap = 3; + +const objectToString = uncurryThis(Object.prototype.toString); +const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty); +const getTime = uncurryThis(Date.prototype.getTime); +const propertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable); + +const objectKeys = Object.keys; +const getPrototypeOf = Object.getPrototypeOf; +const getOwnPropertySymbols = Object.getOwnPropertySymbols; +const objectIs = Object.is; +const numberIsNaN = Number.isNaN; + +const array = []; + // Check if they have the same source and flags function areSimilarRegExps(a, b) { return a.source === b.source && a.flags === b.flags; @@ -66,8 +89,8 @@ function isObjectOrArrayTag(tag) { // b) The same prototypes. function strictDeepEqual(val1, val2, memos) { if (typeof val1 !== 'object') { - return typeof val1 === 'number' && Number.isNaN(val1) && - Number.isNaN(val2); + return typeof val1 === 'number' && numberIsNaN(val1) && + numberIsNaN(val2); } if (typeof val2 !== 'object' || val1 === null || val2 === null) { return false; @@ -78,22 +101,38 @@ function strictDeepEqual(val1, val2, memos) { if (val1Tag !== val2Tag) { return false; } - if (Object.getPrototypeOf(val1) !== Object.getPrototypeOf(val2)) { + if (getPrototypeOf(val1) !== getPrototypeOf(val2)) { return false; } if (val1Tag === '[object Array]') { // Check for sparse arrays and general fast path - if (val1.length !== val2.length) + if (val1.length !== val2.length) { + return false; + } + const keys1 = objectKeys(val1); + const keys2 = objectKeys(val2); + if (keys1.length !== keys2.length) { return false; - // Skip testing the part below and continue with the keyCheck. - return keyCheck(val1, val2, true, memos); + } + // Fast path for non sparse arrays (no key comparison for indices + // properties). + if (val1.length === keys1.length) { + if (keys1.length === 0 || +(keys1[val1.length - 1]) === val1.length - 1) { + return keyCheck(val1, val2, kStrict, memos, kIsArray, array); + } + } else if (keys1.length > val1.length && + +(keys1[val1.length - 1]) === val1.length - 1) { + const keys = keys1.slice(val1.length); + return keyCheck(val1, val2, kStrict, memos, kIsArray, keys); + } + // Only set this to kIsArray in case the array is not sparse! + return keyCheck(val1, val2, kStrict, memos, kNoIterator, keys1); } if (val1Tag === '[object Object]') { - // Skip testing the part below and continue with the keyCheck. - return keyCheck(val1, val2, true, memos); + return keyCheck(val1, val2, kStrict, memos, kNoIterator); } if (isDate(val1)) { - if (val1.getTime() !== val2.getTime()) { + if (getTime(val1) !== getTime(val2)) { return false; } } else if (isRegExp(val1)) { @@ -113,27 +152,50 @@ function strictDeepEqual(val1, val2, memos) { 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); + // if they both only contain numeric keys, we don't need to exam further. + const keys1 = objectKeys(val1); + const keys2 = objectKeys(val2); + if (keys1.length !== keys2.length) { + return false; + } + if (keys1.length === val1.length) { + return keyCheck(val1, val2, kStrict, memos, kNoIterator, array); + } + // Only compare the special keys. + const keys = keys1.slice(val1.length); + return keyCheck(val1, val2, kStrict, memos, kNoIterator, keys); + } else if (isSet(val1)) { + if (!isSet(val2) || val1.size !== val2.size) { + return false; + } + return keyCheck(val1, val2, kStrict, memos, kIsSet); + } else if (isMap(val1)) { + if (!isMap(val2) || val1.size !== val2.size) { + return false; + } + return keyCheck(val1, val2, kStrict, memos, kIsMap); + // TODO: Make the valueOf checks safe. } 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 (typeof val2.valueOf !== 'function') { + return false; + } if (!innerDeepEqual(val1Value, val2.valueOf(), true)) return false; - // Fast path for boxed primitives - var lengthval1 = 0; - var lengthval2 = 0; + // Fast path for boxed primitive strings. if (typeof val1Value === 'string') { - lengthval1 = val1.length; - lengthval2 = val2.length; + const keys1 = objectKeys(val1); + const keys2 = objectKeys(val2); + if (keys1.length !== keys2.length) { + return false; + } + const keys = keys1.slice(val1.length); + return keyCheck(val1, val2, kStrict, memos, kNoIterator, keys); } - return keyCheck(val1, val2, true, memos, lengthval1, - lengthval2); } } - return keyCheck(val1, val2, true, memos); + return keyCheck(val1, val2, kStrict, memos, kNoIterator); } function looseDeepEqual(val1, val2, memos) { @@ -148,7 +210,7 @@ function looseDeepEqual(val1, val2, memos) { return false; } if (isDate(val1) && isDate(val2)) { - return val1.getTime() === val2.getTime(); + return getTime(val1) === getTime(val2); } if (isRegExp(val1) && isRegExp(val2)) { return areSimilarRegExps(val1, val2); @@ -170,10 +232,27 @@ function looseDeepEqual(val1, val2, memos) { } else if (isArguments(val1Tag) || isArguments(val2Tag)) { return false; } - return keyCheck(val1, val2, false, memos); + if (isSet(val1)) { + if (!isSet(val2) || val1.size !== val2.size) { + return false; + } + return keyCheck(val1, val2, kLoose, memos, kIsSet); + } else if (isMap(val1)) { + if (!isMap(val2) || val1.size !== val2.size) { + return false; + } + return keyCheck(val1, val2, kLoose, memos, kIsMap); + } else if (isSet(val2) || isMap(val2)) { + return false; + } + return keyCheck(val1, val2, kLoose, memos, kNoIterator); } -function keyCheck(val1, val2, strict, memos, lengthA, lengthB) { +function enumerable(val, keys) { + return keys.filter((k) => propertyIsEnumerable(val, k)); +} + +function keyCheck(val1, val2, strict, memos, iterator, aKeys) { // For all remaining Object pairs, including Array, objects and Maps, // equivalence is determined by having: // a) The same number of owned enumerable properties @@ -181,50 +260,51 @@ function keyCheck(val1, val2, strict, memos, lengthA, lengthB) { // 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; + if (aKeys === undefined) { + aKeys = objectKeys(val1); + const bKeys = objectKeys(val2); - // The pair must have the same number of owned properties. - if (aKeys.length !== bKeys.length) - return false; + // 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); + let symbolKeysA = getOwnPropertySymbols(val1); + let symbolKeysB = getOwnPropertySymbols(val2); if (symbolKeysA.length !== 0) { - symbolKeysA = symbolKeysA.filter((k) => - propertyIsEnumerable.call(val1, k)); - symbolKeysB = symbolKeysB.filter((k) => - propertyIsEnumerable.call(val2, k)); + symbolKeysA = enumerable(val1, symbolKeysA); + symbolKeysB = enumerable(val2, symbolKeysB); if (symbolKeysA.length !== symbolKeysB.length) return false; - } else if (symbolKeysB.length !== 0 && symbolKeysB.filter((k) => - propertyIsEnumerable.call(val2, k)).length !== 0) { + } else if (symbolKeysB.length !== 0 && + enumerable(val2, symbolKeysB).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) { + if (symbolKeysA.length === 0) { + if (aKeys.length === 0) { + if (iterator === kNoIterator) { + return true; + } + if (iterator === kIsArray && val1.length === 0) { + return true; + } + } + } else if (aKeys.length === 0) { + aKeys = symbolKeysA; + } else { aKeys.push(...symbolKeysA); - bKeys.push(...symbolKeysB); } + } else if (aKeys.length === 0 && iterator === kNoIterator) { + return true; } - // 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) + // Cheap key test + for (var i = 0; i < aKeys.length; i++) { + if (!hasOwnProperty(val2, aKeys[i])) { return false; + } } // Use memos to handle cycles. @@ -251,7 +331,7 @@ function keyCheck(val1, val2, strict, memos, lengthA, lengthB) { memos.val1.set(val1, memos.position); memos.val2.set(val2, memos.position); - const areEq = objEquiv(val1, val2, strict, aKeys, memos); + const areEq = objEquiv(val1, val2, strict, aKeys, memos, iterator); memos.val1.delete(val1); memos.val2.delete(val2); @@ -264,7 +344,7 @@ function innerDeepEqual(val1, val2, strict, memos) { if (val1 === val2) { if (val1 !== 0) return true; - return strict ? Object.is(val1, val2) : true; + return strict ? objectIs(val1, val2) : true; } // Check more closely if val1 and val2 are equal. @@ -294,7 +374,7 @@ function setHasLoosePrim(a, b, val) { if (altValues === undefined) return false; - var matches = 1; + let matches = 1; for (var i = 0; i < altValues.length; i++) { if (b.has(altValues[i])) { matches--; @@ -307,19 +387,9 @@ function setHasLoosePrim(a, b, val) { } 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; + let 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 @@ -355,7 +425,7 @@ function setEquiv(a, b, strict, memo) { } function findLooseMatchingPrimitives(prim) { - var values, number; + let values, number; switch (typeof prim) { case 'number': values = ['' + prim]; @@ -393,7 +463,7 @@ function mapHasLoosePrim(a, b, key1, memo, item1, item2) { const setA = new Set(); const setB = new Set(); - var keyCount = 1; + let keyCount = 1; setA.add(item1); if (b.has(key1)) { @@ -442,10 +512,7 @@ function mapHasEqualEntry(set, map, key1, item1, strict, memo) { } function mapEquiv(a, b, strict, memo) { - if (a.size !== b.size) - return false; - - var set = null; + let set = null; for (const [key, item1] of a) { if (typeof key === 'object' && key !== null) { @@ -480,35 +547,44 @@ function mapEquiv(a, b, strict, memo) { return true; } -function objEquiv(a, b, strict, keys, memos) { +function objEquiv(a, b, strict, keys, memos, iterator) { // Sets and maps don't have their entries accessible via normal object // properties. - if (isSet(a)) { - if (!isSet(b) || !setEquiv(a, b, strict, memos)) + let i = 0; + + if (iterator === kIsSet) { + if (!setEquiv(a, b, strict, memos)) { return false; - } else if (isMap(a)) { - if (!isMap(b) || !mapEquiv(a, b, strict, memos)) + } + } else if (iterator === kIsMap) { + if (!mapEquiv(a, b, strict, memos)) { return false; - } else if (isSet(b) || isMap(b)) { - return false; + } + } else if (iterator === kIsArray) { + for (; i < a.length; i++) { + if (!innerDeepEqual(a[i], b[i], strict, memos)) { + return false; + } + } } // The pair must have equivalent values for every corresponding key. // Possibly expensive deep test: - for (var i = 0; i < keys.length; i++) { + for (i = 0; i < keys.length; i++) { const key = keys[i]; - if (!innerDeepEqual(a[key], b[key], strict, memos)) + if (!innerDeepEqual(a[key], b[key], strict, memos)) { return false; + } } return true; } function isDeepEqual(val1, val2) { - return innerDeepEqual(val1, val2, false); + return innerDeepEqual(val1, val2, kLoose); } function isDeepStrictEqual(val1, val2) { - return innerDeepEqual(val1, val2, true); + return innerDeepEqual(val1, val2, kStrict); } module.exports = { diff --git a/test/parallel/test-assert-deep.js b/test/parallel/test-assert-deep.js index 538f4bef49d8c6..2acd3757b45b16 100644 --- a/test/parallel/test-assert-deep.js +++ b/test/parallel/test-assert-deep.js @@ -367,6 +367,10 @@ assertOnlyDeepEqual( new Map([[null, undefined]]), new Map([[undefined, null]]) ); +assertOnlyDeepEqual( + new Map([[1, {}]]), + new Map([[true, {}]]) +); assertOnlyDeepEqual( new Set([null]), new Set([undefined]) @@ -547,10 +551,9 @@ assertOnlyDeepEqual([1, , , 3], [1, , , 3, , , ]); // Handle different error messages { const err1 = new Error('foo1'); - const err2 = new Error('foo2'); - const err3 = new TypeError('foo1'); - assertNotDeepOrStrict(err1, err2, assert.AssertionError); - assertNotDeepOrStrict(err1, err3, assert.AssertionError); + assertNotDeepOrStrict(err1, new Error('foo2'), assert.AssertionError); + assertNotDeepOrStrict(err1, new TypeError('foo1'), assert.AssertionError); + assertDeepAndStrictEqual(err1, new Error('foo1')); // 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. assertOnlyDeepEqual(err1, {}, assert.AssertionError); @@ -927,3 +930,10 @@ assert.throws(() => assert.deepStrictEqual(new Boolean(true), {}), ); util.inspect.defaultOptions = tmp; } + +// Basic valueOf check. +{ + const a = new String(1); + a.valueOf = undefined; + assertNotDeepOrStrict(a, new String(1), assert.AssertionError); +} diff --git a/test/parallel/test-benchmark-assert.js b/test/parallel/test-benchmark-assert.js index 3ca72df718cd64..8a8ba0244489e5 100644 --- a/test/parallel/test-benchmark-assert.js +++ b/test/parallel/test-benchmark-assert.js @@ -10,6 +10,7 @@ const runBenchmark = require('../common/benchmark'); runBenchmark( 'assert', [ + 'strict=1', 'len=1', 'method=', 'n=1',