deno/cli/js/error_stack.ts
Nayeem Rahman 0ea6eb83a9
refactor(core/js_error): Align JSStackFrame with CallSite (#4715)
Renames and adds missing fields to JSStackFrame from CallSite. Fixes #4705.

Cleans up base changes for line and column numbers.
2020-04-13 10:54:16 -04:00

270 lines
7.4 KiB
TypeScript

// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
// Some of the code here is adapted directly from V8 and licensed under a BSD
// style license available here: https://github.com/v8/v8/blob/24886f2d1c565287d33d71e4109a53bf0b54b75c/LICENSE.v8
import * as colors from "./colors.ts";
import { applySourceMap, Location } from "./ops/errors.ts";
import { assert } from "./util.ts";
import { exposeForTest } from "./internals.ts";
function patchCallSite(callSite: CallSite, location: Location): CallSite {
return {
getThis(): unknown {
return callSite.getThis();
},
getTypeName(): string | null {
return callSite.getTypeName();
},
getFunction(): Function | null {
return callSite.getFunction();
},
getFunctionName(): string | null {
return callSite.getFunctionName();
},
getMethodName(): string | null {
return callSite.getMethodName();
},
getFileName(): string | null {
return location.fileName;
},
getLineNumber(): number {
return location.lineNumber;
},
getColumnNumber(): number {
return location.columnNumber;
},
getEvalOrigin(): string | null {
return callSite.getEvalOrigin();
},
isToplevel(): boolean | null {
return callSite.isToplevel();
},
isEval(): boolean {
return callSite.isEval();
},
isNative(): boolean {
return callSite.isNative();
},
isConstructor(): boolean {
return callSite.isConstructor();
},
isAsync(): boolean {
return callSite.isAsync();
},
isPromiseAll(): boolean {
return callSite.isPromiseAll();
},
getPromiseIndex(): number | null {
return callSite.getPromiseIndex();
},
};
}
function getMethodCall(callSite: CallSite): string {
let result = "";
const typeName = callSite.getTypeName();
const methodName = callSite.getMethodName();
const functionName = callSite.getFunctionName();
if (functionName) {
if (typeName) {
const startsWithTypeName = functionName.startsWith(typeName);
if (!startsWithTypeName) {
result += `${typeName}.`;
}
}
result += functionName;
if (methodName) {
if (!functionName.endsWith(methodName)) {
result += ` [as ${methodName}]`;
}
}
} else {
if (typeName) {
result += `${typeName}.`;
}
if (methodName) {
result += methodName;
} else {
result += "<anonymous>";
}
}
return result;
}
function getFileLocation(callSite: CallSite, isInternal = false): string {
const cyan = isInternal ? colors.gray : colors.cyan;
const yellow = isInternal ? colors.gray : colors.yellow;
const black = isInternal ? colors.gray : (s: string): string => s;
if (callSite.isNative()) {
return cyan("native");
}
let result = "";
const fileName = callSite.getFileName();
if (!fileName && callSite.isEval()) {
const evalOrigin = callSite.getEvalOrigin();
assert(evalOrigin != null);
result += cyan(`${evalOrigin}, `);
}
if (fileName) {
result += cyan(fileName);
} else {
result += cyan("<anonymous>");
}
const lineNumber = callSite.getLineNumber();
if (lineNumber != null) {
result += `${black(":")}${yellow(lineNumber.toString())}`;
const columnNumber = callSite.getColumnNumber();
if (columnNumber != null) {
result += `${black(":")}${yellow(columnNumber.toString())}`;
}
}
return result;
}
function callSiteToString(callSite: CallSite, isInternal = false): string {
const cyan = isInternal ? colors.gray : colors.cyan;
const black = isInternal ? colors.gray : (s: string): string => s;
let result = "";
const functionName = callSite.getFunctionName();
const isTopLevel = callSite.isToplevel();
const isAsync = callSite.isAsync();
const isPromiseAll = callSite.isPromiseAll();
const isConstructor = callSite.isConstructor();
const isMethodCall = !(isTopLevel || isConstructor);
if (isAsync) {
result += colors.gray("async ");
}
if (isPromiseAll) {
result += colors.bold(
colors.italic(black(`Promise.all (index ${callSite.getPromiseIndex()})`))
);
return result;
}
if (isMethodCall) {
result += colors.bold(colors.italic(black(getMethodCall(callSite))));
} else if (isConstructor) {
result += colors.gray("new ");
if (functionName) {
result += colors.bold(colors.italic(black(functionName)));
} else {
result += cyan("<anonymous>");
}
} else if (functionName) {
result += colors.bold(colors.italic(black(functionName)));
} else {
result += getFileLocation(callSite, isInternal);
return result;
}
result += ` ${black("(")}${getFileLocation(callSite, isInternal)}${black(
")"
)}`;
return result;
}
interface CallSiteEval {
this: unknown;
typeName: string | null;
function: Function | null;
functionName: string | null;
methodName: string | null;
fileName: string | null;
lineNumber: number | null;
columnNumber: number | null;
evalOrigin: string | null;
isToplevel: boolean | null;
isEval: boolean;
isNative: boolean;
isConstructor: boolean;
isAsync: boolean;
isPromiseAll: boolean;
promiseIndex: number | null;
}
function evaluateCallSite(callSite: CallSite): CallSiteEval {
return {
this: callSite.getThis(),
typeName: callSite.getTypeName(),
function: callSite.getFunction(),
functionName: callSite.getFunctionName(),
methodName: callSite.getMethodName(),
fileName: callSite.getFileName(),
lineNumber: callSite.getLineNumber(),
columnNumber: callSite.getColumnNumber(),
evalOrigin: callSite.getEvalOrigin(),
isToplevel: callSite.isToplevel(),
isEval: callSite.isEval(),
isNative: callSite.isNative(),
isConstructor: callSite.isConstructor(),
isAsync: callSite.isAsync(),
isPromiseAll: callSite.isPromiseAll(),
promiseIndex: callSite.getPromiseIndex(),
};
}
function prepareStackTrace(
error: Error,
structuredStackTrace: CallSite[]
): string {
Object.defineProperties(error, {
__callSiteEvals: { value: [] },
__formattedFrames: { value: [] },
});
const errorString =
`${error.name}: ${error.message}\n` +
structuredStackTrace
.map(
(callSite): CallSite => {
const fileName = callSite.getFileName();
const lineNumber = callSite.getLineNumber();
const columnNumber = callSite.getColumnNumber();
if (fileName && lineNumber != null && columnNumber != null) {
return patchCallSite(
callSite,
applySourceMap({
fileName,
lineNumber,
columnNumber,
})
);
}
return callSite;
}
)
.map((callSite): string => {
// @ts-ignore
error.__callSiteEvals.push(Object.freeze(evaluateCallSite(callSite)));
const isInternal =
callSite.getFileName()?.startsWith("$deno$") ?? false;
const string = callSiteToString(callSite, isInternal);
// @ts-ignore
error.__formattedFrames.push(string);
return ` at ${colors.stripColor(string)}`;
})
.join("\n");
// @ts-ignore
Object.freeze(error.__callSiteEvals);
// @ts-ignore
Object.freeze(error.__formattedFrames);
return errorString;
}
// @internal
export function setPrepareStackTrace(ErrorConstructor: typeof Error): void {
ErrorConstructor.prepareStackTrace = prepareStackTrace;
}
exposeForTest("setPrepareStackTrace", setPrepareStackTrace);