Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1637b70
Add `deepStrictEqual` support
yassernasc Aug 3, 2026
310f8ab
Add memoization
yassernasc Aug 4, 2026
62915b7
Refactor `Map` and `Set` functions
yassernasc Aug 4, 2026
c191cfa
Update README
yassernasc Aug 4, 2026
76fb428
Apply suggestions from code review
yassernasc Aug 5, 2026
1f82d50
More tests
yassernasc Aug 5, 2026
0bb9f07
Cover a few more cases
yassernasc Aug 6, 2026
5b3da3b
Keep `Map` and `Set` equality linear in common case
kasperisager Aug 10, 2026
0f2fa87
Add additional test cases
kasperisager Aug 10, 2026
d1976dd
Address part of the additional tests
yassernasc Aug 10, 2026
9a7cc33
Enhance memoization algorithm
yassernasc Aug 10, 2026
b1f04e0
Fix linter
yassernasc Aug 10, 2026
6b7c39b
Add more tests and update `bare-inspect`
kasperisager Aug 11, 2026
f3e233b
Update memoization strategy + Tweaks
yassernasc Aug 11, 2026
0d54ec6
Add additional tests
kasperisager Aug 12, 2026
6957b61
Fixes and refactoring
yassernasc Aug 12, 2026
d27c01d
Add a couple more tests
kasperisager Aug 12, 2026
ff8e8dc
Compare cycles at the stopping condition
yassernasc Aug 12, 2026
e6c8f81
Refactoring
yassernasc Aug 12, 2026
fda199c
Refactoring, part two
yassernasc Aug 12, 2026
9f03fb9
Use a single `Set` to store nodes
yassernasc Aug 12, 2026
b2b64cf
API adjustment
yassernasc Aug 12, 2026
e082f1a
Add more tests
kasperisager Aug 13, 2026
b219edb
Add recursion to `compareCycles` method
yassernasc Aug 13, 2026
1469508
Revert "Add recursion to `compareCycles` method"
yassernasc Aug 13, 2026
1a7429a
Adjust memoization and optimise comparisons
kasperisager Aug 14, 2026
9de4dc0
Remove some equivalent test cases
kasperisager Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
209 changes: 209 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const inspect = require('bare-inspect')
const getType = require('bare-type')
const Memoization = require('./lib/memoization')

class AssertionError extends Error {
constructor(opts = {}) {
Expand Down Expand Up @@ -107,3 +109,210 @@ 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, memo = new Memoization()) {
const type = getType(a)

if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b)

const prototype = Object.getPrototypeOf(a)

if (prototype !== Object.getPrototypeOf(b)) return false

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),
new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
)
}

// 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.compare(a, b)
} else {
memo.add(a, b)
}

let result

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)

memo.remove(a, b)

return result
}

// 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
}

return getEnumerableKeys(a).length === getEnumerableKeys(b).length
}

function deepStrictEqualBuffer(a, b) {
return a.byteLength === b.byteLength && Buffer.compare(a, b) === 0
}

function deepStrictEqualError(a, b, memo) {
return (
deepStrictEqualValue(a.name, b.name, memo) &&
deepStrictEqualValue(a.message, b.message, memo) &&
deepStrictEqualObjectKeys(a, b, ['cause', 'errors'], memo) &&
deepStrictEqualObject(a, b, memo)
)
}

function deepStrictEqualArrayUnordered(a, b, memo) {
if (a.length !== b.length) return false

for (let i = 0; i < a.length; i++) {
let found = false
const itemA = a[i]

for (let j = 0; j < b.length; j++) {
const itemB = b[j]

if (deepStrictEqualValue(itemA, itemB, memo)) {
found = true

b.splice(j, 1)

break
}
}

if (found === false) return false
}

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 (!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.
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 (!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.
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 deepStrictEqualObjectKeys(a, b, keys, memo) {
for (const key of keys) {
const hasA = key in a
const hasB = key in b

if ((hasA ^ hasB) === 1) return false
if (hasA && hasB && !deepStrictEqualValue(a[key], b[key], memo)) return false
}

return true
}

// The key counts have already been compared, so only the values are left.
function deepStrictEqualObject(a, b, memo) {
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
}
29 changes: 29 additions & 0 deletions lib/memoization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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()
}

has(a, b) {
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)
}

remove(a, b) {
this._nodes.delete(a)
this._nodes.delete(b)
}
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
},
"files": [
"index.js",
"index.d.ts"
"index.d.ts",
"lib"
],
"scripts": {
"format": "prettier --write . && lunte --fix",
Expand All @@ -29,7 +30,8 @@
},
"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": {
"brittle": "^4.1.0",
Expand Down
Loading