UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

691 lines (636 loc) 19.6 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/util/comparisons.js import { compare } from "nstdlib/stub/binding/buffer"; import * as assert from "nstdlib/lib/internal/assert"; import * as types from "nstdlib/lib/internal/util/types"; import { constants as __constants__, getOwnNonIndexProperties, } from "nstdlib/stub/binding/util"; import * as __hoisted_internal_crypto_util__ from "nstdlib/lib/internal/crypto/util"; const { isAnyArrayBuffer, isArrayBufferView, isDate, isMap, isRegExp, isSet, isNativeError, isBoxedPrimitive, isNumberObject, isStringObject, isBooleanObject, isBigIntObject, isSymbolObject, isFloat32Array, isFloat64Array, isKeyObject, isCryptoKey, } = types; const { ONLY_ENUMERABLE, SKIP_SYMBOLS } = __constants__; const kStrict = true; const kLoose = false; const kNoIterator = 0; const kIsArray = 1; const kIsSet = 2; const kIsMap = 3; let kKeyObject; // Check if they have the same source and flags function areSimilarRegExps(a, b) { return ( a.source === b.source && a.flags === b.flags && a.lastIndex === b.lastIndex ); } function areSimilarFloatArrays(a, b) { if (a.byteLength !== b.byteLength) { return false; } const len = TypedArrayPrototypeGetByteLength(a); for (let offset = 0; offset < len; offset++) { if (a[offset] !== b[offset]) { return false; } } return true; } function areSimilarTypedArrays(a, b) { if (a.byteLength !== b.byteLength) { return false; } return ( compare( new Uint8Array(a.buffer, a.byteOffset, a.byteLength), new Uint8Array(b.buffer, b.byteOffset, b.byteLength), ) === 0 ); } function areEqualArrayBuffers(buf1, buf2) { return ( buf1.byteLength === buf2.byteLength && compare(new Uint8Array(buf1), new Uint8Array(buf2)) === 0 ); } function isEqualBoxedPrimitive(val1, val2) { if (isNumberObject(val1)) { return ( isNumberObject(val2) && Object.is( Number.prototype.valueOf.call(val1), Number.prototype.valueOf.call(val2), ) ); } if (isStringObject(val1)) { return ( isStringObject(val2) && String.prototype.valueOf.call(val1) === String.prototype.valueOf.call(val2) ); } if (isBooleanObject(val1)) { return ( isBooleanObject(val2) && Boolean.prototype.valueOf.call(val1) === Boolean.prototype.valueOf.call(val2) ); } if (isBigIntObject(val1)) { return ( isBigIntObject(val2) && BigInt.prototype.valueOf.call(val1) === BigInt.prototype.valueOf.call(val2) ); } if (isSymbolObject(val1)) { return ( isSymbolObject(val2) && Symbol.prototype.valueOf.call(val1) === Symbol.prototype.valueOf.call(val2) ); } /* c8 ignore next */ assert.fail(`Unknown boxed type ${val1}`); } // Notes: Type tags are historical [[Class]] properties that can be set by // FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS // and retrieved using Object.prototype.toString.call(obj) in JS // See https://tc39.github.io/ecma262/#sec-object.prototype.tostring // for a list of tags pre-defined in the spec. // There are some unspecified tags in the wild too (e.g. typed array tags). // Since tags can be altered, they only serve fast failures // // For strict comparison, objects should have // a) The same built-in type tag. // b) The same prototypes. function innerDeepEqual(val1, val2, strict, memos) { // All identical values are equivalent, as determined by ===. if (val1 === val2) { return val1 !== 0 || Object.is(val1, val2) || !strict; } // Check more closely if val1 and val2 are equal. if (strict) { if (typeof val1 === "number") { return Number.isNaN(val1) && Number.isNaN(val2); } if ( typeof val2 !== "object" || typeof val1 !== "object" || val1 === null || val2 === null || Object.getPrototypeOf(val1) !== Object.getPrototypeOf(val2) ) { return false; } } else { if (val1 === null || typeof val1 !== "object") { if (val2 === null || typeof val2 !== "object") { // eslint-disable-next-line eqeqeq return val1 == val2 || (Number.isNaN(val1) && Number.isNaN(val2)); } return false; } if (val2 === null || typeof val2 !== "object") { return false; } } const val1Tag = Object.prototype.toString.call(val1); const val2Tag = Object.prototype.toString.call(val2); if (val1Tag !== val2Tag) { return false; } if (Array.isArray(val1)) { // Check for sparse arrays and general fast path if (!Array.isArray(val2) || val1.length !== val2.length) { return false; } const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS; const keys1 = getOwnNonIndexProperties(val1, filter); const keys2 = getOwnNonIndexProperties(val2, filter); if (keys1.length !== keys2.length) { return false; } return keyCheck(val1, val2, strict, memos, kIsArray, keys1); } else if (val1Tag === "[object Object]") { return keyCheck(val1, val2, strict, memos, kNoIterator); } else if (isDate(val1)) { if ( !isDate(val2) || Date.prototype.getTime.call(val1) !== Date.prototype.getTime.call(val2) ) { return false; } } else if (isRegExp(val1)) { if (!isRegExp(val2) || !areSimilarRegExps(val1, val2)) { return false; } } else if (isArrayBufferView(val1)) { if ( TypedArrayPrototypeGetSymbolToStringTag(val1) !== TypedArrayPrototypeGetSymbolToStringTag(val2) ) { return false; } if (!strict && (isFloat32Array(val1) || isFloat64Array(val1))) { if (!areSimilarFloatArrays(val1, val2)) { return false; } } else if (!areSimilarTypedArrays(val1, val2)) { return false; } // Buffer.compare returns true, so val1.length === val2.length. If they both // only contain numeric keys, we don't need to exam further than checking // the symbols. const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS; const keys1 = getOwnNonIndexProperties(val1, filter); const keys2 = getOwnNonIndexProperties(val2, filter); if (keys1.length !== keys2.length) { return false; } return keyCheck(val1, val2, strict, memos, kNoIterator, keys1); } else if (isSet(val1)) { if (!isSet(val2) || val1.size !== val2.size) { return false; } return keyCheck(val1, val2, strict, memos, kIsSet); } else if (isMap(val1)) { if (!isMap(val2) || val1.size !== val2.size) { return false; } return keyCheck(val1, val2, strict, memos, kIsMap); } else if (isAnyArrayBuffer(val1)) { if (!isAnyArrayBuffer(val2) || !areEqualArrayBuffers(val1, val2)) { return false; } } else if (isNativeError(val1) || val1 instanceof Error) { // Do not compare the stack as it might differ even though the error itself // is otherwise identical. if (!isNativeError(val2) && !(val2 instanceof Error)) { return false; } const message1Enumerable = Object.prototype.propertyIsEnumerable.call( val1, "message", ); const name1Enumerable = Object.prototype.propertyIsEnumerable.call( val1, "name", ); const cause1Enumerable = Object.prototype.propertyIsEnumerable.call( val1, "cause", ); const errors1Enumerable = Object.prototype.propertyIsEnumerable.call( val1, "errors", ); if ( message1Enumerable !== Object.prototype.propertyIsEnumerable.call(val2, "message") || (!message1Enumerable && val1.message !== val2.message) || name1Enumerable !== Object.prototype.propertyIsEnumerable.call(val2, "name") || (!name1Enumerable && val1.name !== val2.name) || cause1Enumerable !== Object.prototype.propertyIsEnumerable.call(val2, "cause") || (!cause1Enumerable && !innerDeepEqual(val1.cause, val2.cause, strict, memos)) || errors1Enumerable !== Object.prototype.propertyIsEnumerable.call(val2, "errors") || (!errors1Enumerable && !innerDeepEqual(val1.errors, val2.errors, strict, memos)) ) { return false; } } else if (isBoxedPrimitive(val1)) { if (!isEqualBoxedPrimitive(val1, val2)) { return false; } } else if ( Array.isArray(val2) || isArrayBufferView(val2) || isSet(val2) || isMap(val2) || isDate(val2) || isRegExp(val2) || isAnyArrayBuffer(val2) || isBoxedPrimitive(val2) || isNativeError(val2) || val2 instanceof Error ) { return false; } else if (isKeyObject(val1)) { if (!isKeyObject(val2) || !val1.equals(val2)) { return false; } } else if (isCryptoKey(val1)) { kKeyObject ??= __hoisted_internal_crypto_util__.kKeyObject; if ( !isCryptoKey(val2) || val1.extractable !== val2.extractable || !innerDeepEqual(val1.algorithm, val2.algorithm, strict, memos) || !innerDeepEqual(val1.usages, val2.usages, strict, memos) || !innerDeepEqual(val1[kKeyObject], val2[kKeyObject], strict, memos) ) { return false; } } return keyCheck(val1, val2, strict, memos, kNoIterator); } function getEnumerables(val, keys) { return Array.prototype.filter.call(keys, (k) => Object.prototype.propertyIsEnumerable.call(val, k), ); } function keyCheck(val1, val2, strict, memos, iterationType, aKeys) { // For all remaining Object pairs, including Array, objects and Maps, // equivalence is determined by having: // a) The same number of owned enumerable properties // b) The same set of keys/indexes (although not necessarily the same order) // c) Equivalent values for every corresponding key/index // d) For Sets and Maps, equal contents // Note: this accounts for both named and indexed properties on Arrays. const isArrayLikeObject = aKeys !== undefined; if (aKeys === undefined) { aKeys = Object.keys(val1); } // Cheap key test if (aKeys.length > 0) { for (const key of aKeys) { if (!Object.prototype.propertyIsEnumerable.call(val2, key)) { return false; } } } if (!isArrayLikeObject) { // The pair must have the same number of owned properties. if (aKeys.length !== Object.keys(val2).length) { return false; } if (strict) { const symbolKeysA = Object.getOwnPropertySymbols(val1); if (symbolKeysA.length !== 0) { let count = 0; for (const key of symbolKeysA) { if (Object.prototype.propertyIsEnumerable.call(val1, key)) { if (!Object.prototype.propertyIsEnumerable.call(val2, key)) { return false; } Array.prototype.push.call(aKeys, key); count++; } else if (Object.prototype.propertyIsEnumerable.call(val2, key)) { return false; } } const symbolKeysB = Object.getOwnPropertySymbols(val2); if ( symbolKeysA.length !== symbolKeysB.length && getEnumerables(val2, symbolKeysB).length !== count ) { return false; } } else { const symbolKeysB = Object.getOwnPropertySymbols(val2); if ( symbolKeysB.length !== 0 && getEnumerables(val2, symbolKeysB).length !== 0 ) { return false; } } } } if ( aKeys.length === 0 && (iterationType === kNoIterator || (iterationType === kIsArray && val1.length === 0) || val1.size === 0) ) { return true; } // Use memos to handle cycles. if (memos === undefined) { memos = { set: undefined, a: val1, b: val2, c: undefined, d: undefined, deep: false, deleteFailures: false, }; return objEquiv(val1, val2, strict, aKeys, memos, iterationType); } if (memos.set === undefined) { if (memos.deep === false) { if (memos.a === val1) { if (memos.b === val2) return true; } memos.c = val1; memos.d = val2; memos.deep = true; const result = objEquiv(val1, val2, strict, aKeys, memos, iterationType); memos.deep = false; return result; } memos.set = new Set(); memos.set.add(memos.a); memos.set.add(memos.b); memos.set.add(memos.c); memos.set.add(memos.d); } const { set } = memos; const originalSize = set.size; set.add(val1); set.add(val2); if (originalSize === set.size) { return true; } const areEq = objEquiv(val1, val2, strict, aKeys, memos, iterationType); if (areEq || memos.deleteFailures) { set.delete(val1); set.delete(val2); } return areEq; } function setHasEqualElement(set, val2, strict, memo) { const { deleteFailures } = memo; memo.deleteFailures = true; for (const val1 of set) { if (innerDeepEqual(val1, val2, strict, memo)) { // Remove the matching element to make sure we do not check that again. set.delete(val1); memo.deleteFailures = deleteFailures; return true; } } memo.deleteFailures = deleteFailures; return false; } // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#Loose_equality_using // Sadly it is not possible to detect corresponding values properly in case the // type is a string, number, bigint or boolean. The reason is that those values // can match lots of different string values (e.g., 1n == '+00001'). function findLooseMatchingPrimitives(prim) { switch (typeof prim) { case "undefined": return null; case "object": // Only pass in null as object! return undefined; case "symbol": return false; case "string": prim = +prim; // Loose equal entries exist only if the string is possible to convert to // a regular number and not NaN. // Fall through case "number": if (Number.isNaN(prim)) { return false; } } return true; } function setMightHaveLoosePrim(a, b, prim) { const altValue = findLooseMatchingPrimitives(prim); if (altValue != null) return altValue; return b.has(altValue) && !a.has(altValue); } function mapMightHaveLoosePrim(a, b, prim, item, memo) { const altValue = findLooseMatchingPrimitives(prim); if (altValue != null) { return altValue; } const curB = b.get(altValue); if ( (curB === undefined && !b.has(altValue)) || !innerDeepEqual(item, curB, false, memo) ) { return false; } return !a.has(altValue) && innerDeepEqual(item, curB, false, memo); } function setEquiv(a, b, strict, memo) { // This is a lazily initiated Set of entries which have to be compared // pairwise. let set = null; for (const val of a) { if (!b.has(val)) { if ( (typeof val !== "object" || val === null) && (strict || !setMightHaveLoosePrim(a, b, val)) ) { return false; } if (set === null) { if (b.size === 1) { return innerDeepEqual(val, b.values().next().value, strict, memo); } set = new Set(); } // If the specified value doesn't exist in the second set it's a object // (or in loose mode: a non-matching primitive). Find the // deep-(strict-)equal element in a set copy to reduce duplicate checks. set.add(val); } } if (set !== null) { for (const val of b) { // Primitive values have already been handled above. if (typeof val === "object" && val !== null) { if (!a.has(val) && !setHasEqualElement(set, val, strict, memo)) { return false; } } else if ( !strict && !a.has(val) && !setHasEqualElement(set, val, strict, memo) ) { return false; } } return set.size === 0; } return true; } function mapHasEqualEntry(set, map, key2, item2, strict, memo) { // To be able to handle cases like: // Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']]) // ... we need to consider *all* matching keys, not just the first we find. const { deleteFailures } = memo; memo.deleteFailures = true; for (const key1 of set) { if ( innerDeepEqual(key1, key2, strict, memo) && innerDeepEqual(map.get(key1), item2, strict, memo) ) { set.delete(key1); memo.deleteFailures = deleteFailures; return true; } } memo.deleteFailures = deleteFailures; return false; } function mapEquiv(a, b, strict, memo) { let set = null; for (const { 0: key, 1: item1 } of a) { if (typeof key === "object" && key !== null) { if (set === null) { if (b.size === 1) { const { 0: key2, 1: item2 } = b.entries().next().value; return ( innerDeepEqual(key, key2, strict, memo) && innerDeepEqual(item1, item2, strict, memo) ); } set = new Set(); } set.add(key); } else { // By directly retrieving the value we prevent another b.has(key) check in // almost all possible cases. const item2 = b.get(key); if ( (item2 === undefined && !b.has(key)) || !innerDeepEqual(item1, item2, strict, memo) ) { if (strict) return false; // Fast path to detect missing string, symbol, undefined and null // keys. if (!mapMightHaveLoosePrim(a, b, key, item1, memo)) return false; if (set === null) { set = new Set(); } set.add(key); } } } if (set !== null) { for (const { 0: key, 1: item } of b) { if (typeof key === "object" && key !== null) { if (!mapHasEqualEntry(set, a, key, item, strict, memo)) return false; } else if ( !strict && (!a.has(key) || !innerDeepEqual(a.get(key), item, strict, memo)) && !mapHasEqualEntry(set, a, key, item, strict, memo) ) { return false; } } return set.size === 0; } return true; } function objEquiv(a, b, strict, keys, memos, iterationType) { // The pair must have equivalent values for every corresponding key. if (keys.length > 0) { for (const key of keys) { if (!innerDeepEqual(a[key], b[key], strict, memos)) { return false; } } } if (iterationType === kIsArray) { for (let i = 0; i < a.length; i++) { if (Object.prototype.hasOwnProperty.call(a, i)) { if ( !Object.prototype.hasOwnProperty.call(b, i) || !innerDeepEqual(a[i], b[i], strict, memos) ) { return false; } } else if (Object.prototype.hasOwnProperty.call(b, i)) { return false; } else { // Array is sparse. const keysA = Object.keys(a); for (; i < keysA.length; i++) { const key = keysA[i]; if ( !Object.prototype.hasOwnProperty.call(b, key) || !innerDeepEqual(a[key], b[key], strict, memos) ) { return false; } } return keysA.length === Object.keys(b).length; } } } else if (iterationType === kIsSet) { if (!setEquiv(a, b, strict, memos)) { return false; } } else if (iterationType === kIsMap) { if (!mapEquiv(a, b, strict, memos)) { return false; } } return true; } function isDeepEqual(val1, val2) { return innerDeepEqual(val1, val2, kLoose); } function isDeepStrictEqual(val1, val2) { return innerDeepEqual(val1, val2, kStrict); } export { isDeepEqual }; export { isDeepStrictEqual };