tools/utils/parse-stack.ts
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;
}