From 1637b70ca10f388ec33165273aacdc1bd89b7d57 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Mon, 3 Aug 2026 18:47:26 -0300 Subject: [PATCH 01/27] Add `deepStrictEqual` support --- index.js | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- test.js | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 308 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index fe83c74..077e301 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ const inspect = require('bare-inspect') +const getType = require('bare-type') class AssertionError extends Error { constructor(opts = {}) { @@ -107,3 +108,155 @@ exports.ifError = function ifError(actual) { assertFail({ message, actual, operator: 'ifError' }, ifError) } + +exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { + if (deepStrictEqualValue(actual, expected)) return + + assertFail({ message, actual, expected, operator: 'deepStrictEqual' }, deepStrictEqual) +} + +exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, message) { + if (!deepStrictEqualValue(actual, expected)) return + + assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) +} + +function deepStrictEqualValue(a, b) { + const type = getType(a) + + if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) + + if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false + + const isWrapped = [ + String.prototype, + Number.prototype, + Boolean.prototype, + Date.prototype + ].includes(Object.getPrototypeOf(a)) + + if (isWrapped) return deepStrictEqualValue(a.valueOf(), b.valueOf()) + + if (type.isWeakMap() || type.isWeakSet() || type.isPromise()) return a === b + + if (type.isArray()) return deepStrictEqualArray(a, b) + if (type.isMap()) return deepStrictEqualMap(a, b) + if (type.isSet()) return deepStrictEqualSet(a, b) + if (type.isRegExp()) return deepStrictEqualRegexp(a, b) + if (type.isError()) return deepStrictEqualError(a, b) + + return deepStrictEqualObject(a, b) +} + +function deepStrictEqualArray(a, b) { + if (a.length !== b.length) return false + + for (let i = 0; i < a.length; i++) { + if (!deepStrictEqualValue(a[i], b[i])) return false + } + + return true +} + +function deepStrictEqualMap(a, b) { + if (a.size !== b.size) return false + + const nonPrimitiveKeysEntriesFromA = [] + + for (const [key, value] of a) { + if (getType(key).isObject()) nonPrimitiveKeysEntriesFromA.push([key, value]) + else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key))) return false + } + + if (nonPrimitiveKeysEntriesFromA.length > 0) { + const nonPrimitiveKeysEntriesFromB = [] + + for (const [key, value] of b) { + if (getType(key).isObject()) nonPrimitiveKeysEntriesFromB.push([key, value]) + } + + if (nonPrimitiveKeysEntriesFromA.length !== nonPrimitiveKeysEntriesFromB.length) { + return false + } + + for (const [keyA, valueA] of nonPrimitiveKeysEntriesFromA) { + let found = false + + for (let i = 0; i < nonPrimitiveKeysEntriesFromB.length; i++) { + const [keyB, valueB] = nonPrimitiveKeysEntriesFromB[i] + + if (deepStrictEqualValue(keyA, keyB) && deepStrictEqualValue(valueA, valueB)) { + nonPrimitiveKeysEntriesFromB.splice(i, 1) + found = true + break + } + } + + if (found === false) return false + } + } + + return true +} + +function deepStrictEqualSet(a, b) { + if (a.size !== b.size) return false + + const nonPrimitiveItemsFromA = [] + + for (const item of a) { + if (getType(item).isObject()) nonPrimitiveItemsFromA.push(item) + else if (!b.has(item)) return false + } + + if (nonPrimitiveItemsFromA.length > 0) { + const nonPrimitiveItemsFromB = [] + + for (const item of b) { + if (getType(item).isObject()) nonPrimitiveItemsFromB.push(item) + } + + if (nonPrimitiveItemsFromA.length !== nonPrimitiveItemsFromB.length) return false + + for (const itemA of nonPrimitiveItemsFromA) { + let found = false + + for (let i = 0; i < nonPrimitiveItemsFromB.length; i++) { + const itemB = nonPrimitiveItemsFromB[i] + + if (deepStrictEqualValue(itemA, itemB)) { + nonPrimitiveItemsFromB.splice(i, 1) + found = true + break + } + } + + if (found === false) return false + } + + return nonPrimitiveItemsFromB.length === 0 + } + + return true +} + +function deepStrictEqualRegexp(a, b) { + return a.lastIndex === b.lastIndex && a.flags === b.flags && a.source === b.source +} + +function deepStrictEqualError(a, b) { + return deepStrictEqualValue(a.name, b.name) && deepStrictEqualValue(a.message, b.message) +} + +function deepStrictEqualObject(a, b) { + const aKeys = [...Object.keys(a), ...Object.getOwnPropertySymbols(a)] + const bKeys = [...Object.keys(b), ...Object.getOwnPropertySymbols(b)] + + if (aKeys.length !== bKeys.length) return false + + for (const key of aKeys) { + if (!deepStrictEqualValue(a[key], b[key])) return false + } + + return true +} diff --git a/package.json b/package.json index e7d451a..178ac25 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ }, "homepage": "https://github.com/holepunchto/bare-assert#readme", "dependencies": { - "bare-inspect": "^3.1.2" + "bare-inspect": "^3.1.2", + "bare-type": "^1.1.0" }, "devDependencies": { "brittle": "^4.1.0", diff --git a/test.js b/test.js index cd5705f..37a7fb8 100644 --- a/test.js +++ b/test.js @@ -59,3 +59,156 @@ test('ifError', (t) => { t.execution(() => assert.ifError(undefined)) t.exception(() => assert.ifError('error')) }) + +test('deepStrictEqual, basic', (t) => { + t.execution(() => assert.deepStrictEqual(NaN, NaN)) + t.execution(() => assert.deepStrictEqual(1, 1)) + t.execution(() => assert.deepStrictEqual('foo', 'foo')) + t.exception(() => assert.deepStrictEqual(1, new Date(), 'should fail'), /should fail/) +}) + +test('deepStrictEqual, array', (t) => { + t.execution(() => assert.deepStrictEqual([1, 'foo'], [1, 'foo'])) + t.exception(() => assert.deepStrictEqual([1, 'foo'], [1], 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual([1, 'foo'], [1, 'bar'], 'should fail'), /should fail/) +}) + +test('deepStrictEqual, object', (t) => { + t.execution(() => assert.deepStrictEqual({}, {})) + t.execution(() => assert.deepStrictEqual({ a: { b: 1 } }, { a: { b: 1 } })) + t.execution(() => assert.deepStrictEqual({ a: [1, 2] }, { a: [1, 2] })) + t.exception( + () => assert.deepStrictEqual({ a: { b: 1 } }, { a: { b: '1' } }, 'should fail'), + /should fail/ + ) + t.exception(() => assert.deepStrictEqual({ a: [1, 2] }, { a: [1] }, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, regexp', (t) => { + t.execution(() => assert.deepStrictEqual(/abc/, /abc/)) + t.exception(() => assert.deepStrictEqual(/abc/, /abc/g, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, map', (t) => { + t.execution(() => + assert.deepStrictEqual( + new Map([ + [{}, null], + [true, 2], + [undefined, {}] + ]), + new Map([ + [undefined, {}], + [true, 2], + [{}, null] + ]) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new Map([ + [{}, null], + [true, 2], + [undefined, {}] + ]), + new Map([ + [{}, null], + [true, 2], + [null, {}] // different key + ]), + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, set', (t) => { + t.execution(() => assert.deepStrictEqual(new Set(['a', 1, 'b', 2]), new Set(['b', 2, 'a', 1]))) + t.execution(() => + assert.deepStrictEqual(new Set([{ a: 1 }, 1, {}, 2]), new Set([{}, 2, 1, { a: 1 }])) + ) + t.exception( + () => + assert.deepStrictEqual(new Set(['a', 1, 'b', 2]), new Set(['b', 2, 'a', 42]), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, weak map', (t) => { + const map1 = new WeakMap([[Object, true]]) + const map2 = new WeakMap([[Object, true]]) + + t.execution(() => assert.deepStrictEqual(map1, map1)) + t.exception(() => assert.deepStrictEqual(map1, map2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, weak set', (t) => { + const obj = {} + + const set1 = new WeakSet([obj]) + const set2 = new WeakSet([obj]) + + t.execution(() => assert.deepStrictEqual(set1, set1)) + t.exception(() => assert.deepStrictEqual(set1, set2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, symbols', (t) => { + t.execution(() => assert.deepStrictEqual(Symbol.for('foo'), Symbol.for('foo'))) + t.exception( + () => assert.deepStrictEqual(Symbol.for('foo'), Symbol.for('bar'), 'should fail'), + /should fail/ + ) + + const sym1 = Symbol() + const sym2 = Symbol() + + t.execution(() => assert.deepStrictEqual({ [sym1]: 1 }, { [sym1]: 1 })) + t.exception( + () => assert.deepStrictEqual({ [sym1]: 1 }, { [sym2]: 1 }, 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, object wrappers', (t) => { + t.execution(() => assert.deepStrictEqual(new String('foo'), Object('foo'))) + t.execution(() => assert.deepStrictEqual(new Number(1), new Number(1))) + t.exception( + () => assert.deepStrictEqual(new Number(1), new Number(2), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, functions', (t) => { + t.execution(() => assert.deepStrictEqual(new Error('foo'), new Error('foo'))) + t.exception( + () => assert.deepStrictEqual(new Error('foo'), new Error('bar'), 'should fail'), + /should fail/ + ) +}) + +test.skip('deepStrictEqual, recursive self-references', (t) => { + const foo = {} + foo.prop = foo + + const bar = {} + bar.prop = bar + + t.execution(() => assert.deepStrictEqual(foo, bar)) +}) + +test.skip('deepStrictEqual, recursive mutual references', (t) => { + const foo = { prop: null } + const bar = { prop: foo } + foo.prop = bar + + t.execution(() => assert.deepStrictEqual(foo, bar)) +}) + +test.skip('deepStrictEqual, recursive lists', (t) => { + const foo = [] + const bar = [foo] + foo[0] = bar + + t.execution(() => assert.deepStrictEqual(foo, bar)) +}) From 310f8ab2c41f2f3fad3f231617143cd83648d0c0 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Tue, 4 Aug 2026 17:03:17 -0300 Subject: [PATCH 02/27] Add memoization --- index.d.ts | 4 ++++ index.js | 59 +++++++++++++++++++++++++++++++--------------- lib/memoize-map.js | 26 ++++++++++++++++++++ package.json | 3 ++- test.js | 6 ++--- 5 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 lib/memoize-map.js diff --git a/index.d.ts b/index.d.ts index aa4349c..e677b88 100644 --- a/index.d.ts +++ b/index.d.ts @@ -19,6 +19,10 @@ declare namespace assert { export function notStrictEqual(actual: any, expected: any, message?: string | Error): void + export function deepStrictEqual(actual: any, expected: any, message?: string | Error): void + + export function notDeepStrictEqual(actual: any, expected: any, message?: string | Error): void + export function match(actual: string, expected: RegExp, message?: string | Error): void export function doesNotMatch(actual: string, expected: RegExp, message?: string | Error): void diff --git a/index.js b/index.js index 077e301..46a4a2a 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ const inspect = require('bare-inspect') const getType = require('bare-type') +const MemoizeMap = require('./lib/memoize-map') class AssertionError extends Error { constructor(opts = {}) { @@ -110,18 +111,22 @@ exports.ifError = function ifError(actual) { } exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - if (deepStrictEqualValue(actual, expected)) return + const memo = new MemoizeMap() + + if (deepStrictEqualValue(actual, expected, memo)) return assertFail({ message, actual, expected, operator: 'deepStrictEqual' }, deepStrictEqual) } exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, message) { - if (!deepStrictEqualValue(actual, expected)) return + const memo = new MemoizeMap() + + if (!deepStrictEqualValue(actual, expected, memo)) return assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) } -function deepStrictEqualValue(a, b) { +function deepStrictEqualValue(a, b, memo) { const type = getType(a) if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) @@ -139,33 +144,47 @@ function deepStrictEqualValue(a, b) { if (type.isWeakMap() || type.isWeakSet() || type.isPromise()) return a === b - if (type.isArray()) return deepStrictEqualArray(a, b) - if (type.isMap()) return deepStrictEqualMap(a, b) - if (type.isSet()) return deepStrictEqualSet(a, b) if (type.isRegExp()) return deepStrictEqualRegexp(a, b) - if (type.isError()) return deepStrictEqualError(a, b) - return deepStrictEqualObject(a, b) + const memoizedResultA = memo.get(a, b) + if (memoizedResultA !== null) return memoizedResultA + + const memoizedResultB = memo.get(b, a) + if (memoizedResultB !== null) return memoizedResultB + + // Temporary value to break circular recursion + memo.set(a, b, true) + + let result + if (type.isError()) result = deepStrictEqualError(a, b, memo) + else if (type.isArray()) result = deepStrictEqualArray(a, b, memo) + else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) + else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) + else result = deepStrictEqualObject(a, b, memo) + + memo.set(a, b, result) + + return result } -function deepStrictEqualArray(a, b) { +function deepStrictEqualArray(a, b, memo) { if (a.length !== b.length) return false for (let i = 0; i < a.length; i++) { - if (!deepStrictEqualValue(a[i], b[i])) return false + if (!deepStrictEqualValue(a[i], b[i], memo)) return false } return true } -function deepStrictEqualMap(a, b) { +function deepStrictEqualMap(a, b, memo) { if (a.size !== b.size) return false const nonPrimitiveKeysEntriesFromA = [] for (const [key, value] of a) { if (getType(key).isObject()) nonPrimitiveKeysEntriesFromA.push([key, value]) - else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key))) return false + else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), memo)) return false } if (nonPrimitiveKeysEntriesFromA.length > 0) { @@ -185,7 +204,7 @@ function deepStrictEqualMap(a, b) { for (let i = 0; i < nonPrimitiveKeysEntriesFromB.length; i++) { const [keyB, valueB] = nonPrimitiveKeysEntriesFromB[i] - if (deepStrictEqualValue(keyA, keyB) && deepStrictEqualValue(valueA, valueB)) { + if (deepStrictEqualValue(keyA, keyB, memo) && deepStrictEqualValue(valueA, valueB, memo)) { nonPrimitiveKeysEntriesFromB.splice(i, 1) found = true break @@ -199,7 +218,7 @@ function deepStrictEqualMap(a, b) { return true } -function deepStrictEqualSet(a, b) { +function deepStrictEqualSet(a, b, memo) { if (a.size !== b.size) return false const nonPrimitiveItemsFromA = [] @@ -224,7 +243,7 @@ function deepStrictEqualSet(a, b) { for (let i = 0; i < nonPrimitiveItemsFromB.length; i++) { const itemB = nonPrimitiveItemsFromB[i] - if (deepStrictEqualValue(itemA, itemB)) { + if (deepStrictEqualValue(itemA, itemB, memo)) { nonPrimitiveItemsFromB.splice(i, 1) found = true break @@ -244,18 +263,20 @@ function deepStrictEqualRegexp(a, b) { return a.lastIndex === b.lastIndex && a.flags === b.flags && a.source === b.source } -function deepStrictEqualError(a, b) { - return deepStrictEqualValue(a.name, b.name) && deepStrictEqualValue(a.message, b.message) +function deepStrictEqualError(a, b, memo) { + return ( + deepStrictEqualValue(a.name, b.name, memo) && deepStrictEqualValue(a.message, b.message, memo) + ) } -function deepStrictEqualObject(a, b) { +function deepStrictEqualObject(a, b, memo) { const aKeys = [...Object.keys(a), ...Object.getOwnPropertySymbols(a)] const bKeys = [...Object.keys(b), ...Object.getOwnPropertySymbols(b)] if (aKeys.length !== bKeys.length) return false for (const key of aKeys) { - if (!deepStrictEqualValue(a[key], b[key])) return false + if (!deepStrictEqualValue(a[key], b[key], memo)) return false } return true diff --git a/lib/memoize-map.js b/lib/memoize-map.js new file mode 100644 index 0000000..3d970b6 --- /dev/null +++ b/lib/memoize-map.js @@ -0,0 +1,26 @@ +module.exports = class MemoizeMap { + constructor() { + this._map = new WeakMap() + } + + get(a, b) { + const map = this._map.get(a) + if (map === undefined) return null + + const result = map.get(b) + if (result === undefined) return null + + return result + } + + set(a, b, result) { + let map = this._map.get(a) + + if (map === undefined) { + map = new WeakMap() + this._map.set(a, map) + } + + map.set(b, result) + } +} diff --git a/package.json b/package.json index 178ac25..edc5196 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "files": [ "index.js", - "index.d.ts" + "index.d.ts", + "lib" ], "scripts": { "format": "prettier --write . && lunte --fix", diff --git a/test.js b/test.js index 37a7fb8..0639dd6 100644 --- a/test.js +++ b/test.js @@ -187,7 +187,7 @@ test('deepStrictEqual, functions', (t) => { ) }) -test.skip('deepStrictEqual, recursive self-references', (t) => { +test('deepStrictEqual, recursive self-references', (t) => { const foo = {} foo.prop = foo @@ -197,7 +197,7 @@ test.skip('deepStrictEqual, recursive self-references', (t) => { t.execution(() => assert.deepStrictEqual(foo, bar)) }) -test.skip('deepStrictEqual, recursive mutual references', (t) => { +test('deepStrictEqual, recursive mutual references', (t) => { const foo = { prop: null } const bar = { prop: foo } foo.prop = bar @@ -205,7 +205,7 @@ test.skip('deepStrictEqual, recursive mutual references', (t) => { t.execution(() => assert.deepStrictEqual(foo, bar)) }) -test.skip('deepStrictEqual, recursive lists', (t) => { +test('deepStrictEqual, recursive lists', (t) => { const foo = [] const bar = [foo] foo[0] = bar From 62915b7e8381a79e150143db5f8a50e4beff02c4 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Tue, 4 Aug 2026 18:03:43 -0300 Subject: [PATCH 03/27] Refactor `Map` and `Set` functions --- index.js | 79 ++++++++++++++------------------------------------------ 1 file changed, 19 insertions(+), 60 deletions(-) diff --git a/index.js b/index.js index 46a4a2a..5721832 100644 --- a/index.js +++ b/index.js @@ -180,80 +180,39 @@ function deepStrictEqualArray(a, b, memo) { function deepStrictEqualMap(a, b, memo) { if (a.size !== b.size) return false - const nonPrimitiveKeysEntriesFromA = [] - - for (const [key, value] of a) { - if (getType(key).isObject()) nonPrimitiveKeysEntriesFromA.push([key, value]) - else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), memo)) return false - } - - if (nonPrimitiveKeysEntriesFromA.length > 0) { - const nonPrimitiveKeysEntriesFromB = [] - - for (const [key, value] of b) { - if (getType(key).isObject()) nonPrimitiveKeysEntriesFromB.push([key, value]) - } - - if (nonPrimitiveKeysEntriesFromA.length !== nonPrimitiveKeysEntriesFromB.length) { - return false - } - - for (const [keyA, valueA] of nonPrimitiveKeysEntriesFromA) { - let found = false - - for (let i = 0; i < nonPrimitiveKeysEntriesFromB.length; i++) { - const [keyB, valueB] = nonPrimitiveKeysEntriesFromB[i] - - if (deepStrictEqualValue(keyA, keyB, memo) && deepStrictEqualValue(valueA, valueB, memo)) { - nonPrimitiveKeysEntriesFromB.splice(i, 1) - found = true - break - } - } - - if (found === false) return false - } - } - - return true + return deepStrictEqualArrayBruteForceSearch( + Array.from(a.entries()), + Array.from(b.entries()), + memo + ) } function deepStrictEqualSet(a, b, memo) { if (a.size !== b.size) return false - const nonPrimitiveItemsFromA = [] - - for (const item of a) { - if (getType(item).isObject()) nonPrimitiveItemsFromA.push(item) - else if (!b.has(item)) return false - } + return deepStrictEqualArrayBruteForceSearch(Array.from(a.keys()), Array.from(b.keys()), memo) +} - if (nonPrimitiveItemsFromA.length > 0) { - const nonPrimitiveItemsFromB = [] +function deepStrictEqualArrayBruteForceSearch(a, b, memo) { + if (a.length !== b.length) return false - for (const item of b) { - if (getType(item).isObject()) nonPrimitiveItemsFromB.push(item) - } + for (let i = 0; i < a.length; i++) { + let found = false + const itemA = a[i] - if (nonPrimitiveItemsFromA.length !== nonPrimitiveItemsFromB.length) return false + for (let j = 0; j < b.length; j++) { + const itemB = b[j] - for (const itemA of nonPrimitiveItemsFromA) { - let found = false + if (deepStrictEqualValue(itemA, itemB, memo)) { + found = true - for (let i = 0; i < nonPrimitiveItemsFromB.length; i++) { - const itemB = nonPrimitiveItemsFromB[i] + b.splice(j, 1) - if (deepStrictEqualValue(itemA, itemB, memo)) { - nonPrimitiveItemsFromB.splice(i, 1) - found = true - break - } + break } - - if (found === false) return false } - return nonPrimitiveItemsFromB.length === 0 + if (found === false) return false } return true From c191cfae136355d941e8caf5ab162775d3c8a074 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Tue, 4 Aug 2026 18:14:04 -0300 Subject: [PATCH 04/27] Update README --- README.md | 8 ++++++++ index.js | 8 ++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b7fdaeb..5230831 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,14 @@ Throws an `AssertionError` unless `actual` and `expected` are the same value, as Throws an `AssertionError` unless `actual` and `expected` are not the same value, as determined by `Object.is()`. +#### `assert.deepStrictEqual(actual, expected[, message])` + +Throws an `AssertionError` unless `actual` and `expected` are the same value, recursively. + +#### `assert.notDeepStrictEqual(actual, expected[, message])` + +Throws an `AssertionError` unless `actual` and `expected` are not the same value, recursively. + #### `assert.match(actual, regexp[, message])` Throws an `AssertionError` unless `actual` is a string that matches `regexp`. diff --git a/index.js b/index.js index 5721832..1308acf 100644 --- a/index.js +++ b/index.js @@ -111,17 +111,13 @@ exports.ifError = function ifError(actual) { } exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - const memo = new MemoizeMap() - - if (deepStrictEqualValue(actual, expected, memo)) return + if (deepStrictEqualValue(actual, expected, new MemoizeMap())) return assertFail({ message, actual, expected, operator: 'deepStrictEqual' }, deepStrictEqual) } exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, message) { - const memo = new MemoizeMap() - - if (!deepStrictEqualValue(actual, expected, memo)) return + if (!deepStrictEqualValue(actual, expected, new MemoizeMap())) return assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) } From 76fb428fadd63ade56c4053e1bf8b08377abbc58 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Wed, 5 Aug 2026 10:16:30 -0300 Subject: [PATCH 05/27] Apply suggestions from code review --- index.js | 28 +++++++++++++++------------- lib/memoize-map.js | 10 ++++------ test.js | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 1308acf..1a4f80d 100644 --- a/index.js +++ b/index.js @@ -111,42 +111,44 @@ exports.ifError = function ifError(actual) { } exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - if (deepStrictEqualValue(actual, expected, new MemoizeMap())) return + if (deepStrictEqualValue(actual, expected)) return assertFail({ message, actual, expected, operator: 'deepStrictEqual' }, deepStrictEqual) } exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, message) { - if (!deepStrictEqualValue(actual, expected, new MemoizeMap())) return + if (!deepStrictEqualValue(actual, expected)) return assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) } -function deepStrictEqualValue(a, b, memo) { +function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { const type = getType(a) if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) - if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false + const prototype = Object.getPrototypeOf(a) - const isWrapped = [ - String.prototype, - Number.prototype, - Boolean.prototype, - Date.prototype - ].includes(Object.getPrototypeOf(a)) + if (prototype !== Object.getPrototypeOf(b)) return false - if (isWrapped) return deepStrictEqualValue(a.valueOf(), b.valueOf()) + if ( + prototype === String.prototype || + prototype === Number.prototype || + prototype === Boolean.prototype || + prototype === Date.prototype + ) { + return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) + } if (type.isWeakMap() || type.isWeakSet() || type.isPromise()) return a === b if (type.isRegExp()) return deepStrictEqualRegexp(a, b) const memoizedResultA = memo.get(a, b) - if (memoizedResultA !== null) return memoizedResultA + if (memoizedResultA !== undefined) return memoizedResultA const memoizedResultB = memo.get(b, a) - if (memoizedResultB !== null) return memoizedResultB + if (memoizedResultB !== undefined) return memoizedResultB // Temporary value to break circular recursion memo.set(a, b, true) diff --git a/lib/memoize-map.js b/lib/memoize-map.js index 3d970b6..da27633 100644 --- a/lib/memoize-map.js +++ b/lib/memoize-map.js @@ -1,23 +1,21 @@ module.exports = class MemoizeMap { constructor() { - this._map = new WeakMap() + this._map = new Map() } get(a, b) { const map = this._map.get(a) - if (map === undefined) return null - const result = map.get(b) - if (result === undefined) return null + if (map === undefined) return - return result + return map.get(b) } set(a, b, result) { let map = this._map.get(a) if (map === undefined) { - map = new WeakMap() + map = new Map() this._map.set(a, map) } diff --git a/test.js b/test.js index 0639dd6..ade9a4a 100644 --- a/test.js +++ b/test.js @@ -179,7 +179,7 @@ test('deepStrictEqual, object wrappers', (t) => { ) }) -test('deepStrictEqual, functions', (t) => { +test('deepStrictEqual, errors', (t) => { t.execution(() => assert.deepStrictEqual(new Error('foo'), new Error('foo'))) t.exception( () => assert.deepStrictEqual(new Error('foo'), new Error('bar'), 'should fail'), From 1f82d508842cebc0570046cb7e552f8082838c69 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Wed, 5 Aug 2026 19:12:50 -0300 Subject: [PATCH 06/27] More tests --- index.js | 59 ++++++++++-------- test.js | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 209 insertions(+), 29 deletions(-) diff --git a/index.js b/index.js index 1a4f80d..81f0bbc 100644 --- a/index.js +++ b/index.js @@ -134,8 +134,7 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { if ( prototype === String.prototype || prototype === Number.prototype || - prototype === Boolean.prototype || - prototype === Date.prototype + prototype === Boolean.prototype ) { return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) } @@ -154,7 +153,8 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { memo.set(a, b, true) let result - if (type.isError()) result = deepStrictEqualError(a, b, memo) + if (type.isDate()) result = deepStrictEqualDate(a, b, memo) + else if (type.isError()) result = deepStrictEqualError(a, b, memo) else if (type.isArray()) result = deepStrictEqualArray(a, b, memo) else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) @@ -165,33 +165,38 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { return result } -function deepStrictEqualArray(a, b, memo) { - if (a.length !== b.length) return false - - for (let i = 0; i < a.length; i++) { - if (!deepStrictEqualValue(a[i], b[i], memo)) return false - } +function deepStrictEqualRegexp(a, b) { + return a.lastIndex === b.lastIndex && a.flags === b.flags && a.source === b.source +} - return true +function deepStrictEqualDate(a, b, memo) { + return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) && deepStrictEqualObject(a, b, memo) } -function deepStrictEqualMap(a, b, memo) { - if (a.size !== b.size) return false +function deepStrictEqualError(a, b, memo) { + const hasCause = Object.hasOwn(a, 'cause') || Object.hasOwn(b, 'cause') + const hasErrors = Object.hasOwn(a, 'errors') || Object.hasOwn(b, 'errors') - return deepStrictEqualArrayBruteForceSearch( - Array.from(a.entries()), - Array.from(b.entries()), - memo + return ( + deepStrictEqualValue(a.name, b.name, memo) && + deepStrictEqualValue(a.message, b.message, memo) && + (hasCause ? deepStrictEqualValue(a.cause, b.cause, memo) : true) && + (hasErrors ? deepStrictEqualValue(a.errors, b.errors, memo) : true) && + deepStrictEqualObject(a, b, memo) ) } -function deepStrictEqualSet(a, b, memo) { - if (a.size !== b.size) return false +function deepStrictEqualArray(a, b, memo) { + if (a.length !== b.length) return false + + for (let i = 0; i < a.length; i++) { + if (!deepStrictEqualValue(a[i], b[i], memo)) return false + } - return deepStrictEqualArrayBruteForceSearch(Array.from(a.keys()), Array.from(b.keys()), memo) + return true } -function deepStrictEqualArrayBruteForceSearch(a, b, memo) { +function deepStrictEqualArrayUnordered(a, b, memo) { if (a.length !== b.length) return false for (let i = 0; i < a.length; i++) { @@ -216,14 +221,16 @@ function deepStrictEqualArrayBruteForceSearch(a, b, memo) { return true } -function deepStrictEqualRegexp(a, b) { - return a.lastIndex === b.lastIndex && a.flags === b.flags && a.source === b.source +function deepStrictEqualMap(a, b, memo) { + if (a.size !== b.size) return false + + return deepStrictEqualArrayUnordered(Array.from(a.entries()), Array.from(b.entries()), memo) } -function deepStrictEqualError(a, b, memo) { - return ( - deepStrictEqualValue(a.name, b.name, memo) && deepStrictEqualValue(a.message, b.message, memo) - ) +function deepStrictEqualSet(a, b, memo) { + if (a.size !== b.size) return false + + return deepStrictEqualArrayUnordered(Array.from(a.keys()), Array.from(b.keys()), memo) } function deepStrictEqualObject(a, b, memo) { diff --git a/test.js b/test.js index ade9a4a..a7cf2d0 100644 --- a/test.js +++ b/test.js @@ -69,8 +69,10 @@ test('deepStrictEqual, basic', (t) => { test('deepStrictEqual, array', (t) => { t.execution(() => assert.deepStrictEqual([1, 'foo'], [1, 'foo'])) + t.execution(() => assert.deepStrictEqual([1, , , 3], [1, , , 3])) t.exception(() => assert.deepStrictEqual([1, 'foo'], [1], 'should fail'), /should fail/) t.exception(() => assert.deepStrictEqual([1, 'foo'], [1, 'bar'], 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual([1, , , 3], [1, , , 3, ,], 'should fail'), /should fail/) }) test('deepStrictEqual, object', (t) => { @@ -84,6 +86,32 @@ test('deepStrictEqual, object', (t) => { t.exception(() => assert.deepStrictEqual({ a: [1, 2] }, { a: [1] }, 'should fail'), /should fail/) }) +test('deepStrictEqual, object, getter', (t) => { + const obj = { + get foo() { + return 'bar' + } + } + + t.execution(() => assert.deepStrictEqual(obj, { foo: 'bar' })) + t.exception(() => assert.deepStrictEqual(obj, { foo: 'baz' }, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, object, prototype', (t) => { + const prototype = { __proto__: null } + const a = { constructor: 42, foo: 'bar' } + const b = { constructor: 42, foo: 'bar' } + + Object.setPrototypeOf(a, prototype) + Object.setPrototypeOf(b, prototype) + + t.execution(() => assert.deepStrictEqual(a, b)) + + Object.setPrototypeOf(b, { __proto__: null }) + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + test('deepStrictEqual, regexp', (t) => { t.execution(() => assert.deepStrictEqual(/abc/, /abc/)) t.exception(() => assert.deepStrictEqual(/abc/, /abc/g, 'should fail'), /should fail/) @@ -153,7 +181,7 @@ test('deepStrictEqual, weak set', (t) => { t.exception(() => assert.deepStrictEqual(set1, set2, 'should fail'), /should fail/) }) -test('deepStrictEqual, symbols', (t) => { +test('deepStrictEqual, symbol', (t) => { t.execution(() => assert.deepStrictEqual(Symbol.for('foo'), Symbol.for('foo'))) t.exception( () => assert.deepStrictEqual(Symbol.for('foo'), Symbol.for('bar'), 'should fail'), @@ -170,21 +198,166 @@ test('deepStrictEqual, symbols', (t) => { ) }) -test('deepStrictEqual, object wrappers', (t) => { +test('deepStrictEqual, boxed value', (t) => { + const boxedSymbol = Object(Symbol()) + t.execution(() => assert.deepStrictEqual(new String('foo'), Object('foo'))) t.execution(() => assert.deepStrictEqual(new Number(1), new Number(1))) + t.execution(() => assert.deepStrictEqual(boxedSymbol, boxedSymbol)) + t.exception( + () => assert.deepStrictEqual(new Boolean(true), Object(false), 'should fail'), + /should fail/ + ) t.exception( () => assert.deepStrictEqual(new Number(1), new Number(2), 'should fail'), /should fail/ ) }) -test('deepStrictEqual, errors', (t) => { +test('deepStrictEqual, date', (t) => { + t.execution(() => assert.deepStrictEqual(new Date(2000, 3, 14), new Date(2000, 3, 14))) + t.exception( + () => assert.deepStrictEqual(new Date(), new Date(2000, 3, 14), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, date, additional property', (t) => { + const date1 = new Date('foo') + const date2 = new Date('bar') + + date1.foo = true + date2.foo = true + + t.execution(() => assert.deepStrictEqual(date1, date2)) + + date2.foo = false + + t.exception(() => assert.deepStrictEqual(date1, date2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, error', (t) => { t.execution(() => assert.deepStrictEqual(new Error('foo'), new Error('foo'))) t.exception( () => assert.deepStrictEqual(new Error('foo'), new Error('bar'), 'should fail'), /should fail/ ) + t.exception( + () => assert.deepStrictEqual(new Error('foo'), new TypeError('foo'), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, error, cause property', (t) => { + t.execution(() => + assert.deepStrictEqual( + new Error('err', { cause: new Error('foo') }), + new Error('err', { cause: new Error('foo') }) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new Error('err', { cause: new Error('foo') }), + new Error('err', { cause: new Error('bar') }), + 'should fail' + ), + /should fail/ + ) + t.exception( + () => + assert.deepStrictEqual( + new Error('err', { cause: new Error('foo') }), + new Error('err'), + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, error, aggregate error', (t) => { + t.execution(() => + assert.deepStrictEqual( + new AggregateError([new Error('foo'), new Error('bar')]), + new AggregateError([new Error('foo'), new Error('bar')]) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new AggregateError([new Error('foo'), new Error('bar')]), + new AggregateError([new Error('foo'), new Error('baz')]), + 'should fail' + ), + /should fail/ + ) +}) + +test('deepStrictEqual, error, additional property', (t) => { + const error1 = new Error('foo') + const error2 = new Error('foo') + + error1.foo = true + error2.foo = true + + t.execution(() => assert.deepStrictEqual(error1, error2)) + + error2.foo = false + + t.exception(() => assert.deepStrictEqual(error1, error2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, error, custom toStringTag', (t) => { + const error = new Error('foo') + + error[Symbol.toStringTag] = 'CustomTag' + + t.exception(() => assert.deepStrictEqual(error, new Error('foo'), 'should fail'), /should fail/) +}) + +test('deepStrictEqual, buffer', (t) => { + t.execution(() => assert.deepStrictEqual(Buffer.from('foo'), Buffer.from('foo'))) + t.exception( + () => assert.deepStrictEqual(Buffer.from('foo'), Buffer.from('bar'), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, promise', (t) => { + const promise1 = Promise.resolve(1) + const promise2 = Promise.resolve(1) + + t.execution(() => assert.deepStrictEqual(promise1, promise1)) + t.exception(() => assert.deepStrictEqual(promise1, promise2, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, proxy', (t) => { + const proxy = new Proxy([1, 2], {}) + + t.execution(() => assert.deepStrictEqual(proxy, [1, 2])) + t.exception(() => assert.deepStrictEqual(proxy, [1, 1], 'should fail'), /should fail/) +}) + +test('deepStrictEqual, url', (t) => { + t.execution(() => assert.deepStrictEqual(new URL('http://foo'), new URL('http://foo'))) + t.exception( + () => assert.deepStrictEqual(new URL('http://foo'), new URL('http://bar'), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, url, additional property', (t) => { + const url1 = new URL('http://foo') + const url2 = new URL('http://foo') + + url1.foo = true + url2.foo = true + + t.execution(() => assert.deepStrictEqual(url1, url2)) + + url2.foo = false + + t.exception(() => assert.deepStrictEqual(url1, url2, 'should fail'), /should fail/) }) test('deepStrictEqual, recursive self-references', (t) => { From 0bb9f07fff71d08477f4dbf8b49e88c2c384a598 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Thu, 6 Aug 2026 14:57:32 -0300 Subject: [PATCH 07/27] Cover a few more cases --- index.js | 32 ++++++--- lib/memoize-map.js | 10 ++- test.js | 163 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 174 insertions(+), 31 deletions(-) diff --git a/index.js b/index.js index 81f0bbc..650f83c 100644 --- a/index.js +++ b/index.js @@ -132,9 +132,11 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { if (prototype !== Object.getPrototypeOf(b)) return false if ( - prototype === String.prototype || + prototype === BigInt.prototype || + prototype === Boolean.prototype || prototype === Number.prototype || - prototype === Boolean.prototype + prototype === String.prototype || + prototype === Symbol.prototype ) { return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) } @@ -143,19 +145,27 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { if (type.isRegExp()) return deepStrictEqualRegexp(a, b) - const memoizedResultA = memo.get(a, b) - if (memoizedResultA !== undefined) return memoizedResultA + if (Buffer.isBuffer(a)) return deepStrictEqualBuffer(a, b) + if (type.isTypedArray()) return deepStrictEqualBuffer(a, b) + if (type.isArrayBuffer()) return deepStrictEqualBuffer(new Uint8Array(a), new Uint8Array(b)) + if (type.isDataView()) { + return deepStrictEqualBuffer( + new Uint8Array(a.buffer, a.byteOffset, a.byteLength), + new Uint8Array(b.buffer, b.byteOffset, b.byteLength) + ) + } - const memoizedResultB = memo.get(b, a) - if (memoizedResultB !== undefined) return memoizedResultB + if (memo.has(a, b)) return memo.get(a, b) + if (memo.has(b, a)) return memo.get(b, a) // Temporary value to break circular recursion memo.set(a, b, true) let result + if (type.isDate()) result = deepStrictEqualDate(a, b, memo) else if (type.isError()) result = deepStrictEqualError(a, b, memo) - else if (type.isArray()) result = deepStrictEqualArray(a, b, memo) + else if (type.isArguments() || type.isArray()) result = deepStrictEqualArray(a, b, memo) else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) else result = deepStrictEqualObject(a, b, memo) @@ -169,8 +179,12 @@ function deepStrictEqualRegexp(a, b) { return a.lastIndex === b.lastIndex && a.flags === b.flags && a.source === b.source } +function deepStrictEqualBuffer(a, b) { + return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 +} + function deepStrictEqualDate(a, b, memo) { - return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) && deepStrictEqualObject(a, b, memo) + return Object.is(a.getTime(), b.getTime()) && deepStrictEqualObject(a, b, memo) } function deepStrictEqualError(a, b, memo) { @@ -240,7 +254,7 @@ function deepStrictEqualObject(a, b, memo) { if (aKeys.length !== bKeys.length) return false for (const key of aKeys) { - if (!deepStrictEqualValue(a[key], b[key], memo)) return false + if (!(key in b) || !deepStrictEqualValue(a[key], b[key], memo)) return false } return true diff --git a/lib/memoize-map.js b/lib/memoize-map.js index da27633..e885c64 100644 --- a/lib/memoize-map.js +++ b/lib/memoize-map.js @@ -3,12 +3,16 @@ module.exports = class MemoizeMap { this._map = new Map() } - get(a, b) { + has(a, b) { const map = this._map.get(a) - if (map === undefined) return + if (map === undefined) return false + + return map.has(b) + } - return map.get(b) + get(a, b) { + return this._map.get(a).get(b) } set(a, b, result) { diff --git a/test.js b/test.js index a7cf2d0..2c2dada 100644 --- a/test.js +++ b/test.js @@ -86,6 +86,20 @@ test('deepStrictEqual, object', (t) => { t.exception(() => assert.deepStrictEqual({ a: [1, 2] }, { a: [1] }, 'should fail'), /should fail/) }) +test('deepStrictEqual, class', (t) => { + class MyClass { + constructor(value) { + this.value = value + } + } + + t.execution(() => assert.deepStrictEqual(new MyClass('foo'), new MyClass('foo'))) + t.exception( + () => assert.deepStrictEqual(new MyClass('foo'), new MyClass('bar'), 'should fail'), + /should fail/ + ) +}) + test('deepStrictEqual, object, getter', (t) => { const obj = { get foo() { @@ -201,15 +215,24 @@ test('deepStrictEqual, symbol', (t) => { test('deepStrictEqual, boxed value', (t) => { const boxedSymbol = Object(Symbol()) - t.execution(() => assert.deepStrictEqual(new String('foo'), Object('foo'))) t.execution(() => assert.deepStrictEqual(new Number(1), new Number(1))) - t.execution(() => assert.deepStrictEqual(boxedSymbol, boxedSymbol)) + t.exception( + () => assert.deepStrictEqual(new Number(1), new Number(2), 'should fail'), + /should fail/ + ) + + t.execution(() => assert.deepStrictEqual(new String('foo'), Object('foo'))) t.exception( () => assert.deepStrictEqual(new Boolean(true), Object(false), 'should fail'), /should fail/ ) + + t.execution(() => assert.deepStrictEqual(Object(1n), Object(1n))) + t.exception(() => assert.deepStrictEqual(Object(1n), Object(2n), 'should fail'), /should fail/) + + t.execution(() => assert.deepStrictEqual(boxedSymbol, boxedSymbol)) t.exception( - () => assert.deepStrictEqual(new Number(1), new Number(2), 'should fail'), + () => assert.deepStrictEqual(boxedSymbol, Object(Symbol()), 'should fail'), /should fail/ ) }) @@ -323,6 +346,38 @@ test('deepStrictEqual, buffer', (t) => { ) }) +test('deepStrictEqual, arraybuffer', (t) => { + t.execution(() => assert.deepStrictEqual(new ArrayBuffer(8), new ArrayBuffer(8))) + t.exception( + () => assert.deepStrictEqual(new ArrayBuffer(10), new ArrayBuffer(12), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, typed array', (t) => { + t.execution(() => assert.deepStrictEqual(new Uint16Array([21, 31]), new Uint16Array([21, 31]))) + t.exception( + () => + assert.deepStrictEqual(new Uint16Array([21, 31]), new Uint16Array([31, 21]), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, dataview', (t) => { + t.execution(() => + assert.deepStrictEqual(new DataView(new ArrayBuffer(10)), new DataView(new ArrayBuffer(10))) + ) + t.exception( + () => + assert.deepStrictEqual( + new DataView(new ArrayBuffer(10)), + new DataView(new ArrayBuffer(12)), + 'should fail' + ), + /should fail/ + ) +}) + test('deepStrictEqual, promise', (t) => { const promise1 = Promise.resolve(1) const promise2 = Promise.resolve(1) @@ -360,28 +415,98 @@ test('deepStrictEqual, url, additional property', (t) => { t.exception(() => assert.deepStrictEqual(url1, url2, 'should fail'), /should fail/) }) -test('deepStrictEqual, recursive self-references', (t) => { - const foo = {} - foo.prop = foo +test('deepStrictEqual, recursive object', (t) => { + { + const a = {} + a.prop = a + + const b = {} + b.prop = b + + assert.deepStrictEqual(a, b) + } + + { + const a = { prop: null } + const b = { prop: a } + a.prop = b + + t.execution(() => assert.deepStrictEqual(a, b)) + } - const bar = {} - bar.prop = bar + { + const a = {} + a.prop = 'foo' - t.execution(() => assert.deepStrictEqual(foo, bar)) + const b = {} + b.prop = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + } + + /* + { + const a = {} + a.prop = a + + const b = {} + b.prop = {} + b.prop.prop = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + } + + { + const a = {} + a.prop = a + + const b = {} + b.prop = b + + const c = {} + c.prop = a + + t.exception(() => assert.deepStrictEqual(b, c, 'should fail'), /should fail/) + } + */ }) -test('deepStrictEqual, recursive mutual references', (t) => { - const foo = { prop: null } - const bar = { prop: foo } - foo.prop = bar +test('deepStrictEqual, recursive array', (t) => { + const a = [] + const b = [a] + a[0] = b - t.execution(() => assert.deepStrictEqual(foo, bar)) + t.execution(() => assert.deepStrictEqual(a, b)) }) -test('deepStrictEqual, recursive lists', (t) => { - const foo = [] - const bar = [foo] - foo[0] = bar +test('deepStrictEqual, recursive map', (t) => { + { + const a = new Map() + a.set('prop', a) + + const b = new Map() + b.set('prop', b) + + t.execution(() => assert.deepStrictEqual(a, b)) + } - t.execution(() => assert.deepStrictEqual(foo, bar)) + { + const a = new Map() + a.set(a, 'value') + + const b = new Map() + b.set(b, 'value') + + t.execution(() => assert.deepStrictEqual(a, b)) + } +}) + +test('deepStrictEqual, recursive set', (t) => { + const a = new Set() + a.add(a) + + const b = new Set() + b.add(b) + + t.execution(() => assert.deepStrictEqual(a, b)) }) From 5b3da3b9b83963ba4740a2f3f1e9a385ce5104b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 10 Aug 2026 09:35:36 +0200 Subject: [PATCH 08/27] Keep `Map` and `Set` equality linear in common case --- index.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 650f83c..ed5fc0f 100644 --- a/index.js +++ b/index.js @@ -235,16 +235,60 @@ function deepStrictEqualArrayUnordered(a, b, memo) { return true } +// A key can be matched through a native `Map`/`Set` lookup only when it is a +// primitive whose deep equality collapses to the `SameValueZero` relation those +// lookups use. Objects and functions must be matched by deep comparison, and so +// must `+0` and `-0`: `SameValueZero` treats them as equal, but +// `deepStrictEqual` keeps their signs distinct. +function requiresDeepKeyMatch(key) { + if (key === 0) return true // Covers both `+0` and `-0`. + + const type = typeof key + + return (type === 'object' && key !== null) || type === 'function' +} + function deepStrictEqualMap(a, b, memo) { if (a.size !== b.size) return false - return deepStrictEqualArrayUnordered(Array.from(a.entries()), Array.from(b.entries()), memo) + // Match entries with primitive keys directly through `b` in linear time and + // leave only the object-keyed entries for the quadratic fallback. + const restA = [] + const restB = [] + + for (const [key, value] of a) { + if (requiresDeepKeyMatch(key)) { + restA.push([key, value]) + } else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), memo)) { + return false + } + } + + for (const entry of b) { + if (requiresDeepKeyMatch(entry[0])) restB.push(entry) + } + + return deepStrictEqualArrayUnordered(restA, restB, memo) } function deepStrictEqualSet(a, b, memo) { if (a.size !== b.size) return false - return deepStrictEqualArrayUnordered(Array.from(a.keys()), Array.from(b.keys()), memo) + // Match primitive members directly through `b` in linear time and leave only + // the object members for the quadratic fallback. + const restA = [] + const restB = [] + + for (const value of a) { + if (requiresDeepKeyMatch(value)) restA.push(value) + else if (!b.has(value)) return false + } + + for (const value of b) { + if (requiresDeepKeyMatch(value)) restB.push(value) + } + + return deepStrictEqualArrayUnordered(restA, restB, memo) } function deepStrictEqualObject(a, b, memo) { From 0f2fa87972917b3abbf0bb78fb35b7edff701321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Mon, 10 Aug 2026 10:12:10 +0200 Subject: [PATCH 09/27] Add additional test cases --- test.js | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/test.js b/test.js index 2c2dada..99ac3a0 100644 --- a/test.js +++ b/test.js @@ -67,6 +67,14 @@ test('deepStrictEqual, basic', (t) => { t.exception(() => assert.deepStrictEqual(1, new Date(), 'should fail'), /should fail/) }) +test('deepStrictEqual, negative zero', (t) => { + t.exception(() => assert.deepStrictEqual(-0, 0, 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual([-0], [0], 'should fail'), /should fail/) + + t.execution(() => assert.deepStrictEqual(new Set([-0]), new Set([0]))) + t.execution(() => assert.deepStrictEqual(new Map([[-0, 1]]), new Map([[0, 1]]))) +}) + test('deepStrictEqual, array', (t) => { t.execution(() => assert.deepStrictEqual([1, 'foo'], [1, 'foo'])) t.execution(() => assert.deepStrictEqual([1, , , 3], [1, , , 3])) @@ -75,6 +83,36 @@ test('deepStrictEqual, array', (t) => { t.exception(() => assert.deepStrictEqual([1, , , 3], [1, , , 3, ,], 'should fail'), /should fail/) }) +test('deepStrictEqual, array, hole vs explicit undefined', (t) => { + t.exception( + () => assert.deepStrictEqual([1, , 3], [1, undefined, 3], 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, array, additional property', (t) => { + const a = [1, 2] + const b = [1, 2] + + a.foo = 'x' + b.foo = 'y' + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + + const c = [1, 2] + c.foo = 'x' + + t.exception(() => assert.deepStrictEqual(c, [1, 2], 'should fail'), /should fail/) +}) + +test('deepStrictEqual, arguments vs array', (t) => { + function make() { + return arguments + } + + t.exception(() => assert.deepStrictEqual(make(1, 2, 3), [1, 2, 3], 'should fail'), /should fail/) +}) + test('deepStrictEqual, object', (t) => { t.execution(() => assert.deepStrictEqual({}, {})) t.execution(() => assert.deepStrictEqual({ a: { b: 1 } }, { a: { b: 1 } })) @@ -86,6 +124,32 @@ test('deepStrictEqual, object', (t) => { t.exception(() => assert.deepStrictEqual({ a: [1, 2] }, { a: [1] }, 'should fail'), /should fail/) }) +test('deepStrictEqual, object, key order', (t) => { + t.execution(() => assert.deepStrictEqual({ a: 1, b: 2 }, { b: 2, a: 1 })) +}) + +test('deepStrictEqual, object, non-enumerable property', (t) => { + const a = {} + const b = {} + + Object.defineProperty(a, 'x', { value: 1, enumerable: false }) + Object.defineProperty(b, 'x', { value: 2, enumerable: false }) + + t.execution(() => assert.deepStrictEqual(a, b)) +}) + +test('deepStrictEqual, object, non-enumerable symbol', (t) => { + const symbol = Symbol('symbol') + + const a = {} + const b = {} + + Object.defineProperty(a, symbol, { value: 1, enumerable: false }) + Object.defineProperty(b, symbol, { value: 2, enumerable: false }) + + t.execution(() => assert.deepStrictEqual(a, b)) +}) + test('deepStrictEqual, class', (t) => { class MyClass { constructor(value) { @@ -126,11 +190,25 @@ test('deepStrictEqual, object, prototype', (t) => { t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) }) +test('deepStrictEqual, null prototype', (t) => { + t.exception(() => assert.deepStrictEqual({}, Object.create(null), 'should fail'), /should fail/) +}) + test('deepStrictEqual, regexp', (t) => { t.execution(() => assert.deepStrictEqual(/abc/, /abc/)) t.exception(() => assert.deepStrictEqual(/abc/, /abc/g, 'should fail'), /should fail/) }) +test('deepStrictEqual, regexp, additional property', (t) => { + const a = /x/ + const b = /x/ + + a.foo = 1 + b.foo = 2 + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + test('deepStrictEqual, map', (t) => { t.execution(() => assert.deepStrictEqual( @@ -237,6 +315,20 @@ test('deepStrictEqual, boxed value', (t) => { ) }) +test('deepStrictEqual, boxed value, additional property', (t) => { + const a = new Number(1) + const b = new Number(1) + + a.foo = 'x' + b.foo = 'y' + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, boxed value vs primitive', (t) => { + t.exception(() => assert.deepStrictEqual(new Number(1), 1, 'should fail'), /should fail/) +}) + test('deepStrictEqual, date', (t) => { t.execution(() => assert.deepStrictEqual(new Date(2000, 3, 14), new Date(2000, 3, 14))) t.exception( @@ -259,6 +351,10 @@ test('deepStrictEqual, date, additional property', (t) => { t.exception(() => assert.deepStrictEqual(date1, date2, 'should fail'), /should fail/) }) +test('deepStrictEqual, date, invalid', (t) => { + t.execution(() => assert.deepStrictEqual(new Date(NaN), new Date(NaN))) +}) + test('deepStrictEqual, error', (t) => { t.execution(() => assert.deepStrictEqual(new Error('foo'), new Error('foo'))) t.exception( @@ -363,6 +459,27 @@ test('deepStrictEqual, typed array', (t) => { ) }) +test('deepStrictEqual, typed array, additional property', (t) => { + const a = new Uint8Array([1]) + const b = new Uint8Array([1]) + + a.foo = 1 + b.foo = 2 + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, typed array, differing type', (t) => { + t.exception( + () => assert.deepStrictEqual(new Uint8Array([1, 2]), new Int8Array([1, 2]), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(new Uint8Array([1, 2, 3]), Buffer.from([1, 2, 3]), 'should fail'), + /should fail/ + ) +}) + test('deepStrictEqual, dataview', (t) => { t.execution(() => assert.deepStrictEqual(new DataView(new ArrayBuffer(10)), new DataView(new ArrayBuffer(10))) @@ -386,6 +503,21 @@ test('deepStrictEqual, promise', (t) => { t.exception(() => assert.deepStrictEqual(promise1, promise2, 'should fail'), /should fail/) }) +test('deepStrictEqual, function', (t) => { + const fn = () => {} + + t.execution(() => assert.deepStrictEqual(fn, fn)) + t.exception( + () => + assert.deepStrictEqual( + () => {}, + () => {}, + 'should fail' + ), + /should fail/ + ) +}) + test('deepStrictEqual, proxy', (t) => { const proxy = new Proxy([1, 2], {}) @@ -444,7 +576,6 @@ test('deepStrictEqual, recursive object', (t) => { t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) } - /* { const a = {} a.prop = a @@ -468,7 +599,6 @@ test('deepStrictEqual, recursive object', (t) => { t.exception(() => assert.deepStrictEqual(b, c, 'should fail'), /should fail/) } - */ }) test('deepStrictEqual, recursive array', (t) => { From d1976ddd7bcd4abcc5aba18bb1a117b464988340 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Mon, 10 Aug 2026 15:08:59 -0300 Subject: [PATCH 10/27] Address part of the additional tests --- index.js | 64 ++++++++++++++++++++++++++++++++++---------------------- test.js | 7 ++++--- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/index.js b/index.js index ed5fc0f..bb83f8a 100644 --- a/index.js +++ b/index.js @@ -131,22 +131,9 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { if (prototype !== Object.getPrototypeOf(b)) return false - if ( - prototype === BigInt.prototype || - prototype === Boolean.prototype || - prototype === Number.prototype || - prototype === String.prototype || - prototype === Symbol.prototype - ) { - return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) - } - if (type.isWeakMap() || type.isWeakSet() || type.isPromise()) return a === b - if (type.isRegExp()) return deepStrictEqualRegexp(a, b) - if (Buffer.isBuffer(a)) return deepStrictEqualBuffer(a, b) - if (type.isTypedArray()) return deepStrictEqualBuffer(a, b) if (type.isArrayBuffer()) return deepStrictEqualBuffer(new Uint8Array(a), new Uint8Array(b)) if (type.isDataView()) { return deepStrictEqualBuffer( @@ -163,7 +150,17 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { let result - if (type.isDate()) result = deepStrictEqualDate(a, b, memo) + if ( + prototype === BigInt.prototype || + prototype === Boolean.prototype || + prototype === Number.prototype || + prototype === String.prototype || + prototype === Symbol.prototype + ) + result = deepStrictEqualBoxedValue(a, b, memo) + else if (type.isRegExp()) result = deepStrictEqualRegexp(a, b, memo) + else if (type.isTypedArray()) result = deepStrictEqualTypedArray(a, b, memo) + else if (type.isDate()) result = deepStrictEqualDate(a, b, memo) else if (type.isError()) result = deepStrictEqualError(a, b, memo) else if (type.isArguments() || type.isArray()) result = deepStrictEqualArray(a, b, memo) else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) @@ -175,8 +172,17 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { return result } -function deepStrictEqualRegexp(a, b) { - return a.lastIndex === b.lastIndex && a.flags === b.flags && a.source === b.source +function deepStrictEqualBoxedValue(a, b, memo) { + return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) && deepStrictEqualObject(a, b, memo) +} + +function deepStrictEqualRegexp(a, b, memo) { + return ( + a.lastIndex === b.lastIndex && + a.flags === b.flags && + a.source === b.source && + deepStrictEqualObject(a, b, memo) + ) } function deepStrictEqualBuffer(a, b) { @@ -201,13 +207,11 @@ function deepStrictEqualError(a, b, memo) { } function deepStrictEqualArray(a, b, memo) { - if (a.length !== b.length) return false - - for (let i = 0; i < a.length; i++) { - if (!deepStrictEqualValue(a[i], b[i], memo)) return false - } + return a.length === b.length && deepStrictEqualObject(a, b, memo) +} - return true +function deepStrictEqualTypedArray(a, b, memo) { + return deepStrictEqualBuffer(a, b) && deepStrictEqualObject(a, b, memo) } function deepStrictEqualArrayUnordered(a, b, memo) { @@ -292,10 +296,20 @@ function deepStrictEqualSet(a, b, memo) { } function deepStrictEqualObject(a, b, memo) { - const aKeys = [...Object.keys(a), ...Object.getOwnPropertySymbols(a)] - const bKeys = [...Object.keys(b), ...Object.getOwnPropertySymbols(b)] + function getKeys(obj) { + const keys = Object.keys(obj) + + for (const symbolKey of Object.getOwnPropertySymbols(obj)) { + const { enumerable } = Object.getOwnPropertyDescriptor(obj, symbolKey) + if (enumerable) keys.push(symbolKey) + } + + return keys + } + + const aKeys = getKeys(a) - if (aKeys.length !== bKeys.length) return false + if (aKeys.length !== getKeys(b).length) return false for (const key of aKeys) { if (!(key in b) || !deepStrictEqualValue(a[key], b[key], memo)) return false diff --git a/test.js b/test.js index 99ac3a0..0d85502 100644 --- a/test.js +++ b/test.js @@ -81,13 +81,14 @@ test('deepStrictEqual, array', (t) => { t.exception(() => assert.deepStrictEqual([1, 'foo'], [1], 'should fail'), /should fail/) t.exception(() => assert.deepStrictEqual([1, 'foo'], [1, 'bar'], 'should fail'), /should fail/) t.exception(() => assert.deepStrictEqual([1, , , 3], [1, , , 3, ,], 'should fail'), /should fail/) -}) - -test('deepStrictEqual, array, hole vs explicit undefined', (t) => { t.exception( () => assert.deepStrictEqual([1, , 3], [1, undefined, 3], 'should fail'), /should fail/ ) + t.exception( + () => assert.deepStrictEqual([1, , undefined, 3], [1, undefined, , 3], 'should fail'), + /should fail/ + ) }) test('deepStrictEqual, array, additional property', (t) => { From 9a7cc33b3a8ef73c17e1e7b2e819a90802ea8037 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Mon, 10 Aug 2026 18:53:35 -0300 Subject: [PATCH 11/27] Enhance memoization algorithm --- index.js | 89 ++++++++++++++++++++++++---------------------- lib/memoize-map.js | 74 ++++++++++++++++++++++++++++++++------ test.js | 2 +- 3 files changed, 110 insertions(+), 55 deletions(-) diff --git a/index.js b/index.js index bb83f8a..8a0a214 100644 --- a/index.js +++ b/index.js @@ -122,7 +122,7 @@ exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, messa assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) } -function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { +function deepStrictEqualValue(a, b, depth = 0, memo = new MemoizeMap()) { const type = getType(a) if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) @@ -145,8 +145,8 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { if (memo.has(a, b)) return memo.get(a, b) if (memo.has(b, a)) return memo.get(b, a) - // Temporary value to break circular recursion - memo.set(a, b, true) + // Do not pass the result value, the MemoizeMap will use a 'pending' value. + memo.set(a, b, depth) let result @@ -157,64 +157,67 @@ function deepStrictEqualValue(a, b, memo = new MemoizeMap()) { prototype === String.prototype || prototype === Symbol.prototype ) - result = deepStrictEqualBoxedValue(a, b, memo) - else if (type.isRegExp()) result = deepStrictEqualRegexp(a, b, memo) - else if (type.isTypedArray()) result = deepStrictEqualTypedArray(a, b, memo) - else if (type.isDate()) result = deepStrictEqualDate(a, b, memo) - else if (type.isError()) result = deepStrictEqualError(a, b, memo) - else if (type.isArguments() || type.isArray()) result = deepStrictEqualArray(a, b, memo) - else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) - else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) - else result = deepStrictEqualObject(a, b, memo) - - memo.set(a, b, result) + result = deepStrictEqualBoxedValue(a, b, depth, memo) + else if (type.isRegExp()) result = deepStrictEqualRegexp(a, b, depth, memo) + else if (type.isTypedArray()) result = deepStrictEqualTypedArray(a, b, depth, memo) + else if (type.isDate()) result = deepStrictEqualDate(a, b, depth, memo) + else if (type.isError()) result = deepStrictEqualError(a, b, depth, memo) + else if (type.isArguments() || type.isArray()) result = deepStrictEqualArray(a, b, depth, memo) + else if (type.isMap()) result = deepStrictEqualMap(a, b, depth, memo) + else if (type.isSet()) result = deepStrictEqualSet(a, b, depth, memo) + else result = deepStrictEqualObject(a, b, depth, memo) + + memo.set(a, b, depth, result) return result } -function deepStrictEqualBoxedValue(a, b, memo) { - return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) && deepStrictEqualObject(a, b, memo) +function deepStrictEqualBuffer(a, b) { + return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 +} + +function deepStrictEqualBoxedValue(a, b, depth, memo) { + return ( + deepStrictEqualValue(a.valueOf(), b.valueOf(), depth, memo) && + deepStrictEqualObject(a, b, depth, memo) + ) } -function deepStrictEqualRegexp(a, b, memo) { +function deepStrictEqualRegexp(a, b, depth, memo) { return ( a.lastIndex === b.lastIndex && a.flags === b.flags && a.source === b.source && - deepStrictEqualObject(a, b, memo) + deepStrictEqualObject(a, b, depth, memo) ) } -function deepStrictEqualBuffer(a, b) { - return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 -} - -function deepStrictEqualDate(a, b, memo) { - return Object.is(a.getTime(), b.getTime()) && deepStrictEqualObject(a, b, memo) +function deepStrictEqualDate(a, b, depth, memo) { + return Object.is(a.getTime(), b.getTime()) && deepStrictEqualObject(a, b, depth, memo) } -function deepStrictEqualError(a, b, memo) { +function deepStrictEqualError(a, b, depth, memo) { const hasCause = Object.hasOwn(a, 'cause') || Object.hasOwn(b, 'cause') const hasErrors = Object.hasOwn(a, 'errors') || Object.hasOwn(b, 'errors') return ( - deepStrictEqualValue(a.name, b.name, memo) && - deepStrictEqualValue(a.message, b.message, memo) && - (hasCause ? deepStrictEqualValue(a.cause, b.cause, memo) : true) && - (hasErrors ? deepStrictEqualValue(a.errors, b.errors, memo) : true) && - deepStrictEqualObject(a, b, memo) + deepStrictEqualValue(a.name, b.name, depth, memo) && + deepStrictEqualValue(a.message, b.message, depth, memo) && + (hasCause ? deepStrictEqualValue(a.cause, b.cause, depth, memo) : true) && + (hasErrors ? deepStrictEqualValue(a.errors, b.errors, depth, memo) : true) && + deepStrictEqualObject(a, b, depth, memo) ) } -function deepStrictEqualArray(a, b, memo) { - return a.length === b.length && deepStrictEqualObject(a, b, memo) +function deepStrictEqualArray(a, b, depth, memo) { + return a.length === b.length && deepStrictEqualObject(a, b, depth, memo) } -function deepStrictEqualTypedArray(a, b, memo) { - return deepStrictEqualBuffer(a, b) && deepStrictEqualObject(a, b, memo) +function deepStrictEqualTypedArray(a, b, depth, memo) { + return deepStrictEqualBuffer(a, b) && deepStrictEqualObject(a, b, depth, memo) } -function deepStrictEqualArrayUnordered(a, b, memo) { +function deepStrictEqualArrayUnordered(a, b, depth, memo) { if (a.length !== b.length) return false for (let i = 0; i < a.length; i++) { @@ -224,7 +227,7 @@ function deepStrictEqualArrayUnordered(a, b, memo) { for (let j = 0; j < b.length; j++) { const itemB = b[j] - if (deepStrictEqualValue(itemA, itemB, memo)) { + if (deepStrictEqualValue(itemA, itemB, depth + 1, memo)) { found = true b.splice(j, 1) @@ -252,7 +255,7 @@ function requiresDeepKeyMatch(key) { return (type === 'object' && key !== null) || type === 'function' } -function deepStrictEqualMap(a, b, memo) { +function deepStrictEqualMap(a, b, depth, memo) { if (a.size !== b.size) return false // Match entries with primitive keys directly through `b` in linear time and @@ -263,7 +266,7 @@ function deepStrictEqualMap(a, b, memo) { for (const [key, value] of a) { if (requiresDeepKeyMatch(key)) { restA.push([key, value]) - } else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), memo)) { + } else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), depth + 1, memo)) { return false } } @@ -272,10 +275,10 @@ function deepStrictEqualMap(a, b, memo) { if (requiresDeepKeyMatch(entry[0])) restB.push(entry) } - return deepStrictEqualArrayUnordered(restA, restB, memo) + return deepStrictEqualArrayUnordered(restA, restB, depth, memo) } -function deepStrictEqualSet(a, b, memo) { +function deepStrictEqualSet(a, b, depth, memo) { if (a.size !== b.size) return false // Match primitive members directly through `b` in linear time and leave only @@ -292,10 +295,10 @@ function deepStrictEqualSet(a, b, memo) { if (requiresDeepKeyMatch(value)) restB.push(value) } - return deepStrictEqualArrayUnordered(restA, restB, memo) + return deepStrictEqualArrayUnordered(restA, restB, depth, memo) } -function deepStrictEqualObject(a, b, memo) { +function deepStrictEqualObject(a, b, depth, memo) { function getKeys(obj) { const keys = Object.keys(obj) @@ -312,7 +315,7 @@ function deepStrictEqualObject(a, b, memo) { if (aKeys.length !== getKeys(b).length) return false for (const key of aKeys) { - if (!(key in b) || !deepStrictEqualValue(a[key], b[key], memo)) return false + if (!(key in b) || !deepStrictEqualValue(a[key], b[key], depth + 1, memo)) return false } return true diff --git a/lib/memoize-map.js b/lib/memoize-map.js index e885c64..6aa95f8 100644 --- a/lib/memoize-map.js +++ b/lib/memoize-map.js @@ -1,28 +1,80 @@ +const pendingSymbol = Symbol('pending') + +class Node { + constructor(depth) { + this._depth = depth + this._count = 1 + } + + get depth() { + return this._depth + } + + get count() { + return this._count + } + + update(depth) { + this._depth = depth + this._count++ + } + + equals(node) { + return this._depth === node.depth && this._count === node.count + } +} + module.exports = class MemoizeMap { constructor() { - this._map = new Map() + this._pairs = new Map() + this._nodes = new Map() } has(a, b) { - const map = this._map.get(a) + const pairs = this._pairs.get(a) - if (map === undefined) return false + if (pairs === undefined) return false - return map.has(b) + return pairs.has(b) } get(a, b) { - return this._map.get(a).get(b) + const pairs = this._pairs.get(a) + + const result = pairs.get(b) + + if (result === pendingSymbol) { + const nodeA = this._nodes.get(a) + const nodeB = this._nodes.get(b) + + const newResult = nodeA.equals(nodeB) + + pairs.set(b, newResult) + + return newResult + } + + return result } - set(a, b, result) { - let map = this._map.get(a) + set(a, b, depth, result = pendingSymbol) { + this._updateNodes(a, b, depth) + + let pairs = this._pairs.get(a) - if (map === undefined) { - map = new Map() - this._map.set(a, map) + if (pairs === undefined) { + pairs = new Map() + this._pairs.set(a, pairs) } - map.set(b, result) + pairs.set(b, result) + } + + _updateNodes(a, b, depth) { + if (this._nodes.has(a)) this._nodes.get(a).update(depth) + else this._nodes.set(a, new Node(depth)) + + if (this._nodes.has(b)) this._nodes.get(b).update(depth) + else this._nodes.set(b, new Node(depth)) } } diff --git a/test.js b/test.js index 0d85502..a991609 100644 --- a/test.js +++ b/test.js @@ -556,7 +556,7 @@ test('deepStrictEqual, recursive object', (t) => { const b = {} b.prop = b - assert.deepStrictEqual(a, b) + t.execution(() => assert.deepStrictEqual(a, b)) } { From b1f04e0033bbfd953e0465ef7ea984c1b4ad29d6 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Mon, 10 Aug 2026 19:17:48 -0300 Subject: [PATCH 12/27] Fix linter --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 8a0a214..2299660 100644 --- a/index.js +++ b/index.js @@ -156,9 +156,9 @@ function deepStrictEqualValue(a, b, depth = 0, memo = new MemoizeMap()) { prototype === Number.prototype || prototype === String.prototype || prototype === Symbol.prototype - ) + ) { result = deepStrictEqualBoxedValue(a, b, depth, memo) - else if (type.isRegExp()) result = deepStrictEqualRegexp(a, b, depth, memo) + } else if (type.isRegExp()) result = deepStrictEqualRegexp(a, b, depth, memo) else if (type.isTypedArray()) result = deepStrictEqualTypedArray(a, b, depth, memo) else if (type.isDate()) result = deepStrictEqualDate(a, b, depth, memo) else if (type.isError()) result = deepStrictEqualError(a, b, depth, memo) From 6b7c39b13b85cbc1994e04e23ce0338a2ed1e1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Tue, 11 Aug 2026 09:21:23 +0200 Subject: [PATCH 13/27] Add more tests and update `bare-inspect` --- package.json | 2 +- test.js | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index edc5196..0c77306 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ }, "homepage": "https://github.com/holepunchto/bare-assert#readme", "dependencies": { - "bare-inspect": "^3.1.2", + "bare-inspect": "^3.1.5", "bare-type": "^1.1.0" }, "devDependencies": { diff --git a/test.js b/test.js index a991609..a5bab16 100644 --- a/test.js +++ b/test.js @@ -106,6 +106,19 @@ test('deepStrictEqual, array, additional property', (t) => { t.exception(() => assert.deepStrictEqual(c, [1, 2], 'should fail'), /should fail/) }) +test('deepStrictEqual, array, symbol property', (t) => { + const symbol = Symbol.for('symbol') + + const a = [1] + const b = [1] + + a[symbol] = 1 + b[symbol] = 2 + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual(a, b), assert.AssertionError) +}) + test('deepStrictEqual, arguments vs array', (t) => { function make() { return arguments @@ -244,6 +257,21 @@ test('deepStrictEqual, map', (t) => { ) }) +test('deepStrictEqual, map, additional property', (t) => { + const map1 = new Map([['a', 1]]) + const map2 = new Map([['a', 1]]) + + map1.foo = true + map2.foo = true + + t.execution(() => assert.deepStrictEqual(map1, map2)) + + map2.foo = false + + t.exception(() => assert.deepStrictEqual(map1, map2, 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual(map1, new Map([['a', 1]]), 'should fail'), /should fail/) +}) + test('deepStrictEqual, set', (t) => { t.execution(() => assert.deepStrictEqual(new Set(['a', 1, 'b', 2]), new Set(['b', 2, 'a', 1]))) t.execution(() => @@ -256,6 +284,21 @@ test('deepStrictEqual, set', (t) => { ) }) +test('deepStrictEqual, set, additional property', (t) => { + const set1 = new Set([1]) + const set2 = new Set([1]) + + set1.foo = true + set2.foo = true + + t.execution(() => assert.deepStrictEqual(set1, set2)) + + set2.foo = false + + t.exception(() => assert.deepStrictEqual(set1, set2, 'should fail'), /should fail/) + t.exception(() => assert.deepStrictEqual(set1, new Set([1]), 'should fail'), /should fail/) +}) + test('deepStrictEqual, weak map', (t) => { const map1 = new WeakMap([[Object, true]]) const map2 = new WeakMap([[Object, true]]) @@ -393,6 +436,21 @@ test('deepStrictEqual, error, cause property', (t) => { ), /should fail/ ) + t.execution(() => + assert.deepStrictEqual( + new Error('err', { cause: undefined }), + new Error('err', { cause: undefined }) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new Error('err'), + new Error('err', { cause: undefined }), + 'should fail' + ), + /should fail/ + ) }) test('deepStrictEqual, error, aggregate error', (t) => { @@ -602,6 +660,65 @@ test('deepStrictEqual, recursive object', (t) => { } }) +test('deepStrictEqual, recursive object, cycle shape', (t) => { + // Builds a chain of `tail` objects leading into a cycle of `cycle` objects, + // every node linked to the next through a single `prop` property. Every such + // graph unfolds to the same infinite chain, so the shape of the cycle alone + // does not decide equality; what matters is how many objects are reachable + // from the root before one repeats. + function cyclic(tail, cycle) { + const nodes = [] + + for (let i = 0; i < tail + cycle; i++) nodes.push({}) + for (let i = 0; i < nodes.length - 1; i++) nodes[i].prop = nodes[i + 1] + + nodes[nodes.length - 1].prop = nodes[tail] + + return nodes[0] + } + + t.execution(() => assert.deepStrictEqual(cyclic(0, 1), cyclic(0, 1))) + t.execution(() => assert.deepStrictEqual(cyclic(2, 2), cyclic(2, 2))) + + t.execution(() => assert.deepStrictEqual(cyclic(0, 2), cyclic(1, 1))) + t.execution(() => assert.deepStrictEqual(cyclic(1, 3), cyclic(3, 1))) + + t.execution(() => assert.deepStrictEqual(cyclic(0, 3), cyclic(1, 2))) + t.execution(() => assert.deepStrictEqual(cyclic(1, 2), cyclic(2, 1))) + t.execution(() => assert.deepStrictEqual(cyclic(0, 3), cyclic(2, 1))) + + t.exception( + () => assert.deepStrictEqual(cyclic(0, 1), cyclic(0, 2), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(cyclic(0, 1), cyclic(1, 1), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(cyclic(0, 2), cyclic(1, 2), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(cyclic(1, 2), cyclic(2, 2), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(cyclic(0, 3), cyclic(1, 3), 'should fail'), + /should fail/ + ) +}) + +test('deepStrictEqual, recursive object, cycle value', (t) => { + const a = { value: 1 } + a.prop = a + + const b = { value: 2 } + b.prop = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + test('deepStrictEqual, recursive array', (t) => { const a = [] const b = [a] From f3e233b9774e7d81559961c11a53a10b921a15e0 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Tue, 11 Aug 2026 16:27:02 -0300 Subject: [PATCH 14/27] Update memoization strategy + Tweaks --- index.js | 153 +++++++++++++++++++++---------------- lib/get-enumerable-keys.js | 10 +++ lib/memoization.js | 40 ++++++++++ lib/memoize-map.js | 80 ------------------- 4 files changed, 136 insertions(+), 147 deletions(-) create mode 100644 lib/get-enumerable-keys.js create mode 100644 lib/memoization.js delete mode 100644 lib/memoize-map.js diff --git a/index.js b/index.js index 2299660..0175b2d 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const inspect = require('bare-inspect') const getType = require('bare-type') -const MemoizeMap = require('./lib/memoize-map') +const Memoization = require('./lib/memoization') +const getEnumerableKeys = require('./lib/get-enumerable-keys') class AssertionError extends Error { constructor(opts = {}) { @@ -111,18 +112,28 @@ exports.ifError = function ifError(actual) { } exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - if (deepStrictEqualValue(actual, expected)) return + const memo = new Memoization() + + if (deepStrictEqualValue(actual, expected, memo) && deepStrictEqualCycles(...memo.cycles())) { + return + } assertFail({ message, actual, expected, operator: 'deepStrictEqual' }, deepStrictEqual) } exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, message) { - if (!deepStrictEqualValue(actual, expected)) return + const memo = new Memoization() + + if (!deepStrictEqualValue(actual, expected, memo) && deepStrictEqualCycles(...memo.cycles())) { + return + } assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) } -function deepStrictEqualValue(a, b, depth = 0, memo = new MemoizeMap()) { +function deepStrictEqualValue(a, b, memo, opts = {}) { + const { allowDuplicates = false } = opts + const type = getType(a) if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) @@ -134,7 +145,9 @@ function deepStrictEqualValue(a, b, depth = 0, memo = new MemoizeMap()) { if (type.isWeakMap() || type.isWeakSet() || type.isPromise()) return a === b if (Buffer.isBuffer(a)) return deepStrictEqualBuffer(a, b) + if (type.isArrayBuffer()) return deepStrictEqualBuffer(new Uint8Array(a), new Uint8Array(b)) + if (type.isDataView()) { return deepStrictEqualBuffer( new Uint8Array(a.buffer, a.byteOffset, a.byteLength), @@ -142,13 +155,7 @@ function deepStrictEqualValue(a, b, depth = 0, memo = new MemoizeMap()) { ) } - if (memo.has(a, b)) return memo.get(a, b) - if (memo.has(b, a)) return memo.get(b, a) - - // Do not pass the result value, the MemoizeMap will use a 'pending' value. - memo.set(a, b, depth) - - let result + if (memo.register(a, b, allowDuplicates)) return true if ( prototype === BigInt.prototype || @@ -157,67 +164,78 @@ function deepStrictEqualValue(a, b, depth = 0, memo = new MemoizeMap()) { prototype === String.prototype || prototype === Symbol.prototype ) { - result = deepStrictEqualBoxedValue(a, b, depth, memo) - } else if (type.isRegExp()) result = deepStrictEqualRegexp(a, b, depth, memo) - else if (type.isTypedArray()) result = deepStrictEqualTypedArray(a, b, depth, memo) - else if (type.isDate()) result = deepStrictEqualDate(a, b, depth, memo) - else if (type.isError()) result = deepStrictEqualError(a, b, depth, memo) - else if (type.isArguments() || type.isArray()) result = deepStrictEqualArray(a, b, depth, memo) - else if (type.isMap()) result = deepStrictEqualMap(a, b, depth, memo) - else if (type.isSet()) result = deepStrictEqualSet(a, b, depth, memo) - else result = deepStrictEqualObject(a, b, depth, memo) - - memo.set(a, b, depth, result) - - return result + return deepStrictEqualBoxedValue(a, b, memo) + } + + if (type.isRegExp()) return deepStrictEqualRegexp(a, b, memo) + + if (type.isTypedArray()) return deepStrictEqualTypedArray(a, b, memo) + + if (type.isDate()) return deepStrictEqualDate(a, b, memo) + + if (type.isError()) return deepStrictEqualError(a, b, memo) + + if (type.isArguments() || type.isArray()) return deepStrictEqualArray(a, b, memo) + + if (type.isMap()) return deepStrictEqualMap(a, b, memo) + + if (type.isSet()) return deepStrictEqualSet(a, b, memo) + + return deepStrictEqualObject(a, b, memo) } function deepStrictEqualBuffer(a, b) { return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 } -function deepStrictEqualBoxedValue(a, b, depth, memo) { - return ( - deepStrictEqualValue(a.valueOf(), b.valueOf(), depth, memo) && - deepStrictEqualObject(a, b, depth, memo) - ) +function deepStrictEqualCycles(a, b) { + if (a.length !== b.length) return false + + a.sort() + b.sort() + + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + + return true +} + +function deepStrictEqualBoxedValue(a, b, memo) { + return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) && deepStrictEqualObject(a, b, memo) } -function deepStrictEqualRegexp(a, b, depth, memo) { +function deepStrictEqualRegexp(a, b, memo) { return ( a.lastIndex === b.lastIndex && a.flags === b.flags && a.source === b.source && - deepStrictEqualObject(a, b, depth, memo) + deepStrictEqualObject(a, b, memo) ) } -function deepStrictEqualDate(a, b, depth, memo) { - return Object.is(a.getTime(), b.getTime()) && deepStrictEqualObject(a, b, depth, memo) +function deepStrictEqualDate(a, b, memo) { + return Object.is(a.getTime(), b.getTime()) && deepStrictEqualObject(a, b, memo) } -function deepStrictEqualError(a, b, depth, memo) { - const hasCause = Object.hasOwn(a, 'cause') || Object.hasOwn(b, 'cause') - const hasErrors = Object.hasOwn(a, 'errors') || Object.hasOwn(b, 'errors') - +function deepStrictEqualError(a, b, memo) { return ( - deepStrictEqualValue(a.name, b.name, depth, memo) && - deepStrictEqualValue(a.message, b.message, depth, memo) && - (hasCause ? deepStrictEqualValue(a.cause, b.cause, depth, memo) : true) && - (hasErrors ? deepStrictEqualValue(a.errors, b.errors, depth, memo) : true) && - deepStrictEqualObject(a, b, depth, memo) + deepStrictEqualValue(a.name, b.name, memo) && + deepStrictEqualValue(a.message, b.message, memo) && + deepStrictEqualObjectKeys(a, b, ['cause', 'errors'], memo) && + deepStrictEqualObject(a, b, memo) ) } -function deepStrictEqualArray(a, b, depth, memo) { - return a.length === b.length && deepStrictEqualObject(a, b, depth, memo) +function deepStrictEqualArray(a, b, memo) { + return a.length === b.length && deepStrictEqualObject(a, b, memo) } -function deepStrictEqualTypedArray(a, b, depth, memo) { - return deepStrictEqualBuffer(a, b) && deepStrictEqualObject(a, b, depth, memo) +function deepStrictEqualTypedArray(a, b, memo) { + return deepStrictEqualBuffer(a, b) && deepStrictEqualObject(a, b, memo) } -function deepStrictEqualArrayUnordered(a, b, depth, memo) { +function deepStrictEqualArrayUnordered(a, b, memo) { if (a.length !== b.length) return false for (let i = 0; i < a.length; i++) { @@ -227,7 +245,7 @@ function deepStrictEqualArrayUnordered(a, b, depth, memo) { for (let j = 0; j < b.length; j++) { const itemB = b[j] - if (deepStrictEqualValue(itemA, itemB, depth + 1, memo)) { + if (deepStrictEqualValue(itemA, itemB, memo, { allowDuplicates: true })) { found = true b.splice(j, 1) @@ -255,8 +273,8 @@ function requiresDeepKeyMatch(key) { return (type === 'object' && key !== null) || type === 'function' } -function deepStrictEqualMap(a, b, depth, memo) { - if (a.size !== b.size) return false +function deepStrictEqualMap(a, b, memo) { + if (a.size !== b.size || !deepStrictEqualObject(a, b, memo)) return false // Match entries with primitive keys directly through `b` in linear time and // leave only the object-keyed entries for the quadratic fallback. @@ -266,7 +284,7 @@ function deepStrictEqualMap(a, b, depth, memo) { for (const [key, value] of a) { if (requiresDeepKeyMatch(key)) { restA.push([key, value]) - } else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), depth + 1, memo)) { + } else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), memo)) { return false } } @@ -275,11 +293,11 @@ function deepStrictEqualMap(a, b, depth, memo) { if (requiresDeepKeyMatch(entry[0])) restB.push(entry) } - return deepStrictEqualArrayUnordered(restA, restB, depth, memo) + return deepStrictEqualArrayUnordered(restA, restB, memo) } -function deepStrictEqualSet(a, b, depth, memo) { - if (a.size !== b.size) return false +function deepStrictEqualSet(a, b, memo) { + if (a.size !== b.size || !deepStrictEqualObject(a, b, memo)) return false // Match primitive members directly through `b` in linear time and leave only // the object members for the quadratic fallback. @@ -295,27 +313,28 @@ function deepStrictEqualSet(a, b, depth, memo) { if (requiresDeepKeyMatch(value)) restB.push(value) } - return deepStrictEqualArrayUnordered(restA, restB, depth, memo) + return deepStrictEqualArrayUnordered(restA, restB, memo) } -function deepStrictEqualObject(a, b, depth, memo) { - function getKeys(obj) { - const keys = Object.keys(obj) +function deepStrictEqualObjectKeys(a, b, keys, memo) { + for (const key of keys) { + const hasA = key in a + const hasB = key in b - for (const symbolKey of Object.getOwnPropertySymbols(obj)) { - const { enumerable } = Object.getOwnPropertyDescriptor(obj, symbolKey) - if (enumerable) keys.push(symbolKey) - } - - return keys + if ((hasA ^ hasB) === 1) return false + if (hasA && hasB && !deepStrictEqualValue(a[key], b[key], memo)) return false } - const aKeys = getKeys(a) + return true +} + +function deepStrictEqualObject(a, b, memo) { + const aKeys = getEnumerableKeys(a) - if (aKeys.length !== getKeys(b).length) return false + if (aKeys.length !== getEnumerableKeys(b).length) return false for (const key of aKeys) { - if (!(key in b) || !deepStrictEqualValue(a[key], b[key], depth + 1, memo)) return false + if (!(key in b) || !deepStrictEqualValue(a[key], b[key], memo)) return false } return true diff --git a/lib/get-enumerable-keys.js b/lib/get-enumerable-keys.js new file mode 100644 index 0000000..c37817a --- /dev/null +++ b/lib/get-enumerable-keys.js @@ -0,0 +1,10 @@ +module.exports = function getEnumerableKeys(obj) { + const keys = Object.keys(obj) + + for (const symbolKey of Object.getOwnPropertySymbols(obj)) { + const { enumerable } = Object.getOwnPropertyDescriptor(obj, symbolKey) + if (enumerable) keys.push(symbolKey) + } + + return keys +} diff --git a/lib/memoization.js b/lib/memoization.js new file mode 100644 index 0000000..b6ce144 --- /dev/null +++ b/lib/memoization.js @@ -0,0 +1,40 @@ +const getEnumerableKeys = require('./get-enumerable-keys') + +module.exports = class Memoization { + constructor() { + this._aNodes = new Set() + this._bNodes = new Set() + + this._aCycleSizes = [] + this._bCycleSizes = [] + } + + register(a, b, allowDuplicates = false) { + const hasA = this._aNodes.has(a) + const hasB = this._bNodes.has(b) + + const newCycleFound = hasA || hasB + + if (hasA && !allowDuplicates) { + this._aCycleSizes.push(getEnumerableKeys(a).length) + + this._aNodes.delete(a) + } else { + this._aNodes.add(a) + } + + if (hasB && !allowDuplicates) { + this._bCycleSizes.push(getEnumerableKeys(b).length) + + this._bNodes.delete(b) + } else { + this._bNodes.add(b) + } + + return newCycleFound + } + + cycles() { + return [this._aCycleSizes, this._bCycleSizes] + } +} diff --git a/lib/memoize-map.js b/lib/memoize-map.js deleted file mode 100644 index 6aa95f8..0000000 --- a/lib/memoize-map.js +++ /dev/null @@ -1,80 +0,0 @@ -const pendingSymbol = Symbol('pending') - -class Node { - constructor(depth) { - this._depth = depth - this._count = 1 - } - - get depth() { - return this._depth - } - - get count() { - return this._count - } - - update(depth) { - this._depth = depth - this._count++ - } - - equals(node) { - return this._depth === node.depth && this._count === node.count - } -} - -module.exports = class MemoizeMap { - constructor() { - this._pairs = new Map() - this._nodes = new Map() - } - - has(a, b) { - const pairs = this._pairs.get(a) - - if (pairs === undefined) return false - - return pairs.has(b) - } - - get(a, b) { - const pairs = this._pairs.get(a) - - const result = pairs.get(b) - - if (result === pendingSymbol) { - const nodeA = this._nodes.get(a) - const nodeB = this._nodes.get(b) - - const newResult = nodeA.equals(nodeB) - - pairs.set(b, newResult) - - return newResult - } - - return result - } - - set(a, b, depth, result = pendingSymbol) { - this._updateNodes(a, b, depth) - - let pairs = this._pairs.get(a) - - if (pairs === undefined) { - pairs = new Map() - this._pairs.set(a, pairs) - } - - pairs.set(b, result) - } - - _updateNodes(a, b, depth) { - if (this._nodes.has(a)) this._nodes.get(a).update(depth) - else this._nodes.set(a, new Node(depth)) - - if (this._nodes.has(b)) this._nodes.get(b).update(depth) - else this._nodes.set(b, new Node(depth)) - } -} From 0d54ec60725a070fbf30cceead12c45327cf59d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 12 Aug 2026 10:15:30 +0200 Subject: [PATCH 15/27] Add additional tests --- test.js | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/test.js b/test.js index a5bab16..e0d6ffb 100644 --- a/test.js +++ b/test.js @@ -272,6 +272,70 @@ test('deepStrictEqual, map, additional property', (t) => { t.exception(() => assert.deepStrictEqual(map1, new Map([['a', 1]]), 'should fail'), /should fail/) }) +test('deepStrictEqual, map, object keys', (t) => { + t.execution(() => + assert.deepStrictEqual( + new Map([ + [{ x: 1 }, 'a'], + [{ x: 2 }, 'b'] + ]), + new Map([ + [{ x: 2 }, 'b'], + [{ x: 1 }, 'a'] + ]) + ) + ) + t.exception( + () => + assert.deepStrictEqual( + new Map([ + [{ x: 1 }, 'a'], + [{ x: 9 }, 'zzz'] + ]), + new Map([ + [{ x: 2 }, 'b'], + [{ x: 3 }, 'c'] + ]), + 'should fail' + ), + /should fail/ + ) + t.exception( + () => + assert.deepStrictEqual( + new Map([ + [{ x: 1 }, 'a'], + [{ x: 2 }, 'b'] + ]), + new Map([ + [{ x: 1 }, 'a'], + [{ x: 2 }, 'c'] + ]), + 'should fail' + ), + /should fail/ + ) + t.exception( + () => + assert.deepStrictEqual( + { + map: new Map([ + [{ k: 1 }, [1, 2]], + [{ k: 2 }, [3, 4]] + ]) + }, + { + map: new Map([ + [{ k: 1 }, [9, 9]], + [{ k: 2 }, [8, 8]] + ]) + }, + 'should fail' + ), + /should fail/ + ) +}) + test('deepStrictEqual, set', (t) => { t.execution(() => assert.deepStrictEqual(new Set(['a', 1, 'b', 2]), new Set(['b', 2, 'a', 1]))) t.execution(() => @@ -299,6 +363,21 @@ test('deepStrictEqual, set, additional property', (t) => { t.exception(() => assert.deepStrictEqual(set1, new Set([1]), 'should fail'), /should fail/) }) +test('deepStrictEqual, set, object members', (t) => { + t.execution(() => + assert.deepStrictEqual(new Set([{ x: 1 }, { x: 2 }]), new Set([{ x: 2 }, { x: 1 }])) + ) + t.exception( + () => + assert.deepStrictEqual( + new Set([{ x: 1 }, { x: 9 }]), + new Set([{ x: 2 }, { x: 3 }]), + 'should fail' + ), + /should fail/ + ) +}) + test('deepStrictEqual, weak map', (t) => { const map1 = new WeakMap([[Object, true]]) const map2 = new WeakMap([[Object, true]]) @@ -709,6 +788,89 @@ test('deepStrictEqual, recursive object, cycle shape', (t) => { ) }) +test('deepStrictEqual, recursive object, repeated edges', (t) => { + // The same object reached through more than one property is still a single + // cycle, so revisiting it must not restart the traversal. + t.execution(() => { + const build = () => { + const a = {} + a.foo = a + a.bar = a + return a + } + + return assert.deepStrictEqual(build(), build()) + }) + + t.execution(() => { + const build = () => { + const a = {} + a.foo = a + a.bar = a + a.baz = a + return a + } + + return assert.deepStrictEqual(build(), build()) + }) + + t.execution(() => { + const build = () => { + const a = {} + const b = {} + + a.foo = b + a.bar = b + b.foo = a + b.bar = a + + return a + } + + return assert.deepStrictEqual(build(), build()) + }) + + const a = { value: 1 } + a.foo = a + a.bar = a + + const b = { value: 2 } + b.foo = b + b.bar = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) +}) + +test('deepStrictEqual, shared reference', (t) => { + // One object referenced from several properties is not a cycle: it must + // compare equal to a structure that repeats the value instead of sharing it. + const shared = { value: 1 } + + t.execution(() => + assert.deepStrictEqual({ foo: shared, bar: shared }, { foo: { value: 1 }, bar: { value: 1 } }) + ) + t.execution(() => + assert.deepStrictEqual( + { foo: shared, bar: shared, baz: shared }, + { foo: { value: 1 }, bar: { value: 1 }, baz: { value: 1 } } + ) + ) + t.execution(() => assert.deepStrictEqual([shared, shared], [{ value: 1 }, { value: 1 }])) + t.execution(() => + assert.deepStrictEqual({ foo: shared, bar: shared }, { foo: shared, bar: shared }) + ) + + t.exception( + () => + assert.deepStrictEqual( + { foo: shared, bar: shared }, + { foo: { value: 1 }, bar: { value: 2 } }, + 'should fail' + ), + /should fail/ + ) +}) + test('deepStrictEqual, recursive object, cycle value', (t) => { const a = { value: 1 } a.prop = a @@ -749,6 +911,55 @@ test('deepStrictEqual, recursive map', (t) => { } }) +test('notDeepStrictEqual', (t) => { + t.execution(() => assert.notDeepStrictEqual({ foo: 1 }, { foo: 2 })) + t.execution(() => assert.notDeepStrictEqual([1, 2], [1, 2, 3])) + t.exception(() => assert.notDeepStrictEqual({ foo: 1 }, { foo: 1 }, 'should fail'), /should fail/) + t.exception(() => assert.notDeepStrictEqual([1, 2], [1, 2], 'should fail'), /should fail/) +}) + +test('notDeepStrictEqual, recursive object', (t) => { + function cyclic(tail, cycle) { + const nodes = [] + + for (let i = 0; i < tail + cycle; i++) nodes.push({}) + for (let i = 0; i < nodes.length - 1; i++) nodes[i].prop = nodes[i + 1] + + nodes[nodes.length - 1].prop = nodes[tail] + + return nodes[0] + } + + // Reachable object counts differ, so these are not deeply equal. + t.execution(() => assert.notDeepStrictEqual(cyclic(0, 2), cyclic(1, 2))) + + // Equal reachable object counts, so these are deeply equal. + t.exception( + () => assert.notDeepStrictEqual(cyclic(1, 2), cyclic(2, 1), 'should fail'), + /should fail/ + ) +}) + +test('notDeepStrictEqual, shared reference', (t) => { + const shared = { value: 1 } + + t.execution(() => + assert.notDeepStrictEqual( + { foo: shared, bar: shared, baz: 1 }, + { foo: { value: 1 }, bar: { value: 1 }, baz: 2 } + ) + ) + t.exception( + () => + assert.notDeepStrictEqual( + { foo: shared, bar: shared }, + { foo: { value: 1 }, bar: { value: 1 } }, + 'should fail' + ), + /should fail/ + ) +}) + test('deepStrictEqual, recursive set', (t) => { const a = new Set() a.add(a) From 6957b61869c2c58462e7fb7758bda77770df9a3f Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Wed, 12 Aug 2026 10:34:34 -0300 Subject: [PATCH 16/27] Fixes and refactoring --- index.js | 64 +++++++++++++++------------------------------- lib/memoization.js | 30 ++++++++++++---------- 2 files changed, 37 insertions(+), 57 deletions(-) diff --git a/index.js b/index.js index 0175b2d..f249394 100644 --- a/index.js +++ b/index.js @@ -114,9 +114,7 @@ exports.ifError = function ifError(actual) { exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { const memo = new Memoization() - if (deepStrictEqualValue(actual, expected, memo) && deepStrictEqualCycles(...memo.cycles())) { - return - } + if (deepStrictEqualValue(actual, expected, memo)) return assertFail({ message, actual, expected, operator: 'deepStrictEqual' }, deepStrictEqual) } @@ -124,16 +122,12 @@ exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, message) { const memo = new Memoization() - if (!deepStrictEqualValue(actual, expected, memo) && deepStrictEqualCycles(...memo.cycles())) { - return - } + if (!deepStrictEqualValue(actual, expected, memo)) return assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) } -function deepStrictEqualValue(a, b, memo, opts = {}) { - const { allowDuplicates = false } = opts - +function deepStrictEqualValue(a, b, memo) { const type = getType(a) if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) @@ -145,9 +139,7 @@ function deepStrictEqualValue(a, b, memo, opts = {}) { if (type.isWeakMap() || type.isWeakSet() || type.isPromise()) return a === b if (Buffer.isBuffer(a)) return deepStrictEqualBuffer(a, b) - if (type.isArrayBuffer()) return deepStrictEqualBuffer(new Uint8Array(a), new Uint8Array(b)) - if (type.isDataView()) { return deepStrictEqualBuffer( new Uint8Array(a.buffer, a.byteOffset, a.byteLength), @@ -155,7 +147,9 @@ function deepStrictEqualValue(a, b, memo, opts = {}) { ) } - if (memo.register(a, b, allowDuplicates)) return true + if (memo.add(a, b)) return true + + let result if ( prototype === BigInt.prototype || @@ -164,43 +158,25 @@ function deepStrictEqualValue(a, b, memo, opts = {}) { prototype === String.prototype || prototype === Symbol.prototype ) { - return deepStrictEqualBoxedValue(a, b, memo) - } - - if (type.isRegExp()) return deepStrictEqualRegexp(a, b, memo) - - if (type.isTypedArray()) return deepStrictEqualTypedArray(a, b, memo) - - if (type.isDate()) return deepStrictEqualDate(a, b, memo) - - if (type.isError()) return deepStrictEqualError(a, b, memo) - - if (type.isArguments() || type.isArray()) return deepStrictEqualArray(a, b, memo) - - if (type.isMap()) return deepStrictEqualMap(a, b, memo) - - if (type.isSet()) return deepStrictEqualSet(a, b, memo) - - return deepStrictEqualObject(a, b, memo) + result = deepStrictEqualBoxedValue(a, b, memo) + } else if (type.isRegExp()) result = deepStrictEqualRegexp(a, b, memo) + else if (type.isTypedArray()) result = deepStrictEqualTypedArray(a, b, memo) + else if (type.isDate()) result = deepStrictEqualDate(a, b, memo) + else if (type.isError()) result = deepStrictEqualError(a, b, memo) + else if (type.isArguments() || type.isArray()) result = deepStrictEqualArray(a, b, memo) + else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) + else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) + else result = deepStrictEqualObject(a, b, memo) + + const sameCycleSize = memo.remove(a, b) + + return result && sameCycleSize } function deepStrictEqualBuffer(a, b) { return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 } -function deepStrictEqualCycles(a, b) { - if (a.length !== b.length) return false - - a.sort() - b.sort() - - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false - } - - return true -} - function deepStrictEqualBoxedValue(a, b, memo) { return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) && deepStrictEqualObject(a, b, memo) } @@ -245,7 +221,7 @@ function deepStrictEqualArrayUnordered(a, b, memo) { for (let j = 0; j < b.length; j++) { const itemB = b[j] - if (deepStrictEqualValue(itemA, itemB, memo, { allowDuplicates: true })) { + if (deepStrictEqualValue(itemA, itemB, memo)) { found = true b.splice(j, 1) diff --git a/lib/memoization.js b/lib/memoization.js index b6ce144..c91179e 100644 --- a/lib/memoization.js +++ b/lib/memoization.js @@ -5,28 +5,24 @@ module.exports = class Memoization { this._aNodes = new Set() this._bNodes = new Set() - this._aCycleSizes = [] - this._bCycleSizes = [] + this._aCycleSize = 0 + this._bCycleSize = 0 } - register(a, b, allowDuplicates = false) { + add(a, b) { const hasA = this._aNodes.has(a) const hasB = this._bNodes.has(b) const newCycleFound = hasA || hasB - if (hasA && !allowDuplicates) { - this._aCycleSizes.push(getEnumerableKeys(a).length) - - this._aNodes.delete(a) + if (hasA) { + this._aCycleSize = getEnumerableKeys(a).length } else { this._aNodes.add(a) } - if (hasB && !allowDuplicates) { - this._bCycleSizes.push(getEnumerableKeys(b).length) - - this._bNodes.delete(b) + if (hasB) { + this._bCycleSize = getEnumerableKeys(b).length } else { this._bNodes.add(b) } @@ -34,7 +30,15 @@ module.exports = class Memoization { return newCycleFound } - cycles() { - return [this._aCycleSizes, this._bCycleSizes] + remove(a, b) { + this._aNodes.delete(a) + this._bNodes.delete(b) + + const sameCycleSize = this._aCycleSize === this._bCycleSize + + this._aCycleSize = 0 + this._bCycleSize = 0 + + return sameCycleSize } } From d27c01d5f4dc7953d7b618d12c19b7f62b064248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Wed, 12 Aug 2026 15:53:53 +0200 Subject: [PATCH 17/27] Add a couple more tests --- test.js | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/test.js b/test.js index e0d6ffb..7787a19 100644 --- a/test.js +++ b/test.js @@ -788,6 +788,87 @@ test('deepStrictEqual, recursive object, cycle shape', (t) => { ) }) +test('deepStrictEqual, recursive object, one-sided cycle', (t) => { + // Reaching an already-visited object on one side says nothing about the other + // side, so it must not be taken as equality. Here `a` repeats under `foo` + // while `b` repeats under `bar`, so each side closes a cycle where the other + // holds an ordinary value. + const a = {} + a.foo = a + a.bar = { value: 1 } + + const b = {} + b.foo = { value: 1, extra: 2, more: 3 } + b.bar = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + + // Only one side closes a cycle. + const c = {} + c.foo = c + c.bar = { value: 1 } + + const d = {} + d.foo = { value: 9, other: 9 } + d.bar = { value: 1 } + + t.exception(() => assert.deepStrictEqual(c, d, 'should fail'), /should fail/) + + const build = () => { + const value = {} + value.foo = value + value.bar = { value: 1 } + return value + } + + t.execution(() => assert.deepStrictEqual(build(), build())) +}) + +test('deepStrictEqual, recursive object, cycle position', (t) => { + // A self-edge under a different property, or at a different depth, describes a + // different structure. None of these are equal to each other, so treating any + // pair as equal would also make equality intransitive. + const selfThenLeaf = () => { + const a = {} + a.foo = a + a.bar = { value: 1 } + return a + } + + const onwardThenSelf = () => { + const a = {} + a.foo = { foo: { value: 1 } } + a.bar = a + return a + } + + const selfThenChain = () => { + const a = {} + const b = {} + a.foo = a + a.bar = b + b.foo = { value: 1 } + return a + } + + t.exception( + () => assert.deepStrictEqual(selfThenLeaf(), onwardThenSelf(), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(onwardThenSelf(), selfThenChain(), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual(selfThenLeaf(), selfThenChain(), 'should fail'), + /should fail/ + ) + + t.execution(() => assert.deepStrictEqual(selfThenLeaf(), selfThenLeaf())) + t.execution(() => assert.deepStrictEqual(onwardThenSelf(), onwardThenSelf())) + t.execution(() => assert.deepStrictEqual(selfThenChain(), selfThenChain())) +}) + test('deepStrictEqual, recursive object, repeated edges', (t) => { // The same object reached through more than one property is still a single // cycle, so revisiting it must not restart the traversal. From ff8e8dc922f9b8d685f2ae08f7e347dd40ec9004 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Wed, 12 Aug 2026 14:58:01 -0300 Subject: [PATCH 18/27] Compare cycles at the stopping condition --- index.js | 6 +++--- lib/memoization.js | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index f249394..ffc2ed5 100644 --- a/index.js +++ b/index.js @@ -147,7 +147,7 @@ function deepStrictEqualValue(a, b, memo) { ) } - if (memo.add(a, b)) return true + if (memo.add(a, b)) return memo.compareCycles() let result @@ -168,9 +168,9 @@ function deepStrictEqualValue(a, b, memo) { else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) else result = deepStrictEqualObject(a, b, memo) - const sameCycleSize = memo.remove(a, b) + memo.remove(a, b) - return result && sameCycleSize + return result } function deepStrictEqualBuffer(a, b) { diff --git a/lib/memoization.js b/lib/memoization.js index c91179e..8f1641c 100644 --- a/lib/memoization.js +++ b/lib/memoization.js @@ -33,12 +33,9 @@ module.exports = class Memoization { remove(a, b) { this._aNodes.delete(a) this._bNodes.delete(b) + } - const sameCycleSize = this._aCycleSize === this._bCycleSize - - this._aCycleSize = 0 - this._bCycleSize = 0 - - return sameCycleSize + compareCycles() { + return this._aCycleSize === this._bCycleSize } } From e6c8f813eae56085272a94d99225c9ab568f3a86 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Wed, 12 Aug 2026 16:58:19 -0300 Subject: [PATCH 19/27] Refactoring --- index.js | 3 ++- lib/memoization.js | 25 +++++++------------------ 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index ffc2ed5..3f1eddb 100644 --- a/index.js +++ b/index.js @@ -147,7 +147,8 @@ function deepStrictEqualValue(a, b, memo) { ) } - if (memo.add(a, b)) return memo.compareCycles() + const { isCircular, isEqual } = memo.add(a, b) + if (isCircular) return isEqual let result diff --git a/lib/memoization.js b/lib/memoization.js index 8f1641c..21630b8 100644 --- a/lib/memoization.js +++ b/lib/memoization.js @@ -4,38 +4,27 @@ module.exports = class Memoization { constructor() { this._aNodes = new Set() this._bNodes = new Set() - - this._aCycleSize = 0 - this._bCycleSize = 0 } add(a, b) { const hasA = this._aNodes.has(a) const hasB = this._bNodes.has(b) - const newCycleFound = hasA || hasB + if (hasA || hasB) { + const aSize = hasA ? getEnumerableKeys(a).length : 0 + const bSize = hasB ? getEnumerableKeys(b).length : 0 - if (hasA) { - this._aCycleSize = getEnumerableKeys(a).length - } else { - this._aNodes.add(a) + return { isCircular: true, isEqual: aSize === bSize } } - if (hasB) { - this._bCycleSize = getEnumerableKeys(b).length - } else { - this._bNodes.add(b) - } + this._aNodes.add(a) + this._bNodes.add(b) - return newCycleFound + return { isCircular: false } } remove(a, b) { this._aNodes.delete(a) this._bNodes.delete(b) } - - compareCycles() { - return this._aCycleSize === this._bCycleSize - } } From fda199ca4fc88e550ff869671f024eedb080b4df Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Wed, 12 Aug 2026 17:16:00 -0300 Subject: [PATCH 20/27] Refactoring, part two --- index.js | 7 +++++-- lib/memoization.js | 23 +++++++++++------------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 3f1eddb..0af41b5 100644 --- a/index.js +++ b/index.js @@ -147,8 +147,11 @@ function deepStrictEqualValue(a, b, memo) { ) } - const { isCircular, isEqual } = memo.add(a, b) - if (isCircular) return isEqual + if (memo.has(a, b)) { + return memo.compareCycles(a, b) + } else { + memo.add(a, b) + } let result diff --git a/lib/memoization.js b/lib/memoization.js index 21630b8..dcce4c7 100644 --- a/lib/memoization.js +++ b/lib/memoization.js @@ -6,25 +6,24 @@ module.exports = class Memoization { this._bNodes = new Set() } - add(a, b) { - const hasA = this._aNodes.has(a) - const hasB = this._bNodes.has(b) - - if (hasA || hasB) { - const aSize = hasA ? getEnumerableKeys(a).length : 0 - const bSize = hasB ? getEnumerableKeys(b).length : 0 - - return { isCircular: true, isEqual: aSize === bSize } - } + has(a, b) { + return this._aNodes.has(a) || this._bNodes.has(b) + } + add(a, b) { this._aNodes.add(a) this._bNodes.add(b) - - return { isCircular: false } } remove(a, b) { this._aNodes.delete(a) this._bNodes.delete(b) } + + compareCycles(a, b) { + const aCycleSize = this._aNodes.has(a) ? getEnumerableKeys(a).length : 0 + const bCycleSize = this._bNodes.has(b) ? getEnumerableKeys(b).length : 0 + + return aCycleSize === bCycleSize + } } From 9f03fb9218be7bd084402678a2a9c208c731f59d Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Wed, 12 Aug 2026 17:43:37 -0300 Subject: [PATCH 21/27] Use a single `Set` to store nodes --- lib/memoization.js | 16 +++++++--------- test.js | 12 ++++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/memoization.js b/lib/memoization.js index dcce4c7..8479ed2 100644 --- a/lib/memoization.js +++ b/lib/memoization.js @@ -2,27 +2,25 @@ const getEnumerableKeys = require('./get-enumerable-keys') module.exports = class Memoization { constructor() { - this._aNodes = new Set() - this._bNodes = new Set() + this._nodes = new Set() } has(a, b) { - return this._aNodes.has(a) || this._bNodes.has(b) + return this._nodes.has(a) || this._nodes.has(b) } add(a, b) { - this._aNodes.add(a) - this._bNodes.add(b) + this._nodes.add(a).add(b) } remove(a, b) { - this._aNodes.delete(a) - this._bNodes.delete(b) + this._nodes.delete(a) + this._nodes.delete(b) } compareCycles(a, b) { - const aCycleSize = this._aNodes.has(a) ? getEnumerableKeys(a).length : 0 - const bCycleSize = this._bNodes.has(b) ? getEnumerableKeys(b).length : 0 + const aCycleSize = this._nodes.has(a) ? getEnumerableKeys(a).length : 0 + const bCycleSize = this._nodes.has(b) ? getEnumerableKeys(b).length : 0 return aCycleSize === bCycleSize } diff --git a/test.js b/test.js index 7787a19..2502d95 100644 --- a/test.js +++ b/test.js @@ -704,6 +704,18 @@ test('deepStrictEqual, recursive object', (t) => { t.execution(() => assert.deepStrictEqual(a, b)) } + { + const a = {} + a.prop = {} + a.prop.prop = a.prop + + const b = {} + b.prop = {} + b.prop.prop = a.prop + + t.execution(() => assert.deepStrictEqual(a, b)) + } + { const a = {} a.prop = 'foo' From b2b64cf5ac9f201b3d718804e3233472644269e8 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Wed, 12 Aug 2026 18:26:40 -0300 Subject: [PATCH 22/27] API adjustment --- index.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 0af41b5..6246120 100644 --- a/index.js +++ b/index.js @@ -112,22 +112,18 @@ exports.ifError = function ifError(actual) { } exports.deepStrictEqual = function deepStrictEqual(actual, expected, message) { - const memo = new Memoization() - - if (deepStrictEqualValue(actual, expected, memo)) return + if (deepStrictEqualValue(actual, expected)) return assertFail({ message, actual, expected, operator: 'deepStrictEqual' }, deepStrictEqual) } exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, message) { - const memo = new Memoization() - - if (!deepStrictEqualValue(actual, expected, memo)) return + if (!deepStrictEqualValue(actual, expected)) return assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual) } -function deepStrictEqualValue(a, b, memo) { +function deepStrictEqualValue(a, b, memo = new Memoization()) { const type = getType(a) if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b) From e082f1a4532a160a5cd09cc09f3a520ec4fce84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Thu, 13 Aug 2026 09:16:57 +0200 Subject: [PATCH 23/27] Add more tests --- test.js | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/test.js b/test.js index 2502d95..f11bdae 100644 --- a/test.js +++ b/test.js @@ -881,6 +881,208 @@ test('deepStrictEqual, recursive object, cycle position', (t) => { t.execution(() => assert.deepStrictEqual(selfThenChain(), selfThenChain())) }) +test('deepStrictEqual, recursive object, sibling cycles', (t) => { + // Cycles found while comparing one property must not affect the comparison of + // the next. Here the first property holds a cycle on both sides, and the + // second holds a cycle on one side only, which is a difference in its own + // right regardless of what the first property established. + { + const a1 = {} + a1.foo = a1 + + const a2 = {} + a2.foo = a2 + + const b1 = {} + b1.foo = b1 + + const b2 = {} + b2.foo = { value: 1 } + + t.exception( + () => + assert.deepStrictEqual({ first: a1, second: a2 }, { first: b1, second: b2 }, 'should fail'), + /should fail/ + ) + + // The same difference on its own, and with the properties swapped. + t.exception( + () => assert.deepStrictEqual({ second: a2 }, { second: b2 }, 'should fail'), + /should fail/ + ) + t.exception( + () => + assert.deepStrictEqual({ first: a2, second: a1 }, { first: b2, second: b1 }, 'should fail'), + /should fail/ + ) + } + + { + const a1 = {} + a1.foo = a1 + a1.bar = a1 + + const a2 = {} + a2.foo = a2 + a2.bar = { value: 1 } + + const b1 = {} + b1.foo = b1 + b1.bar = b1 + + const b2 = {} + b2.foo = { value: 1, extra: 2 } + b2.bar = { value: 1 } + + t.exception( + () => + assert.deepStrictEqual({ first: a1, second: a2 }, { first: b1, second: b2 }, 'should fail'), + /should fail/ + ) + } + + const build = () => { + const first = {} + first.foo = first + + const second = {} + second.foo = second + + return { first, second } + } + + t.execution(() => assert.deepStrictEqual(build(), build())) +}) + +test('deepStrictEqual, recursive object, nested up-reference', (t) => { + // Both children point back at the root on one side, while on the other the + // second child points at its sibling instead. + const a = { foo: {}, bar: {} } + a.foo.up = a + a.bar.up = a + + const b = { foo: {}, bar: {} } + b.foo.up = b + b.bar.up = b.foo + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + + const build = () => { + const value = { foo: {}, bar: {} } + value.foo.up = value + value.bar.up = value + return value + } + + t.execution(() => assert.deepStrictEqual(build(), build())) +}) + +test('deepStrictEqual, recursive object, self vs sibling', (t) => { + // Reaching an object that is already being compared is only conclusive when + // both sides have reached one. Here the second property points at the root on + // one side and at its sibling on the other, yet both describe the same + // structure. + const a = {} + const aNext = {} + a.foo = aNext + a.bar = a + aNext.foo = a + aNext.bar = a + + const b = {} + const bNext = {} + b.foo = bNext + b.bar = bNext + bNext.foo = b + bNext.bar = b + + t.execution(() => assert.deepStrictEqual(a, b)) + + const buildSelf = () => { + const first = {} + const second = {} + first.foo = second + first.bar = first + second.foo = first + second.bar = first + return first + } + + const buildSibling = () => { + const first = {} + const second = {} + first.foo = second + first.bar = second + second.foo = first + second.bar = first + return first + } + + t.execution(() => assert.deepStrictEqual(buildSelf(), buildSelf())) + t.execution(() => assert.deepStrictEqual(buildSibling(), buildSibling())) +}) + +test('deepStrictEqual, recursive object, differing node count', (t) => { + // Three distinct objects on one side against two on the other. Stopping as + // soon as either side repeats hides the extra object. + { + const a = {} + const aNext = {} + const aLast = {} + a.foo = aLast + a.bar = aNext + aNext.foo = a + aNext.bar = aNext + aLast.foo = a + aLast.bar = a + + const b = {} + const bNext = {} + b.foo = bNext + b.bar = bNext + bNext.foo = b + bNext.bar = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + } + + { + const a = {} + const aNext = {} + const aLast = {} + a.foo = aLast + a.bar = aNext + aNext.foo = { leaf: 1 } + aNext.bar = a + aLast.foo = { leaf: 1 } + aLast.bar = aLast + + const b = {} + const bNext = {} + b.foo = bNext + b.bar = bNext + bNext.foo = { leaf: 1 } + bNext.bar = b + + t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) + } + + const build = () => { + const first = {} + const second = {} + const third = {} + first.foo = third + first.bar = second + second.foo = first + second.bar = second + third.foo = first + third.bar = first + return first + } + + t.execution(() => assert.deepStrictEqual(build(), build())) +}) + test('deepStrictEqual, recursive object, repeated edges', (t) => { // The same object reached through more than one property is still a single // cycle, so revisiting it must not restart the traversal. From b219edb1197ea12f3fbc6e536fffde970de87b35 Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Thu, 13 Aug 2026 10:42:17 -0300 Subject: [PATCH 24/27] Add recursion to `compareCycles` method --- lib/memoization.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/memoization.js b/lib/memoization.js index 8479ed2..302b8ab 100644 --- a/lib/memoization.js +++ b/lib/memoization.js @@ -1,4 +1,5 @@ const getEnumerableKeys = require('./get-enumerable-keys') +const getType = require('bare-type') module.exports = class Memoization { constructor() { @@ -19,9 +20,31 @@ module.exports = class Memoization { } compareCycles(a, b) { - const aCycleSize = this._nodes.has(a) ? getEnumerableKeys(a).length : 0 - const bCycleSize = this._nodes.has(b) ? getEnumerableKeys(b).length : 0 + const aCycleSize = this._nodes.has(a) ? this._countNodes(a) : 0 + const bCycleSize = this._nodes.has(b) ? this._countNodes(b) : 0 return aCycleSize === bCycleSize } + + _countNodes(obj) { + const nodes = new Set() + + function walk(rootNode) { + const keys = getEnumerableKeys(rootNode) + + for (const key of keys) { + const node = obj[key] + + if (nodes.has(node)) continue + + nodes.add(node) + + if (getType(node).isObject()) walk(node) + } + } + + walk(obj) + + return nodes.size + } } From 1469508b5c7149355b9a7dcfdc820a43abf1d1ec Mon Sep 17 00:00:00 2001 From: Yasser Nascimento Date: Thu, 13 Aug 2026 12:19:01 -0300 Subject: [PATCH 25/27] Revert "Add recursion to `compareCycles` method" This reverts commit b219edb1197ea12f3fbc6e536fffde970de87b35. --- lib/memoization.js | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/lib/memoization.js b/lib/memoization.js index 302b8ab..8479ed2 100644 --- a/lib/memoization.js +++ b/lib/memoization.js @@ -1,5 +1,4 @@ const getEnumerableKeys = require('./get-enumerable-keys') -const getType = require('bare-type') module.exports = class Memoization { constructor() { @@ -20,31 +19,9 @@ module.exports = class Memoization { } compareCycles(a, b) { - const aCycleSize = this._nodes.has(a) ? this._countNodes(a) : 0 - const bCycleSize = this._nodes.has(b) ? this._countNodes(b) : 0 + const aCycleSize = this._nodes.has(a) ? getEnumerableKeys(a).length : 0 + const bCycleSize = this._nodes.has(b) ? getEnumerableKeys(b).length : 0 return aCycleSize === bCycleSize } - - _countNodes(obj) { - const nodes = new Set() - - function walk(rootNode) { - const keys = getEnumerableKeys(rootNode) - - for (const key of keys) { - const node = obj[key] - - if (nodes.has(node)) continue - - nodes.add(node) - - if (getType(node).isObject()) walk(node) - } - } - - walk(obj) - - return nodes.size - } } From 1a7429a683e6436ee93aec0c224811d750af79ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Fri, 14 Aug 2026 11:07:56 +0200 Subject: [PATCH 26/27] Adjust memoization and optimise comparisons --- index.js | 93 +++++++++++++------------- lib/get-enumerable-keys.js | 10 --- lib/memoization.js | 20 +++--- test.js | 130 ++++++++++++++++++------------------- 4 files changed, 120 insertions(+), 133 deletions(-) delete mode 100644 lib/get-enumerable-keys.js diff --git a/index.js b/index.js index 6246120..36d117e 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,6 @@ const inspect = require('bare-inspect') const getType = require('bare-type') const Memoization = require('./lib/memoization') -const getEnumerableKeys = require('./lib/get-enumerable-keys') class AssertionError extends Error { constructor(opts = {}) { @@ -143,27 +142,20 @@ function deepStrictEqualValue(a, b, memo = new Memoization()) { ) } + // Anything that can be settled without descending into the values is settled + // first. A pair that already differs in its own right is unequal whatever the + // surrounding structures do. + if (!deepStrictEqualShallow(a, b, type, prototype)) return false + if (memo.has(a, b)) { - return memo.compareCycles(a, b) + return memo.compare(a, b) } else { memo.add(a, b) } let result - if ( - prototype === BigInt.prototype || - prototype === Boolean.prototype || - prototype === Number.prototype || - prototype === String.prototype || - prototype === Symbol.prototype - ) { - result = deepStrictEqualBoxedValue(a, b, memo) - } else if (type.isRegExp()) result = deepStrictEqualRegexp(a, b, memo) - else if (type.isTypedArray()) result = deepStrictEqualTypedArray(a, b, memo) - else if (type.isDate()) result = deepStrictEqualDate(a, b, memo) - else if (type.isError()) result = deepStrictEqualError(a, b, memo) - else if (type.isArguments() || type.isArray()) result = deepStrictEqualArray(a, b, memo) + if (type.isError()) result = deepStrictEqualError(a, b, memo) else if (type.isMap()) result = deepStrictEqualMap(a, b, memo) else if (type.isSet()) result = deepStrictEqualSet(a, b, memo) else result = deepStrictEqualObject(a, b, memo) @@ -173,25 +165,34 @@ function deepStrictEqualValue(a, b, memo = new Memoization()) { return result } -function deepStrictEqualBuffer(a, b) { - return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 -} - -function deepStrictEqualBoxedValue(a, b, memo) { - return deepStrictEqualValue(a.valueOf(), b.valueOf(), memo) && deepStrictEqualObject(a, b, memo) -} +// Compares everything about a pair that can be decided on the spot, leaving +// only the values reachable from it for the caller to walk. +function deepStrictEqualShallow(a, b, type, prototype) { + if ( + prototype === BigInt.prototype || + prototype === Boolean.prototype || + prototype === Number.prototype || + prototype === String.prototype || + prototype === Symbol.prototype + ) { + if (!Object.is(a.valueOf(), b.valueOf())) return false + } else if (type.isRegExp()) { + if (a.lastIndex !== b.lastIndex || a.flags !== b.flags || a.source !== b.source) return false + } else if (type.isTypedArray()) { + if (!deepStrictEqualBuffer(a, b)) return false + } else if (type.isDate()) { + if (!Object.is(a.getTime(), b.getTime())) return false + } else if (type.isArguments() || type.isArray()) { + if (a.length !== b.length) return false + } else if (type.isMap() || type.isSet()) { + if (a.size !== b.size) return false + } -function deepStrictEqualRegexp(a, b, memo) { - return ( - a.lastIndex === b.lastIndex && - a.flags === b.flags && - a.source === b.source && - deepStrictEqualObject(a, b, memo) - ) + return getEnumerableKeys(a).length === getEnumerableKeys(b).length } -function deepStrictEqualDate(a, b, memo) { - return Object.is(a.getTime(), b.getTime()) && deepStrictEqualObject(a, b, memo) +function deepStrictEqualBuffer(a, b) { + return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0 } function deepStrictEqualError(a, b, memo) { @@ -203,14 +204,6 @@ function deepStrictEqualError(a, b, memo) { ) } -function deepStrictEqualArray(a, b, memo) { - return a.length === b.length && deepStrictEqualObject(a, b, memo) -} - -function deepStrictEqualTypedArray(a, b, memo) { - return deepStrictEqualBuffer(a, b) && deepStrictEqualObject(a, b, memo) -} - function deepStrictEqualArrayUnordered(a, b, memo) { if (a.length !== b.length) return false @@ -250,7 +243,7 @@ function requiresDeepKeyMatch(key) { } function deepStrictEqualMap(a, b, memo) { - if (a.size !== b.size || !deepStrictEqualObject(a, b, memo)) return false + if (!deepStrictEqualObject(a, b, memo)) return false // Match entries with primitive keys directly through `b` in linear time and // leave only the object-keyed entries for the quadratic fallback. @@ -273,7 +266,7 @@ function deepStrictEqualMap(a, b, memo) { } function deepStrictEqualSet(a, b, memo) { - if (a.size !== b.size || !deepStrictEqualObject(a, b, memo)) return false + if (!deepStrictEqualObject(a, b, memo)) return false // Match primitive members directly through `b` in linear time and leave only // the object members for the quadratic fallback. @@ -304,14 +297,22 @@ function deepStrictEqualObjectKeys(a, b, keys, memo) { return true } +// The key counts have already been compared, so only the values are left. function deepStrictEqualObject(a, b, memo) { - const aKeys = getEnumerableKeys(a) - - if (aKeys.length !== getEnumerableKeys(b).length) return false - - for (const key of aKeys) { + for (const key of getEnumerableKeys(a)) { if (!(key in b) || !deepStrictEqualValue(a[key], b[key], memo)) return false } return true } + +function getEnumerableKeys(obj) { + const keys = Object.keys(obj) + + for (const symbolKey of Object.getOwnPropertySymbols(obj)) { + const { enumerable } = Object.getOwnPropertyDescriptor(obj, symbolKey) + if (enumerable) keys.push(symbolKey) + } + + return keys +} diff --git a/lib/get-enumerable-keys.js b/lib/get-enumerable-keys.js deleted file mode 100644 index c37817a..0000000 --- a/lib/get-enumerable-keys.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = function getEnumerableKeys(obj) { - const keys = Object.keys(obj) - - for (const symbolKey of Object.getOwnPropertySymbols(obj)) { - const { enumerable } = Object.getOwnPropertyDescriptor(obj, symbolKey) - if (enumerable) keys.push(symbolKey) - } - - return keys -} diff --git a/lib/memoization.js b/lib/memoization.js index 8479ed2..1ade1a7 100644 --- a/lib/memoization.js +++ b/lib/memoization.js @@ -1,5 +1,10 @@ -const getEnumerableKeys = require('./get-enumerable-keys') - +// Tracks the values currently being compared so that a comparison which +// revisits a pair can be settled without recursing forever. +// +// Reaching a pair where both values are already being compared means the two +// structures have looped back in step, so they agree. Reaching a pair where +// only one of them has looped means one structure repeated where the other did +// not, so they differ. module.exports = class Memoization { constructor() { this._nodes = new Set() @@ -9,6 +14,10 @@ module.exports = class Memoization { return this._nodes.has(a) || this._nodes.has(b) } + compare(a, b) { + return this._nodes.has(a) && this._nodes.has(b) + } + add(a, b) { this._nodes.add(a).add(b) } @@ -17,11 +26,4 @@ module.exports = class Memoization { this._nodes.delete(a) this._nodes.delete(b) } - - compareCycles(a, b) { - const aCycleSize = this._nodes.has(a) ? getEnumerableKeys(a).length : 0 - const bCycleSize = this._nodes.has(b) ? getEnumerableKeys(b).length : 0 - - return aCycleSize === bCycleSize - } } diff --git a/test.js b/test.js index f11bdae..09ed3c1 100644 --- a/test.js +++ b/test.js @@ -978,26 +978,8 @@ test('deepStrictEqual, recursive object, nested up-reference', (t) => { }) test('deepStrictEqual, recursive object, self vs sibling', (t) => { - // Reaching an object that is already being compared is only conclusive when - // both sides have reached one. Here the second property points at the root on - // one side and at its sibling on the other, yet both describe the same - // structure. - const a = {} - const aNext = {} - a.foo = aNext - a.bar = a - aNext.foo = a - aNext.bar = a - - const b = {} - const bNext = {} - b.foo = bNext - b.bar = bNext - bNext.foo = b - bNext.bar = b - - t.execution(() => assert.deepStrictEqual(a, b)) - + // The second property points back at the root on one side and at its sibling + // on the other, which are different structures. const buildSelf = () => { const first = {} const second = {} @@ -1018,56 +1000,29 @@ test('deepStrictEqual, recursive object, self vs sibling', (t) => { return first } - t.execution(() => assert.deepStrictEqual(buildSelf(), buildSelf())) - t.execution(() => assert.deepStrictEqual(buildSibling(), buildSibling())) + // The answer must not depend on how deeply the comparison is nested. + t.exception( + () => assert.deepStrictEqual(buildSelf(), buildSibling(), 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual({ value: buildSelf() }, { value: buildSibling() }, 'should fail'), + /should fail/ + ) + t.exception( + () => assert.deepStrictEqual([buildSelf()], [buildSibling()], 'should fail'), + /should fail/ + ) + + t.execution(() => assert.deepStrictEqual({ value: buildSelf() }, { value: buildSelf() })) + t.execution(() => assert.deepStrictEqual({ value: buildSibling() }, { value: buildSibling() })) }) test('deepStrictEqual, recursive object, differing node count', (t) => { - // Three distinct objects on one side against two on the other. Stopping as - // soon as either side repeats hides the extra object. - { - const a = {} - const aNext = {} - const aLast = {} - a.foo = aLast - a.bar = aNext - aNext.foo = a - aNext.bar = aNext - aLast.foo = a - aLast.bar = a - - const b = {} - const bNext = {} - b.foo = bNext - b.bar = bNext - bNext.foo = b - bNext.bar = b - - t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) - } - - { - const a = {} - const aNext = {} - const aLast = {} - a.foo = aLast - a.bar = aNext - aNext.foo = { leaf: 1 } - aNext.bar = a - aLast.foo = { leaf: 1 } - aLast.bar = aLast - - const b = {} - const bNext = {} - b.foo = bNext - b.bar = bNext - bNext.foo = { leaf: 1 } - bNext.bar = b - - t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) - } - - const build = () => { + // Three objects on one side against two on the other, describing the same + // structure once the cycles are followed. How many objects a structure is + // built from does not decide the answer. + const buildThree = () => { const first = {} const second = {} const third = {} @@ -1080,7 +1035,46 @@ test('deepStrictEqual, recursive object, differing node count', (t) => { return first } - t.execution(() => assert.deepStrictEqual(build(), build())) + const buildTwo = () => { + const first = {} + const second = {} + first.foo = second + first.bar = second + second.foo = first + second.bar = first + return first + } + + t.execution(() => assert.deepStrictEqual({ value: buildThree() }, { value: buildTwo() })) + t.execution(() => assert.deepStrictEqual([buildThree()], [buildTwo()])) + t.execution(() => assert.deepStrictEqual({ value: buildThree() }, { value: buildThree() })) + + const buildThreeWithLeaves = () => { + const first = {} + const second = {} + const third = {} + first.foo = third + first.bar = second + second.foo = { leaf: 1 } + second.bar = first + third.foo = { leaf: 1 } + third.bar = third + return first + } + + const buildTwoWithLeaves = () => { + const first = {} + const second = {} + first.foo = second + first.bar = second + second.foo = { leaf: 1 } + second.bar = first + return first + } + + t.execution(() => + assert.deepStrictEqual({ value: buildThreeWithLeaves() }, { value: buildTwoWithLeaves() }) + ) }) test('deepStrictEqual, recursive object, repeated edges', (t) => { From 9de4dc04db499dc27c331c0e57c9b6415a495c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Isager=20Dalsgar=C3=B0?= Date: Sat, 15 Aug 2026 09:03:04 +0200 Subject: [PATCH 27/27] Remove some equivalent test cases --- test.js | 127 ++++++++++++++------------------------------------------ 1 file changed, 31 insertions(+), 96 deletions(-) diff --git a/test.js b/test.js index 09ed3c1..6b58d17 100644 --- a/test.js +++ b/test.js @@ -725,30 +725,6 @@ test('deepStrictEqual, recursive object', (t) => { t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) } - - { - const a = {} - a.prop = a - - const b = {} - b.prop = {} - b.prop.prop = b - - t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) - } - - { - const a = {} - a.prop = a - - const b = {} - b.prop = b - - const c = {} - c.prop = a - - t.exception(() => assert.deepStrictEqual(b, c, 'should fail'), /should fail/) - } }) test('deepStrictEqual, recursive object, cycle shape', (t) => { @@ -800,42 +776,6 @@ test('deepStrictEqual, recursive object, cycle shape', (t) => { ) }) -test('deepStrictEqual, recursive object, one-sided cycle', (t) => { - // Reaching an already-visited object on one side says nothing about the other - // side, so it must not be taken as equality. Here `a` repeats under `foo` - // while `b` repeats under `bar`, so each side closes a cycle where the other - // holds an ordinary value. - const a = {} - a.foo = a - a.bar = { value: 1 } - - const b = {} - b.foo = { value: 1, extra: 2, more: 3 } - b.bar = b - - t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) - - // Only one side closes a cycle. - const c = {} - c.foo = c - c.bar = { value: 1 } - - const d = {} - d.foo = { value: 9, other: 9 } - d.bar = { value: 1 } - - t.exception(() => assert.deepStrictEqual(c, d, 'should fail'), /should fail/) - - const build = () => { - const value = {} - value.foo = value - value.bar = { value: 1 } - return value - } - - t.execution(() => assert.deepStrictEqual(build(), build())) -}) - test('deepStrictEqual, recursive object, cycle position', (t) => { // A self-edge under a different property, or at a different depth, describes a // different structure. None of these are equal to each other, so treating any @@ -876,6 +816,17 @@ test('deepStrictEqual, recursive object, cycle position', (t) => { /should fail/ ) + // The same shape with no cycle at all is different again. + t.exception( + () => + assert.deepStrictEqual( + selfThenLeaf(), + { foo: { value: 9, other: 9 }, bar: { value: 1 } }, + 'should fail' + ), + /should fail/ + ) + t.execution(() => assert.deepStrictEqual(selfThenLeaf(), selfThenLeaf())) t.execution(() => assert.deepStrictEqual(onwardThenSelf(), onwardThenSelf())) t.execution(() => assert.deepStrictEqual(selfThenChain(), selfThenChain())) @@ -1160,16 +1111,6 @@ test('deepStrictEqual, shared reference', (t) => { ) }) -test('deepStrictEqual, recursive object, cycle value', (t) => { - const a = { value: 1 } - a.prop = a - - const b = { value: 2 } - b.prop = b - - t.exception(() => assert.deepStrictEqual(a, b, 'should fail'), /should fail/) -}) - test('deepStrictEqual, recursive array', (t) => { const a = [] const b = [a] @@ -1200,6 +1141,16 @@ test('deepStrictEqual, recursive map', (t) => { } }) +test('deepStrictEqual, recursive set', (t) => { + const a = new Set() + a.add(a) + + const b = new Set() + b.add(b) + + t.execution(() => assert.deepStrictEqual(a, b)) +}) + test('notDeepStrictEqual', (t) => { t.execution(() => assert.notDeepStrictEqual({ foo: 1 }, { foo: 2 })) t.execution(() => assert.notDeepStrictEqual([1, 2], [1, 2, 3])) @@ -1208,25 +1159,19 @@ test('notDeepStrictEqual', (t) => { }) test('notDeepStrictEqual, recursive object', (t) => { - function cyclic(tail, cycle) { - const nodes = [] - - for (let i = 0; i < tail + cycle; i++) nodes.push({}) - for (let i = 0; i < nodes.length - 1; i++) nodes[i].prop = nodes[i + 1] - - nodes[nodes.length - 1].prop = nodes[tail] - - return nodes[0] + // A two object cycle against the same cycle behind one extra object. + const twoCycle = () => { + const first = {} + const second = {} + first.prop = second + second.prop = first + return first } - // Reachable object counts differ, so these are not deeply equal. - t.execution(() => assert.notDeepStrictEqual(cyclic(0, 2), cyclic(1, 2))) + const tailIntoTwoCycle = () => ({ prop: twoCycle() }) - // Equal reachable object counts, so these are deeply equal. - t.exception( - () => assert.notDeepStrictEqual(cyclic(1, 2), cyclic(2, 1), 'should fail'), - /should fail/ - ) + t.execution(() => assert.notDeepStrictEqual(twoCycle(), tailIntoTwoCycle())) + t.exception(() => assert.notDeepStrictEqual(twoCycle(), twoCycle(), 'should fail'), /should fail/) }) test('notDeepStrictEqual, shared reference', (t) => { @@ -1248,13 +1193,3 @@ test('notDeepStrictEqual, shared reference', (t) => { /should fail/ ) }) - -test('deepStrictEqual, recursive set', (t) => { - const a = new Set() - a.add(a) - - const b = new Set() - b.add(b) - - t.execution(() => assert.deepStrictEqual(a, b)) -})