1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-23 07:44:48 -05:00
denoland-deno/ext/node/polyfills/internal/util/comparisons.ts
2024-01-01 19:58:21 +00:00

681 lines
16 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// deno-lint-ignore-file
import {
isAnyArrayBuffer,
isArrayBufferView,
isBigIntObject,
isBooleanObject,
isBoxedPrimitive,
isDate,
isFloat32Array,
isFloat64Array,
isMap,
isNativeError,
isNumberObject,
isRegExp,
isSet,
isStringObject,
isSymbolObject,
isTypedArray,
} from "ext:deno_node/internal/util/types.ts";
import { Buffer } from "node:buffer";
import {
getOwnNonIndexProperties,
ONLY_ENUMERABLE,
SKIP_SYMBOLS,
} from "ext:deno_node/internal_binding/util.ts";
enum valueType {
noIterator,
isArray,
isSet,
isMap,
}
interface Memo {
val1: Map<unknown, unknown>;
val2: Map<unknown, unknown>;
position: number;
}
let memo: Memo;
export function isDeepStrictEqual(val1: unknown, val2: unknown): boolean {
return innerDeepEqual(val1, val2, true);
}
export function isDeepEqual(val1: unknown, val2: unknown): boolean {
return innerDeepEqual(val1, val2, false);
}
function innerDeepEqual(
val1: unknown,
val2: unknown,
strict: boolean,
memos = memo,
): boolean {
// Basic case covered by Strict Equality Comparison
if (val1 === val2) {
if (val1 !== 0) return true;
return strict ? Object.is(val1, val2) : true;
}
if (strict) {
// Cases where the values are not objects
// If both values are Not a Number NaN
if (typeof val1 !== "object") {
return (
typeof val1 === "number" && Number.isNaN(val1) && Number.isNaN(val2)
);
}
// If either value is null
if (typeof val2 !== "object" || val1 === null || val2 === null) {
return false;
}
// If the prototype are not the same
if (Object.getPrototypeOf(val1) !== Object.getPrototypeOf(val2)) {
return false;
}
} else {
// Non strict case where values are either null or NaN
if (val1 === null || typeof val1 !== "object") {
if (val2 === null || typeof val2 !== "object") {
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);
// prototype must be Strictly Equal
if (
val1Tag !== val2Tag
) {
return false;
}
// handling when values are array
if (Array.isArray(val1)) {
// quick rejection cases
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, valueType.isArray, keys1);
} else if (val1Tag === "[object Object]") {
return keyCheck(
val1 as object,
val2 as object,
strict,
memos,
valueType.noIterator,
);
} else if (val1 instanceof Date) {
if (!(val2 instanceof Date) || val1.getTime() !== val2.getTime()) {
return false;
}
} else if (val1 instanceof RegExp) {
if (!(val2 instanceof RegExp) || !areSimilarRegExps(val1, val2)) {
return false;
}
} else if (isNativeError(val1) || val1 instanceof Error) {
// stack may or may not be same, hence it shouldn't be compared
if (
// How to handle the type errors here
(!isNativeError(val2) && !(val2 instanceof Error)) ||
(val1 as Error).message !== (val2 as Error).message ||
(val1 as Error).name !== (val2 as Error).name
) {
return false;
}
} else if (isArrayBufferView(val1)) {
const TypedArrayPrototypeGetSymbolToStringTag = (
val:
| BigInt64Array
| BigUint64Array
| Float32Array
| Float64Array
| Int8Array
| Int16Array
| Int32Array
| Uint8Array
| Uint8ClampedArray
| Uint16Array
| Uint32Array,
) =>
Object.getOwnPropertySymbols(val)
.map((item) => item.toString())
.toString();
if (
isTypedArray(val1) &&
isTypedArray(val2) &&
(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;
}
const filter = strict ? ONLY_ENUMERABLE : ONLY_ENUMERABLE | SKIP_SYMBOLS;
const keysVal1 = getOwnNonIndexProperties(val1 as object, filter);
const keysVal2 = getOwnNonIndexProperties(val2 as object, filter);
if (keysVal1.length !== keysVal2.length) {
return false;
}
return keyCheck(
val1 as object,
val2 as object,
strict,
memos,
valueType.noIterator,
keysVal1,
);
} else if (isSet(val1)) {
if (
!isSet(val2) ||
(val1 as Set<unknown>).size !== (val2 as Set<unknown>).size
) {
return false;
}
return keyCheck(
val1 as object,
val2 as object,
strict,
memos,
valueType.isSet,
);
} else if (isMap(val1)) {
if (
!isMap(val2) || val1.size !== val2.size
) {
return false;
}
return keyCheck(
val1 as object,
val2 as object,
strict,
memos,
valueType.isMap,
);
} else if (isAnyArrayBuffer(val1)) {
if (!isAnyArrayBuffer(val2) || !areEqualArrayBuffers(val1, val2)) {
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;
}
return keyCheck(
val1 as object,
val2 as object,
strict,
memos,
valueType.noIterator,
);
}
function keyCheck(
val1: object,
val2: object,
strict: boolean,
memos: Memo,
iterationType: valueType,
aKeys: (string | symbol)[] = [],
) {
if (arguments.length === 5) {
aKeys = Object.keys(val1);
const bKeys = Object.keys(val2);
// The pair must have the same number of owned properties.
if (aKeys.length !== bKeys.length) {
return false;
}
}
// Cheap key test
let i = 0;
for (; i < aKeys.length; i++) {
if (!val2.propertyIsEnumerable(aKeys[i])) {
return false;
}
}
if (strict && arguments.length === 5) {
const symbolKeysA = Object.getOwnPropertySymbols(val1);
if (symbolKeysA.length !== 0) {
let count = 0;
for (i = 0; i < symbolKeysA.length; i++) {
const key = symbolKeysA[i];
if (val1.propertyIsEnumerable(key)) {
if (!val2.propertyIsEnumerable(key)) {
return false;
}
// added toString here
aKeys.push(key.toString());
count++;
} else if (val2.propertyIsEnumerable(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 === valueType.noIterator ||
(iterationType === valueType.isArray && (val1 as []).length === 0) ||
(val1 as Set<unknown>).size === 0)
) {
return true;
}
if (memos === undefined) {
memos = {
val1: new Map(),
val2: new Map(),
position: 0,
};
} else {
const val2MemoA = memos.val1.get(val1);
if (val2MemoA !== undefined) {
const val2MemoB = memos.val2.get(val2);
if (val2MemoB !== undefined) {
return val2MemoA === val2MemoB;
}
}
memos.position++;
}
memos.val1.set(val1, memos.position);
memos.val2.set(val2, memos.position);
const areEq = objEquiv(val1, val2, strict, aKeys, memos, iterationType);
memos.val1.delete(val1);
memos.val2.delete(val2);
return areEq;
}
function areSimilarRegExps(a: RegExp, b: RegExp) {
return a.source === b.source && a.flags === b.flags &&
a.lastIndex === b.lastIndex;
}
// TODO(standvpmnt): add type for arguments
function areSimilarFloatArrays(arr1: any, arr2: any): boolean {
if (arr1.byteLength !== arr2.byteLength) {
return false;
}
for (let i = 0; i < arr1.byteLength; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
// TODO(standvpmnt): add type for arguments
function areSimilarTypedArrays(arr1: any, arr2: any): boolean {
if (arr1.byteLength !== arr2.byteLength) {
return false;
}
return (
Buffer.compare(
new Uint8Array(arr1.buffer, arr1.byteOffset, arr1.byteLength),
new Uint8Array(arr2.buffer, arr2.byteOffset, arr2.byteLength),
) === 0
);
}
// TODO(standvpmnt): add type for arguments
function areEqualArrayBuffers(buf1: any, buf2: any): boolean {
return (
buf1.byteLength === buf2.byteLength &&
Buffer.compare(new Uint8Array(buf1), new Uint8Array(buf2)) === 0
);
}
// TODO(standvpmnt): this check of getOwnPropertySymbols and getOwnPropertyNames
// length is sufficient to handle the current test case, however this will fail
// to catch a scenario wherein the getOwnPropertySymbols and getOwnPropertyNames
// length is the same(will be very contrived but a possible shortcoming
function isEqualBoxedPrimitive(a: any, b: any): boolean {
if (
Object.getOwnPropertyNames(a).length !==
Object.getOwnPropertyNames(b).length
) {
return false;
}
if (
Object.getOwnPropertySymbols(a).length !==
Object.getOwnPropertySymbols(b).length
) {
return false;
}
if (isNumberObject(a)) {
return (
isNumberObject(b) &&
Object.is(
Number.prototype.valueOf.call(a),
Number.prototype.valueOf.call(b),
)
);
}
if (isStringObject(a)) {
return (
isStringObject(b) &&
(String.prototype.valueOf.call(a) === String.prototype.valueOf.call(b))
);
}
if (isBooleanObject(a)) {
return (
isBooleanObject(b) &&
(Boolean.prototype.valueOf.call(a) === Boolean.prototype.valueOf.call(b))
);
}
if (isBigIntObject(a)) {
return (
isBigIntObject(b) &&
(BigInt.prototype.valueOf.call(a) === BigInt.prototype.valueOf.call(b))
);
}
if (isSymbolObject(a)) {
return (
isSymbolObject(b) &&
(Symbol.prototype.valueOf.call(a) ===
Symbol.prototype.valueOf.call(b))
);
}
// assert.fail(`Unknown boxed type ${val1}`);
// return false;
throw Error(`Unknown boxed type`);
}
function getEnumerables(val: any, keys: any) {
return keys.filter((key: string) => val.propertyIsEnumerable(key));
}
function objEquiv(
obj1: any,
obj2: any,
strict: boolean,
keys: any,
memos: Memo,
iterationType: valueType,
): boolean {
let i = 0;
if (iterationType === valueType.isSet) {
if (!setEquiv(obj1, obj2, strict, memos)) {
return false;
}
} else if (iterationType === valueType.isMap) {
if (!mapEquiv(obj1, obj2, strict, memos)) {
return false;
}
} else if (iterationType === valueType.isArray) {
for (; i < obj1.length; i++) {
if (obj1.hasOwnProperty(i)) {
if (
!obj2.hasOwnProperty(i) ||
!innerDeepEqual(obj1[i], obj2[i], strict, memos)
) {
return false;
}
} else if (obj2.hasOwnProperty(i)) {
return false;
} else {
const keys1 = Object.keys(obj1);
for (; i < keys1.length; i++) {
const key = keys1[i];
if (
!obj2.hasOwnProperty(key) ||
!innerDeepEqual(obj1[key], obj2[key], strict, memos)
) {
return false;
}
}
if (keys1.length !== Object.keys(obj2).length) {
return false;
}
if (keys1.length !== Object.keys(obj2).length) {
return false;
}
return true;
}
}
}
// Expensive test
for (i = 0; i < keys.length; i++) {
const key = keys[i];
if (!innerDeepEqual(obj1[key], obj2[key], strict, memos)) {
return false;
}
}
return true;
}
function findLooseMatchingPrimitives(
primitive: unknown,
): boolean | null | undefined {
switch (typeof primitive) {
case "undefined":
return null;
case "object":
return undefined;
case "symbol":
return false;
case "string":
primitive = +primitive;
case "number":
if (Number.isNaN(primitive)) {
return false;
}
}
return true;
}
function setMightHaveLoosePrim(
set1: Set<unknown>,
set2: Set<unknown>,
primitive: any,
) {
const altValue = findLooseMatchingPrimitives(primitive);
if (altValue != null) return altValue;
return set2.has(altValue) && !set1.has(altValue);
}
function setHasEqualElement(
set: any,
val1: any,
strict: boolean,
memos: Memo,
): boolean {
for (const val2 of set) {
if (innerDeepEqual(val1, val2, strict, memos)) {
set.delete(val2);
return true;
}
}
return false;
}
function setEquiv(set1: any, set2: any, strict: boolean, memos: Memo): boolean {
let set = null;
for (const item of set1) {
if (typeof item === "object" && item !== null) {
if (set === null) {
// What is SafeSet from primordials?
// set = new SafeSet();
set = new Set();
}
set.add(item);
} else if (!set2.has(item)) {
if (strict) return false;
if (!setMightHaveLoosePrim(set1, set2, item)) {
return false;
}
if (set === null) {
set = new Set();
}
set.add(item);
}
}
if (set !== null) {
for (const item of set2) {
if (typeof item === "object" && item !== null) {
if (!setHasEqualElement(set, item, strict, memos)) return false;
} else if (
!strict &&
!set1.has(item) &&
!setHasEqualElement(set, item, strict, memos)
) {
return false;
}
}
return set.size === 0;
}
return true;
}
// TODO(standvpmnt): add types for argument
function mapMightHaveLoosePrimitive(
map1: Map<unknown, unknown>,
map2: Map<unknown, unknown>,
primitive: any,
item: any,
memos: Memo,
): boolean {
const altValue = findLooseMatchingPrimitives(primitive);
if (altValue != null) {
return altValue;
}
const curB = map2.get(altValue);
if (
(curB === undefined && !map2.has(altValue)) ||
!innerDeepEqual(item, curB, false, memo)
) {
return false;
}
return !map1.has(altValue) && innerDeepEqual(item, curB, false, memos);
}
function mapEquiv(map1: any, map2: any, strict: boolean, memos: Memo): boolean {
let set = null;
for (const { 0: key, 1: item1 } of map1) {
if (typeof key === "object" && key !== null) {
if (set === null) {
set = new Set();
}
set.add(key);
} else {
const item2 = map2.get(key);
if (
(
(item2 === undefined && !map2.has(key)) ||
!innerDeepEqual(item1, item2, strict, memos)
)
) {
if (strict) return false;
if (!mapMightHaveLoosePrimitive(map1, map2, key, item1, memos)) {
return false;
}
if (set === null) {
set = new Set();
}
set.add(key);
}
}
}
if (set !== null) {
for (const { 0: key, 1: item } of map2) {
if (typeof key === "object" && key !== null) {
if (!mapHasEqualEntry(set, map1, key, item, strict, memos)) {
return false;
}
} else if (
!strict && (!map1.has(key) ||
!innerDeepEqual(map1.get(key), item, false, memos)) &&
!mapHasEqualEntry(set, map1, key, item, false, memos)
) {
return false;
}
}
return set.size === 0;
}
return true;
}
function mapHasEqualEntry(
set: any,
map: any,
key1: any,
item1: any,
strict: boolean,
memos: Memo,
): boolean {
for (const key2 of set) {
if (
innerDeepEqual(key1, key2, strict, memos) &&
innerDeepEqual(item1, map.get(key2), strict, memos)
) {
set.delete(key2);
return true;
}
}
return false;
}