tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

error.mjs (8431B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 /* eslint no-shadow: ["error", { "allow": ["name", "location", "frames"] }] */
      6 
      7 import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs";
      8 import {
      9  div,
     10  span,
     11 } from "resource://devtools/client/shared/vendor/react-dom-factories.mjs";
     12 
     13 import { wrapRender } from "./rep-utils.mjs";
     14 import { cleanFunctionName } from "./function.mjs";
     15 import { isLongString } from "./string.mjs";
     16 import { MODE } from "./constants.mjs";
     17 
     18 const IGNORED_SOURCE_URLS = ["debugger eval code"];
     19 
     20 /**
     21 * Renders Error objects.
     22 */
     23 ErrorRep.propTypes = {
     24  object: PropTypes.object.isRequired,
     25  mode: PropTypes.oneOf(Object.values(MODE)),
     26  // An optional function that will be used to render the Error stacktrace.
     27  renderStacktrace: PropTypes.func,
     28  shouldRenderTooltip: PropTypes.bool,
     29 };
     30 
     31 /**
     32 * Render an Error object.
     33 * The customFormat prop allows to print a simplified view of the object, with only the
     34 * message and the stacktrace, e.g.:
     35 *      Error: "blah"
     36 *          <anonymous> debugger eval code:1
     37 *
     38 * The customFormat prop will only be taken into account if the mode isn't tiny and the
     39 * depth is 0. This is because we don't want error in previews or in object to be
     40 * displayed unlike other objects:
     41 *      - Object { err: Error }
     42 *      - â–¼ {
     43 *            err: Error: "blah"
     44 *        }
     45 */
     46 function ErrorRep(props) {
     47  const { object, mode, shouldRenderTooltip, depth } = props;
     48  const preview = object.preview;
     49  const customFormat =
     50    props.customFormat && mode !== MODE.TINY && mode !== MODE.HEADER && !depth;
     51 
     52  const name = getErrorName(props);
     53  const errorTitle =
     54    mode === MODE.TINY || mode === MODE.HEADER ? name : `${name}: `;
     55  const content = [];
     56 
     57  if (customFormat) {
     58    content.push(errorTitle);
     59  } else {
     60    content.push(span({ className: "objectTitle", key: "title" }, errorTitle));
     61  }
     62 
     63  if (mode !== MODE.TINY && mode !== MODE.HEADER) {
     64    content.push(
     65      props.Rep({
     66        ...props,
     67        key: "message",
     68        object: preview.message,
     69        mode: props.mode || MODE.TINY,
     70        useQuotes: false,
     71      })
     72    );
     73  }
     74  const renderStack = preview.stack && customFormat;
     75  if (renderStack) {
     76    const stacktrace = props.renderStacktrace
     77      ? props.renderStacktrace(parseStackString(preview.stack))
     78      : getStacktraceElements(props, preview);
     79    content.push(stacktrace);
     80  }
     81 
     82  const renderCause = customFormat && preview.hasOwnProperty("cause");
     83  if (renderCause) {
     84    content.push(getCauseElement(props, preview));
     85  }
     86 
     87  return span(
     88    {
     89      "data-link-actor-id": object.actor,
     90      className: `objectBox-stackTrace ${
     91        customFormat ? "reps-custom-format" : ""
     92      }`,
     93      title: shouldRenderTooltip ? `${name}: "${preview.message}"` : null,
     94    },
     95    ...content
     96  );
     97 }
     98 
     99 function getErrorName(props) {
    100  const { object } = props;
    101  const preview = object.preview;
    102 
    103  let name;
    104  if (typeof preview?.name === "string" && preview.kind) {
    105    switch (preview.kind) {
    106      case "Error":
    107        name = preview.name;
    108        break;
    109      case "DOMException":
    110        name = preview.kind;
    111        break;
    112      default:
    113        throw new Error("Unknown preview kind for the Error rep.");
    114    }
    115  } else {
    116    name = "Error";
    117  }
    118 
    119  return name;
    120 }
    121 
    122 /**
    123 * Returns a React element reprensenting the Error stacktrace, i.e.
    124 * transform error.stack from:
    125 *
    126 * ```
    127 * semicolon@debugger eval code:1:109
    128 * jkl@debugger eval code:1:63
    129 * asdf@debugger eval code:1:28
    130 *
    131 * @debugger eval code:1:227
    132 * ```
    133 *
    134 * Into a column layout:
    135 *
    136 * ```
    137 * semicolon  (<anonymous>:8:10)
    138 * jkl        (<anonymous>:5:10)
    139 * asdf       (<anonymous>:2:10)
    140 *            (<anonymous>:11:1)
    141 * ```
    142 */
    143 function getStacktraceElements(props, preview) {
    144  const stack = [];
    145  if (!preview.stack) {
    146    return stack;
    147  }
    148 
    149  parseStackString(preview.stack).forEach((frame, index) => {
    150    let onLocationClick;
    151    const { filename, lineNumber, columnNumber, functionName, location } =
    152      frame;
    153 
    154    if (
    155      props.onViewSourceInDebugger &&
    156      !IGNORED_SOURCE_URLS.includes(filename)
    157    ) {
    158      onLocationClick = e => {
    159        // Don't trigger ObjectInspector expand/collapse.
    160        e.stopPropagation();
    161        props.onViewSourceInDebugger({
    162          url: filename,
    163          line: lineNumber,
    164          column: columnNumber,
    165        });
    166      };
    167    }
    168 
    169    stack.push(
    170      "\t",
    171      span(
    172        {
    173          key: `fn${index}`,
    174          className: "objectBox-stackTrace-fn",
    175        },
    176        cleanFunctionName(functionName)
    177      ),
    178      " ",
    179      span(
    180        {
    181          key: `location${index}`,
    182          className: "objectBox-stackTrace-location",
    183          onClick: onLocationClick,
    184          title: onLocationClick
    185            ? `View source in debugger → ${location}`
    186            : undefined,
    187        },
    188        location
    189      ),
    190      "\n"
    191    );
    192  });
    193 
    194  return span(
    195    {
    196      key: "stack",
    197      className: "objectBox-stackTrace-grid",
    198    },
    199    stack
    200  );
    201 }
    202 
    203 /**
    204 * Returns a React element representing the cause of the Error i.e. the `cause`
    205 * property in the second parameter of the Error constructor (`new Error("message", { cause })`)
    206 *
    207 * Example:
    208 * Caused by: Error: original error
    209 */
    210 function getCauseElement(props, preview) {
    211  return div(
    212    {
    213      key: "cause-container",
    214      className: "error-rep-cause",
    215    },
    216    "Caused by: ",
    217    props.Rep({
    218      ...props,
    219      key: "cause",
    220      object: preview.cause,
    221      mode: props.mode || MODE.TINY,
    222    })
    223  );
    224 }
    225 
    226 /**
    227 * Parse a string that should represent a stack trace and returns an array of
    228 * the frames. The shape of the frames are extremely important as they can then
    229 * be processed here or in the toolbox by other components.
    230 *
    231 * @param {string} stack
    232 * @returns {Array} Array of frames, which are object with the following shape:
    233 *                  - {String} filename
    234 *                  - {String} functionName
    235 *                  - {String} location
    236 *                  - {Number} columnNumber
    237 *                  - {Number} lineNumber
    238 */
    239 function parseStackString(stack) {
    240  if (!stack) {
    241    return [];
    242  }
    243 
    244  const isStacktraceALongString = isLongString(stack);
    245  const stackString = isStacktraceALongString ? stack.initial : stack;
    246 
    247  if (typeof stackString !== "string") {
    248    return [];
    249  }
    250 
    251  const res = [];
    252  stackString.split("\n").forEach((frame, index, frames) => {
    253    if (!frame) {
    254      // Skip any blank lines
    255      return;
    256    }
    257 
    258    // If the stacktrace is a longString, don't include the last frame in the
    259    // array, since it is certainly incomplete.
    260    // Can be removed when https://bugzilla.mozilla.org/show_bug.cgi?id=1448833
    261    // is fixed.
    262    if (isStacktraceALongString && index === frames.length - 1) {
    263      return;
    264    }
    265 
    266    let functionName;
    267    let location;
    268 
    269    // Retrieve the index of the first @ to split the frame string.
    270    const atCharIndex = frame.indexOf("@");
    271    if (atCharIndex > -1) {
    272      functionName = frame.slice(0, atCharIndex);
    273      location = frame.slice(atCharIndex + 1);
    274    }
    275 
    276    if (location && location.includes(" -> ")) {
    277      // If the resource was loaded by base-loader.sys.mjs, the location looks like:
    278      // resource://devtools/shared/base-loader.sys.mjs -> resource://path/to/file.js .
    279      // What's needed is only the last part after " -> ".
    280      location = location.split(" -> ").pop();
    281    }
    282 
    283    if (!functionName) {
    284      functionName = "<anonymous>";
    285    }
    286 
    287    // Given the input: "scriptLocation:2:100"
    288    // Result:
    289    // ["scriptLocation:2:100", "scriptLocation", "2", "100"]
    290    const locationParts = location
    291      ? location.match(/^(.*):(\d+):(\d+)$/)
    292      : null;
    293 
    294    if (location && locationParts) {
    295      const [, filename, line, column] = locationParts;
    296      res.push({
    297        filename,
    298        functionName,
    299        location,
    300        columnNumber: Number(column),
    301        lineNumber: Number(line),
    302      });
    303    }
    304  });
    305 
    306  return res;
    307 }
    308 
    309 // Registration
    310 function supportsObject(object) {
    311  return (
    312    object?.isError ||
    313    object?.class === "DOMException" ||
    314    object?.class === "Exception"
    315  );
    316 }
    317 
    318 const rep = wrapRender(ErrorRep);
    319 
    320 // Exports from this module
    321 export { rep, supportsObject };