stringifySafe.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @format
  8. * @flow strict-local
  9. */
  10. 'use strict';
  11. import invariant from 'invariant';
  12. /**
  13. * Tries to stringify with JSON.stringify and toString, but catches exceptions
  14. * (e.g. from circular objects) and always returns a string and never throws.
  15. */
  16. export function createStringifySafeWithLimits(limits: {|
  17. maxDepth?: number,
  18. maxStringLimit?: number,
  19. maxArrayLimit?: number,
  20. maxObjectKeysLimit?: number,
  21. |}): mixed => string {
  22. const {
  23. maxDepth = Number.POSITIVE_INFINITY,
  24. maxStringLimit = Number.POSITIVE_INFINITY,
  25. maxArrayLimit = Number.POSITIVE_INFINITY,
  26. maxObjectKeysLimit = Number.POSITIVE_INFINITY,
  27. } = limits;
  28. const stack = [];
  29. function replacer(key: string, value: mixed): mixed {
  30. while (stack.length && this !== stack[0]) {
  31. stack.shift();
  32. }
  33. if (typeof value === 'string') {
  34. const truncatedString = '...(truncated)...';
  35. if (value.length > maxStringLimit + truncatedString.length) {
  36. return value.substring(0, maxStringLimit) + truncatedString;
  37. }
  38. return value;
  39. }
  40. if (typeof value !== 'object' || value === null) {
  41. return value;
  42. }
  43. let retval = value;
  44. if (Array.isArray(value)) {
  45. if (stack.length >= maxDepth) {
  46. retval = `[ ... array with ${value.length} values ... ]`;
  47. } else if (value.length > maxArrayLimit) {
  48. retval = value
  49. .slice(0, maxArrayLimit)
  50. .concat([
  51. `... extra ${value.length - maxArrayLimit} values truncated ...`,
  52. ]);
  53. }
  54. } else {
  55. // Add refinement after Array.isArray call.
  56. invariant(typeof value === 'object', 'This was already found earlier');
  57. let keys = Object.keys(value);
  58. if (stack.length >= maxDepth) {
  59. retval = `{ ... object with ${keys.length} keys ... }`;
  60. } else if (keys.length > maxObjectKeysLimit) {
  61. // Return a sample of the keys.
  62. retval = {};
  63. for (let k of keys.slice(0, maxObjectKeysLimit)) {
  64. retval[k] = value[k];
  65. }
  66. const truncatedKey = '...(truncated keys)...';
  67. retval[truncatedKey] = keys.length - maxObjectKeysLimit;
  68. }
  69. }
  70. stack.unshift(retval);
  71. return retval;
  72. }
  73. return function stringifySafe(arg: mixed): string {
  74. if (arg === undefined) {
  75. return 'undefined';
  76. } else if (arg === null) {
  77. return 'null';
  78. } else if (typeof arg === 'function') {
  79. try {
  80. return arg.toString();
  81. } catch (e) {
  82. return '[function unknown]';
  83. }
  84. } else if (arg instanceof Error) {
  85. return arg.name + ': ' + arg.message;
  86. } else {
  87. // Perform a try catch, just in case the object has a circular
  88. // reference or stringify throws for some other reason.
  89. try {
  90. const ret = JSON.stringify(arg, replacer);
  91. if (ret === undefined) {
  92. return '["' + typeof arg + '" failed to stringify]';
  93. }
  94. return ret;
  95. } catch (e) {
  96. if (typeof arg.toString === 'function') {
  97. try {
  98. // $FlowFixMe: toString shouldn't take any arguments in general.
  99. return arg.toString();
  100. } catch (E) {}
  101. }
  102. }
  103. }
  104. return '["' + typeof arg + '" failed to stringify]';
  105. };
  106. }
  107. const stringifySafe: mixed => string = createStringifySafeWithLimits({
  108. maxDepth: 10,
  109. maxStringLimit: 100,
  110. maxArrayLimit: 50,
  111. maxObjectKeysLimit: 50,
  112. });
  113. export default stringifySafe;