123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- /**
- * Copyright (c) Facebook, Inc. and its 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-local
- */
- 'use 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 = 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 = {};
- 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: 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;
|