Files
react-native/Libraries/Utilities/stringifySafe.js
T
George Zahariev 20d9d3aa6a Fixes to non-product code (e.g React Native)
Summary:
We are working on making the empty object literal `{}` have the type `{}` - i.e. exact empty object - rather than being unsealed.

Some manual fixes, in particular to React Native code, which is used and can be synced to other repos (e.g. WWW).

With these changes, error diff in Xplat is down to ~1990 errors

Note that after I roll out `exact_empty_objects`, I'll codemod all the `{...null}` (the only way to get an exact empty object currently) back to `{}`

Changelog: [Internal]

Reviewed By: SamChou19815

Differential Revision: D36142838

fbshipit-source-id: 054caf370db230f42a4c5f5706c88979ef246537
2022-05-04 16:13:07 -07:00

123 lines
3.7 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/
import invariant from 'invariant';
/**
* Tries to stringify with JSON.stringify and toString, but catches exceptions
* (e.g. from circular objects) and always returns a string and never throws.
*/
export function createStringifySafeWithLimits(limits: {|
maxDepth?: number,
maxStringLimit?: number,
maxArrayLimit?: number,
maxObjectKeysLimit?: number,
|}): mixed => string {
const {
maxDepth = Number.POSITIVE_INFINITY,
maxStringLimit = Number.POSITIVE_INFINITY,
maxArrayLimit = Number.POSITIVE_INFINITY,
maxObjectKeysLimit = Number.POSITIVE_INFINITY,
} = limits;
const stack = [];
function replacer(key: string, value: mixed): mixed {
while (stack.length && this !== stack[0]) {
stack.shift();
}
if (typeof value === 'string') {
const truncatedString = '...(truncated)...';
if (value.length > maxStringLimit + truncatedString.length) {
return value.substring(0, maxStringLimit) + truncatedString;
}
return value;
}
if (typeof value !== 'object' || value === null) {
return value;
}
let retval:
| string
| {+[string]: mixed}
| $TEMPORARY$object<{'...(truncated keys)...': number}> = value;
if (Array.isArray(value)) {
if (stack.length >= maxDepth) {
retval = `[ ... array with ${value.length} values ... ]`;
} else if (value.length > maxArrayLimit) {
retval = value
.slice(0, maxArrayLimit)
.concat([
`... extra ${value.length - maxArrayLimit} values truncated ...`,
]);
}
} else {
// Add refinement after Array.isArray call.
invariant(typeof value === 'object', 'This was already found earlier');
let keys = Object.keys(value);
if (stack.length >= maxDepth) {
retval = `{ ... object with ${keys.length} keys ... }`;
} else if (keys.length > maxObjectKeysLimit) {
// Return a sample of the keys.
retval = ({}: {[string]: mixed});
for (let k of keys.slice(0, maxObjectKeysLimit)) {
retval[k] = value[k];
}
const truncatedKey = '...(truncated keys)...';
retval[truncatedKey] = keys.length - maxObjectKeysLimit;
}
}
stack.unshift(retval);
return retval;
}
return function stringifySafe(arg: mixed): string {
if (arg === undefined) {
return 'undefined';
} else if (arg === null) {
return 'null';
} else if (typeof arg === 'function') {
try {
return arg.toString();
} catch (e) {
return '[function unknown]';
}
} else if (arg instanceof Error) {
return arg.name + ': ' + arg.message;
} else {
// Perform a try catch, just in case the object has a circular
// reference or stringify throws for some other reason.
try {
const ret = JSON.stringify(arg, replacer);
if (ret === undefined) {
return '["' + typeof arg + '" failed to stringify]';
}
return ret;
} catch (e) {
if (typeof arg.toString === 'function') {
try {
// $FlowFixMe[incompatible-use] : toString shouldn't take any arguments in general.
return arg.toString();
} catch (E) {}
}
}
}
return '["' + typeof arg + '" failed to stringify]';
};
}
const stringifySafe: mixed => string = createStringifySafeWithLimits({
maxDepth: 10,
maxStringLimit: 100,
maxArrayLimit: 50,
maxObjectKeysLimit: 50,
});
export default stringifySafe;