Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ Throws an `AssertionError` unless `actual` and `expected` are the same value, re

Throws an `AssertionError` unless `actual` and `expected` are not the same value, recursively.

#### `assert.partialDeepStrictEqual(actual, expected[, message])`

Throws an `AssertionError` unless `actual` and `expected` are the same value, partially and recursively.

#### `assert.match(actual, regexp[, message])`

Throws an `AssertionError` unless `actual` is a string that matches `regexp`.
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ declare namespace assert {

export function notDeepStrictEqual(actual: any, expected: any, message?: string | Error): void

export function partialDeepStrictEqual(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: 153 additions & 56 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,16 @@ exports.notDeepStrictEqual = function notDeepStrictEqual(actual, expected, messa
assertFail({ message, actual, expected, operator: 'notDeepStrictEqual' }, notDeepStrictEqual)
}

function deepStrictEqualValue(a, b, memo = new Memoization()) {
exports.partialDeepStrictEqual = function partialDeepStrictEqual(actual, expected, message) {
if (deepStrictEqualValue(actual, expected, true)) return

assertFail(
{ message, actual, expected, operator: 'partialDeepStrictEqual' },
partialDeepStrictEqual
)
}

function deepStrictEqualValue(a, b, partial = false, memo = new Memoization()) {
const type = getType(a)

if (!type.isObject() || !getType(b).isObject()) return Object.is(a, b)
Expand All @@ -133,32 +142,41 @@ function deepStrictEqualValue(a, b, memo = new Memoization()) {

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))
// 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, partial)) return false

if (Buffer.isBuffer(a)) return deepStrictEqualBuffer(a, b, partial)

if (type.isArrayBuffer() || type.isSharedArrayBuffer()) {
return deepStrictEqualBuffer(new Uint8Array(a), new Uint8Array(b), partial)
}

if (type.isDataView()) {
return deepStrictEqualBuffer(
new Uint8Array(a.buffer, a.byteOffset, a.byteLength),
new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
new Uint8Array(b.buffer, b.byteOffset, b.byteLength),
partial
)
}

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

if (partial === true && type.isArray()) {
if (!partialDeepStrictEqualArray(a, b, partial, memo)) return false
}

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)
if (type.isError()) result = deepStrictEqualError(a, b, type, partial, memo)
else if (type.isMap()) result = deepStrictEqualMap(a, b, type, partial, memo)
else if (type.isSet()) result = deepStrictEqualSet(a, b, type, partial, memo)
else result = deepStrictEqualObject(a, b, type, partial, memo)

memo.remove(a, b)

Expand All @@ -167,7 +185,7 @@ function deepStrictEqualValue(a, b, memo = new Memoization()) {

// 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) {
function deepStrictEqualShallow(a, b, type, prototype, partial) {
if (
prototype === BigInt.prototype ||
prototype === Boolean.prototype ||
Expand All @@ -179,45 +197,68 @@ function deepStrictEqualShallow(a, b, type, prototype) {
} 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
if (!deepStrictEqualBuffer(a, b, partial)) 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
}

if (type.isArguments() || type.isArray()) {
if (partial === true) {
if (b.length > a.length) return false
} else {
if (a.length !== b.length) return false
}
} else if (type.isMap() || type.isSet()) {
if (a.size !== b.size) return false
if (partial === true) {
if (b.size > a.size) return false
} else {
if (a.size !== b.size) return false
}
} else if (type.isSharedArrayBuffer()) {
if (partial === true) {
if (b.byteLength > a.byteLength) return false
} else {
if (a.byteLength !== b.byteLength) return false
}
}

return getEnumerableKeys(a).length === getEnumerableKeys(b).length
if (partial === true) {
return getEnumerableKeys(a).length >= getEnumerableKeys(b).length
} else {
return getEnumerableKeys(a).length === getEnumerableKeys(b).length
}
}

function deepStrictEqualBuffer(a, b) {
function deepStrictEqualBuffer(a, b, partial) {
if (partial === true) return partialDeepStrictEqualBuffer(a, b)

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

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

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

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

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

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

b.splice(j, 1)
a.splice(j, 1)

break
}
Expand All @@ -242,65 +283,121 @@ function requiresDeepKeyMatch(key) {
return (type === 'object' && key !== null) || type === 'function'
}

function deepStrictEqualMap(a, b, memo) {
if (!deepStrictEqualObject(a, b, memo)) return false
function deepStrictEqualMap(a, b, type, partial, memo) {
if (!deepStrictEqualObject(a, b, type, partial, 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 = []
const restA = []

for (const [key, value] of a) {
for (const [key, value] of b) {
if (requiresDeepKeyMatch(key)) {
restA.push([key, value])
} else if (!b.has(key) || !deepStrictEqualValue(value, b.get(key), memo)) {
restB.push({ key, value })
} else if (!a.has(key) || !deepStrictEqualValue(a.get(key), value, partial, memo)) {
return false
}
}

for (const entry of b) {
if (requiresDeepKeyMatch(entry[0])) restB.push(entry)
for (const [key, value] of a) {
if (requiresDeepKeyMatch(key)) restA.push({ key, value })
}

return deepStrictEqualArrayUnordered(restA, restB, memo)
return deepStrictEqualArrayUnordered(restA, restB, partial, memo)
}

function deepStrictEqualSet(a, b, memo) {
if (!deepStrictEqualObject(a, b, memo)) return false
function deepStrictEqualSet(a, b, type, partial, memo) {
if (!deepStrictEqualObject(a, b, type, partial, 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
}
const restA = []

for (const value of b) {
if (requiresDeepKeyMatch(value)) restB.push(value)
else if (!a.has(value)) return false
}

for (const value of a) {
if (requiresDeepKeyMatch(value)) restA.push(value)
}

return deepStrictEqualArrayUnordered(restA, restB, memo)
return deepStrictEqualArrayUnordered(restA, restB, partial, memo)
}

function deepStrictEqualObjectKeys(a, b, keys, memo) {
function deepStrictEqualObjectKeys(a, b, keys, partial, memo) {
for (const key of keys) {
const hasA = key in a
const hasB = key in b

if (partial === true && !hasB) continue
if ((hasA ^ hasB) === 1) return false
if (hasA && hasB && !deepStrictEqualValue(a[key], b[key], memo)) return false
if (hasA && !deepStrictEqualValue(a[key], b[key], partial, 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
function deepStrictEqualObject(a, b, type, partial, memo) {
const aKeys = getEnumerableKeys(a)
const bKeys = getEnumerableKeys(b)

const indexedPartial = partial === true && (type.isArray() || type.isTypedArray())

for (const key of bKeys) {
// the indexes of partial objects are tested at
// partialDeepStrictEqualArray and partialDeepStrictEqualBuffer
if (indexedPartial && typeof key === 'string' && !isNaN(key)) continue

if (!aKeys.includes(key) || !deepStrictEqualValue(a[key], b[key], partial, memo)) {
return false
}
}

return true
}

function partialDeepStrictEqualArray(a, b, partial, memo) {
let j = -1

for (let i = 0; i < b.length; i++) {
let found = false

if (!(i in b)) continue // Ignore hole in array

while (++j < a.length) {
if (!(j in a)) continue // Ignore hole in array

if (deepStrictEqualValue(a[j], b[i], partial, memo)) {
found = true

break
}
}

if (found === false) return false
}

return true
}

function partialDeepStrictEqualBuffer(a, b) {
let j = -1

for (let i = 0; i < b.length; i++) {
let found = false

while (++j < a.length) {
if (Object.is(a[j], b[i])) {
found = true

break
}
}

if (found === false) return false
}

return true
Expand Down
Loading