forked from Fediversity/fediversity.eu
318 lines
7.5 KiB
JavaScript
318 lines
7.5 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const Types = require('./types');
|
||
|
|
||
|
|
||
|
const internals = {
|
||
|
mismatched: null
|
||
|
};
|
||
|
|
||
|
|
||
|
module.exports = function (obj, ref, options) {
|
||
|
|
||
|
options = Object.assign({ prototype: true }, options);
|
||
|
|
||
|
return !!internals.isDeepEqual(obj, ref, options, []);
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.isDeepEqual = function (obj, ref, options, seen) {
|
||
|
|
||
|
if (obj === ref) { // Copied from Deep-eql, copyright(c) 2013 Jake Luer, jake@alogicalparadox.com, MIT Licensed, https://github.com/chaijs/deep-eql
|
||
|
return obj !== 0 || 1 / obj === 1 / ref;
|
||
|
}
|
||
|
|
||
|
const type = typeof obj;
|
||
|
|
||
|
if (type !== typeof ref) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (obj === null ||
|
||
|
ref === null) {
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (type === 'function') {
|
||
|
if (!options.deepFunction ||
|
||
|
obj.toString() !== ref.toString()) {
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Continue as object
|
||
|
}
|
||
|
else if (type !== 'object') {
|
||
|
return obj !== obj && ref !== ref; // NaN
|
||
|
}
|
||
|
|
||
|
const instanceType = internals.getSharedType(obj, ref, !!options.prototype);
|
||
|
switch (instanceType) {
|
||
|
case Types.buffer:
|
||
|
return Buffer && Buffer.prototype.equals.call(obj, ref); // $lab:coverage:ignore$
|
||
|
case Types.promise:
|
||
|
return obj === ref;
|
||
|
case Types.regex:
|
||
|
return obj.toString() === ref.toString();
|
||
|
case internals.mismatched:
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
for (let i = seen.length - 1; i >= 0; --i) {
|
||
|
if (seen[i].isSame(obj, ref)) {
|
||
|
return true; // If previous comparison failed, it would have stopped execution
|
||
|
}
|
||
|
}
|
||
|
|
||
|
seen.push(new internals.SeenEntry(obj, ref));
|
||
|
|
||
|
try {
|
||
|
return !!internals.isDeepEqualObj(instanceType, obj, ref, options, seen);
|
||
|
}
|
||
|
finally {
|
||
|
seen.pop();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.getSharedType = function (obj, ref, checkPrototype) {
|
||
|
|
||
|
if (checkPrototype) {
|
||
|
if (Object.getPrototypeOf(obj) !== Object.getPrototypeOf(ref)) {
|
||
|
return internals.mismatched;
|
||
|
}
|
||
|
|
||
|
return Types.getInternalProto(obj);
|
||
|
}
|
||
|
|
||
|
const type = Types.getInternalProto(obj);
|
||
|
if (type !== Types.getInternalProto(ref)) {
|
||
|
return internals.mismatched;
|
||
|
}
|
||
|
|
||
|
return type;
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.valueOf = function (obj) {
|
||
|
|
||
|
const objValueOf = obj.valueOf;
|
||
|
if (objValueOf === undefined) {
|
||
|
return obj;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
return objValueOf.call(obj);
|
||
|
}
|
||
|
catch (err) {
|
||
|
return err;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.hasOwnEnumerableProperty = function (obj, key) {
|
||
|
|
||
|
return Object.prototype.propertyIsEnumerable.call(obj, key);
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.isSetSimpleEqual = function (obj, ref) {
|
||
|
|
||
|
for (const entry of Set.prototype.values.call(obj)) {
|
||
|
if (!Set.prototype.has.call(ref, entry)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.isDeepEqualObj = function (instanceType, obj, ref, options, seen) {
|
||
|
|
||
|
const { isDeepEqual, valueOf, hasOwnEnumerableProperty } = internals;
|
||
|
const { keys, getOwnPropertySymbols } = Object;
|
||
|
|
||
|
if (instanceType === Types.array) {
|
||
|
if (options.part) {
|
||
|
|
||
|
// Check if any index match any other index
|
||
|
|
||
|
for (const objValue of obj) {
|
||
|
for (const refValue of ref) {
|
||
|
if (isDeepEqual(objValue, refValue, options, seen)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
if (obj.length !== ref.length) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < obj.length; ++i) {
|
||
|
if (!isDeepEqual(obj[i], ref[i], options, seen)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
else if (instanceType === Types.set) {
|
||
|
if (obj.size !== ref.size) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!internals.isSetSimpleEqual(obj, ref)) {
|
||
|
|
||
|
// Check for deep equality
|
||
|
|
||
|
const ref2 = new Set(Set.prototype.values.call(ref));
|
||
|
for (const objEntry of Set.prototype.values.call(obj)) {
|
||
|
if (ref2.delete(objEntry)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
let found = false;
|
||
|
for (const refEntry of ref2) {
|
||
|
if (isDeepEqual(objEntry, refEntry, options, seen)) {
|
||
|
ref2.delete(refEntry);
|
||
|
found = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!found) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (instanceType === Types.map) {
|
||
|
if (obj.size !== ref.size) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
for (const [key, value] of Map.prototype.entries.call(obj)) {
|
||
|
if (value === undefined && !Map.prototype.has.call(ref, key)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!isDeepEqual(value, Map.prototype.get.call(ref, key), options, seen)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (instanceType === Types.error) {
|
||
|
|
||
|
// Always check name and message
|
||
|
|
||
|
if (obj.name !== ref.name ||
|
||
|
obj.message !== ref.message) {
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check .valueOf()
|
||
|
|
||
|
const valueOfObj = valueOf(obj);
|
||
|
const valueOfRef = valueOf(ref);
|
||
|
if ((obj !== valueOfObj || ref !== valueOfRef) &&
|
||
|
!isDeepEqual(valueOfObj, valueOfRef, options, seen)) {
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Check properties
|
||
|
|
||
|
const objKeys = keys(obj);
|
||
|
if (!options.part &&
|
||
|
objKeys.length !== keys(ref).length &&
|
||
|
!options.skip) {
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
let skipped = 0;
|
||
|
for (const key of objKeys) {
|
||
|
if (options.skip &&
|
||
|
options.skip.includes(key)) {
|
||
|
|
||
|
if (ref[key] === undefined) {
|
||
|
++skipped;
|
||
|
}
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (!hasOwnEnumerableProperty(ref, key)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!isDeepEqual(obj[key], ref[key], options, seen)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!options.part &&
|
||
|
objKeys.length - skipped !== keys(ref).length) {
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Check symbols
|
||
|
|
||
|
if (options.symbols !== false) { // Defaults to true
|
||
|
const objSymbols = getOwnPropertySymbols(obj);
|
||
|
const refSymbols = new Set(getOwnPropertySymbols(ref));
|
||
|
|
||
|
for (const key of objSymbols) {
|
||
|
if (!options.skip ||
|
||
|
!options.skip.includes(key)) {
|
||
|
|
||
|
if (hasOwnEnumerableProperty(obj, key)) {
|
||
|
if (!hasOwnEnumerableProperty(ref, key)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
if (!isDeepEqual(obj[key], ref[key], options, seen)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
else if (hasOwnEnumerableProperty(ref, key)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
refSymbols.delete(key);
|
||
|
}
|
||
|
|
||
|
for (const key of refSymbols) {
|
||
|
if (hasOwnEnumerableProperty(ref, key)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
|
||
|
internals.SeenEntry = class {
|
||
|
|
||
|
constructor(obj, ref) {
|
||
|
|
||
|
this.obj = obj;
|
||
|
this.ref = ref;
|
||
|
}
|
||
|
|
||
|
isSame(obj, ref) {
|
||
|
|
||
|
return this.obj === obj && this.ref === ref;
|
||
|
}
|
||
|
};
|