@eagleoutice/flowr-dev
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
197 lines • 8.82 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isObjectOrArray = isObjectOrArray;
exports.isPlainObject = isPlainObject;
exports.deepMergeObject = deepMergeObject;
exports.deepMergeObjectInPlace = deepMergeObjectInPlace;
exports.compactRecord = compactRecord;
exports.deepClonePreserveUnclonable = deepClonePreserveUnclonable;
exports.looselyCompareObjects = looselyCompareObjects;
const json_1 = require("./json");
const log_1 = require("./log");
/**
* checks if `item` is an object (it may be an array, ...)
*/
function isObjectOrArray(item) {
return typeof item === 'object';
}
/**
* checks if `item` is a record with keys, i.e. an object that is neither `null` nor an array
* @see {@link isObjectOrArray} to allow arrays as well
*/
function isPlainObject(item) {
return typeof item === 'object' && item !== null && !Array.isArray(item);
}
function deepMergeObject(base, addon) {
if (!base) {
return addon;
}
else if (!addon) {
return base;
}
else if (typeof base !== 'object' || typeof addon !== 'object') {
// this case should be guarded by type guards, but in case we do not know
throw new Error('illegal types for deepMergeObject!');
}
assertSameType(base, addon);
const result = { ...base };
const baseIsArray = Array.isArray(base);
const addonIsArray = Array.isArray(addon);
if (!baseIsArray && !addonIsArray) {
deepMergeObjectWithResult(addon, base, result);
}
else if (baseIsArray && addonIsArray) {
return base.concat(addon);
}
else {
throw new Error('cannot merge object with array!');
}
return result;
}
function deepMergeObjectWithResult(addon, base, result) {
for (const key of Object.keys(addon)) {
// values that are undefined (like from a partial object) should NOT be overwritten
if (addon[key] === undefined) {
continue;
}
if (typeof addon[key] === 'object') {
if (key in base) {
result[key] = deepMergeObject(base[key], addon[key]);
}
else {
result[key] = addon[key];
}
}
else {
assertSameType(result[key], addon[key]);
result[key] = addon[key];
}
}
}
function deepMergeObjectInPlace(base, addon) {
if (!base) {
return addon;
}
else if (!addon) {
return base;
}
else if (typeof base !== 'object' || typeof addon !== 'object') {
// this case should be guarded by type guards, but in case we do not know
throw new Error('illegal types for deepMergeObjectInPlace!');
}
assertSameType(base, addon);
const baseIsArray = Array.isArray(base);
const addonIsArray = Array.isArray(addon);
if (!baseIsArray && !addonIsArray) {
deepMergeObjectWithResult(addon, base, base);
}
else if (baseIsArray && addonIsArray) {
for (const item of addon) {
(base).push(item);
}
}
else {
throw new Error('cannot merge object with array!');
}
return base;
}
function assertSameType(base, addon) {
if (base !== undefined && addon !== undefined && typeof base !== typeof addon) {
throw new Error(`cannot merge different types! ${typeof base} (${JSON.stringify(base, json_1.jsonReplacer)}) !== ${typeof addon} (${JSON.stringify(addon, json_1.jsonReplacer)})`);
}
}
/** from a record take only the keys that are not undefined */
function compactRecord(record) {
if (record === undefined) {
return undefined;
}
const result = {};
for (const key of Object.keys(record)) {
if (record[key] !== undefined) {
result[key] = record[key];
}
}
return result;
}
/**
* This is a version of a deep clone that preserves unclonable values (like functions, symbols, ...) by keeping the same reference to them.
*/
function deepClonePreserveUnclonable(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
else if (Array.isArray(obj)) {
return obj.map(deepClonePreserveUnclonable);
}
else if (obj instanceof Date) {
return new Date(obj.getTime());
}
else if (obj instanceof Map) {
return new Map(obj.entries().map(([k, v]) => [deepClonePreserveUnclonable(k), deepClonePreserveUnclonable(v)]));
}
else if (obj instanceof Set) {
return new Set(obj.values().map(deepClonePreserveUnclonable));
}
else {
const result = {};
for (const key of Object.keys(obj)) {
result[key] = deepClonePreserveUnclonable(obj[key]);
}
return result;
}
}
/**
* Compares the two passed objects deeply using the loose comparison system designed for the {@link FlowrFilter.MatchesEnrichment}. For this system in use, see {@link FlowrFilter.MatchesEnrichment} in use.
* @param obj - The real object which we want to test against.
* @param expected - The object to test the real value {@link obj} against, which should be an object in the shape of {@link obj} with each value to test for replaced by a {@link RegExp} or value to match against. The test will pass if the partial structure matches and the value at each {@link RegExp}, string or primitive location matches the corresponding regular expression. For array entries, {@link arrayMatch} determines whether every element in the array has to match the given expected value, or only some.
* @param arrayMatch - For array entries, the expected value in {@link test} is compared against each array entry in the real value. This property determines whether every element in the array has to match, or only some. If unset, this defaults to `some`.
* @param logger - The logger to use for trace debugging.
*/
function looselyCompareObjects(obj, expected, arrayMatch, logger) {
(0, log_1.expensiveTrace)(logger, () => `Comparing ${JSON.stringify(obj)} against ${JSON.stringify(expected)}`);
for (const [expectedKey, expectedValue] of Object.entries(expected)) {
const realValue = obj[expectedKey];
if (!realValue) {
(0, log_1.expensiveTrace)(logger, () => `Real value ${JSON.stringify(realValue)} does not exist for expected key ${expectedKey}`);
return false;
}
if (Array.isArray(realValue)) {
const match = typeof expectedValue === 'object' ? expectedValue instanceof RegExp ?
// if we expect a regular expression but an array is supplied, test each value
(value) => expectedValue.test(typeof value === 'string' ? value : String(value)) :
// if we expect an object that is not a regular expression, match against our expected structure
(value) => looselyCompareObjects(value, expectedValue, arrayMatch, logger) :
// in any other case (primitives!), match against the exact value
(value) => expectedValue === value;
if (!(arrayMatch === 'every' ? realValue.every(match) : realValue.some(match))) {
(0, log_1.expensiveTrace)(logger, () => `Array ${JSON.stringify(realValue)} does not match expected value ${JSON.stringify(expectedValue)} (array match ${arrayMatch})`);
return false;
}
}
else if (typeof realValue === 'object') {
// for objects, we recursively match
if (!looselyCompareObjects(realValue, expectedValue, arrayMatch, logger)) {
(0, log_1.expensiveTrace)(logger, () => `Object ${JSON.stringify(realValue)} does not match expected object ${JSON.stringify(expectedValue)}`);
return false;
}
}
else if (expectedValue instanceof RegExp) {
// for anything else, we match with our regular expression or string
// (arrays and objects are handled above, so only primitives reach this point)
const realPrimitive = realValue;
if (!expectedValue.test(typeof realPrimitive === 'string' ? realPrimitive : String(realPrimitive))) {
(0, log_1.expensiveTrace)(logger, () => `Value ${JSON.stringify(realValue)} does not match expected regular expression ${expectedValue}`);
return false;
}
}
else if (typeof expectedValue !== 'object') {
if (expectedValue !== realValue) {
(0, log_1.expensiveTrace)(logger, () => `Value ${JSON.stringify(realValue)} does not match expected string ${JSON.stringify(expectedValue)}`);
return false;
}
}
}
(0, log_1.expensiveTrace)(logger, () => `Object ${JSON.stringify(obj)} matches ${JSON.stringify(expected)}`);
return true;
}
//# sourceMappingURL=objects.js.map