meteor/meteor

View on GitHub
tools/utils/parse-stack.ts

Summary

Maintainability
B
5 hrs
Test Coverage
type ParsedStackFrame = {
  /**
   * filename as it appears in the stack
   */
  file: string;

  /**
   * 1-indexed line in the file
   */
  line: number | null;

  /**
   * 1-indexed column in the line
   */
  column: number | null;

  /**
   * name of the function in the frame
   */
  func: string | null;
};

/**
 * Returns the stack associated with an error as an array.
 * More recently called functions appear first.
 * 
 * Accomplishes this by parsing the text representation of the stack
 * with regular expressions. Unlikely to work anywhere but v8.
 * 
 * If a function on the stack has been marked with mark(), will not
 * return anything past that function. We call this the "user portion"
 * of the stack.
 */
export function parse(err: Error): {
  insideFiber?: ParsedStackFrame[],
  outsideFiber?: ParsedStackFrame[],
} {
  const stack = err.stack;
  if (typeof stack !== "string") {
    return {};
  }

  // at least the first line is the exception
  const frames = stack.split("\n").slice(1)
    // longjohn adds lines of the form '---' (45 times) to separate
    // the trace across async boundaries. It's not clear if we need to
    // separate the trace in the same way we do for future boundaries below
    // (it's not clear that that code is still useful either)
    // so for now, we'll just remove such lines
    .filter(f => ! f.match(/^\-{45}$/));
  
  // "    - - - - -"
  // This is something added when you throw an Error through a Future. The
  // stack above the dashes is the stack of the 'wait' call; the stack below
  // is the stack inside the fiber where the Error is originally
  // constructed.
  // XXX This code assumes that the stack trace can only be split once. It's not
  // clear whether this can happen multiple times.
  const indexOfFiberSplit = frames.indexOf('    - - - - -');

  if (indexOfFiberSplit === -1) {
    // This is a normal stack trace, not a split fiber stack trace
    return {
      outsideFiber: parseStackFrames(frames)
    }
  }

  // If this is a split stack trace from a future, parse the frames above and
  // below the split separately.
  const outsideFiber = parseStackFrames(frames);
  const insideFiber = parseStackFrames(frames.slice(indexOfFiberSplit + 1));

  return {
    insideFiber,
    outsideFiber
  };
}

/**
 * Decorator. Mark the point at which a stack trace returned by
 * parse() should stop: no frames earlier than this point will be
 * included in the parsed stack. Confusingly, in the argot of the
 * times, you'd say that frames "higher up" than this or "above" this
 * will not be returned, but you'd also say that those frames are "at
 * the bottom of the stack". Frames below the bottom are the outer
 * context of the framework running the user's code.
 */
export function markBottom(f: Function, context: any) {
  /* eslint-disable camelcase */
  return function __bottom_mark__() {
    // @ts-ignore: Implicit this
    return f.apply(context || this, arguments);
  };
  /* eslint-enable camelcase */
}

/**
 * Decorator. Mark the point at which a stack trace returned by
 * parse() should begin: no frames later than this point will be
 * included in the parsed stack. The opposite of markBottom().
 * Frames above the top are helper functions defined by the
 * framework and executed by user code whose internal behavior
 * should not be exposed.
 */
export function markTop(f: Function, context: any) {
  /* eslint-disable camelcase */
  return function __top_mark__() {
    // @ts-ignore: Implicit this
    return f.apply(context || this, arguments);
  };
  /* eslint-enable camelcase */
}

function parseStackFrames(frames: string[]): ParsedStackFrame[] {
  let stop = false;
  let parsedFrames: ParsedStackFrame[] = [];

  frames.some(frame => {
    if (stop) {
      return true;
    }

    let m;

    /* eslint-disable max-len */
    if (m = frame.match(/^\s*at\s*((new )?.+?)\s*(\[as\s*([^\]]*)\]\s*)?\((.*?)(:(\d+))?(:(\d+))?\)\s*$/)) {
      // https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
      // "    at My.Function (/path/to/myfile.js:532:39)"
      // "    at Array.forEach (native)"
      // "    at new My.Class (file.js:1:2)"
      // "    at [object Object].main.registerCommand.name [as func] (meteor/tools/commands.js:1225:19)"
      // "    at __top_mark__ [as matchErr] (meteor/tools/parse-stack.js:82:14)"
      //
      // In that last example, it is not at all clear to me what the
      // 'as' stanza refers to, but it is in m[3] if you find a use for it.
      if (m[1].match(/(?:^|\.)__top_mark__$/)) {
        // m[1] could be Object.__top_mark__ or something like that
        // depending on where exactly you put the function returned by
        // markTop
        parsedFrames = [];
        return;
      }

      if (m[1].match(/(?:^|\.)__bottom_mark__$/)) {
        return stop = true;
      }

      parsedFrames.push({
        func: m[1],
        file: m[5],
        line: m[7] ? +m[7] : null,
        column: m[9] ? +m[9] : null
      });
      return;
    }
    /* eslint-enable max-len */

    if (m = frame.match(/^\s*at\s+(.+?)(:(\d+))?(:(\d+))?\s*$/)) {
      // "    at /path/to/myfile.js:532:39"
      parsedFrames.push({
        file: m[1],
        line: m[3] ? +m[3] : null,
        column: m[5] ? +m[5] : null,
        func: null,
      });
      return;
    }

    if (m = frame.match(/^\s*-\s*-\s*-\s*-\s*-\s*$/)) {
      // Stop parsing if we reach a stack split from a Future
      return stop = true;
    }

    if (frame.startsWith(" => awaited here:")) {
      // The meteor-promise library inserts " => awaited here:" lines to
      // indicate async boundaries.
      return stop = true;
    }

    if (parsedFrames.length === 0) {
      // We haven't found any stack frames, so probably we have newlines in the
      // error message. Just skip this line.
      return;
    }

    throw new Error(`Couldn't parse stack frame: '${frame}'`);
  });

  return parsedFrames;
}