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