deno/cli/js/error_stack.ts
2020-02-25 09:14:27 -05:00

276 lines
7.2 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 { sendSync } from "./dispatch_json.ts";
import { assert } from "./util.ts";
import { exposeForTest } from "./internals.ts";
export interface Location {
/** The full url for the module, e.g. `file://some/file.ts` or
* `https://some/file.ts`. */
filename: string;
/** The line number in the file. It is assumed to be 1-indexed. */
line: number;
/** The column number in the file. It is assumed to be 1-indexed. */
column: number;
}
/** Given a current location in a module, lookup the source location and
* return it.
*
* When Deno transpiles code, it keep source maps of the transpiled code. This
* function can be used to lookup the original location. This is automatically
* done when accessing the `.stack` of an error, or when an uncaught error is
* logged. This function can be used to perform the lookup for creating better
* error handling.
*
* **Note:** `line` and `column` are 1 indexed, which matches display
* expectations, but is not typical of most index numbers in Deno.
*
* An example:
*
* const orig = Deno.applySourceMap({
* location: "file://my/module.ts",
* line: 5,
* column: 15
* });
* console.log(`${orig.filename}:${orig.line}:${orig.column}`);
*
*/
export function applySourceMap(location: Location): Location {
const { filename, line, column } = location;
// On this side, line/column are 1 based, but in the source maps, they are
// 0 based, so we have to convert back and forth
const res = sendSync("op_apply_source_map", {
filename,
line: line - 1,
column: column - 1
});
return {
filename: res.filename,
line: res.line + 1,
column: res.column + 1
};
}
/** Mutate the call site so that it returns the location, instead of its
* original location.
*/
function patchCallSite(callSite: CallSite, location: Location): CallSite {
return {
getThis(): unknown {
return callSite.getThis();
},
getTypeName(): string {
return callSite.getTypeName();
},
getFunction(): Function {
return callSite.getFunction();
},
getFunctionName(): string {
return callSite.getFunctionName();
},
getMethodName(): string {
return callSite.getMethodName();
},
getFileName(): string {
return location.filename;
},
getLineNumber(): number {
return location.line;
},
getColumnNumber(): number {
return location.column;
},
getEvalOrigin(): string | null {
return callSite.getEvalOrigin();
},
isToplevel(): boolean {
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();
}
};
}
/** Return a string representations of a CallSite's method call name
*
* This is adapted directly from V8.
*/
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;
}
/** Return a string representations of a CallSite's file location
*
* This is adapted directly from V8.
*/
function getFileLocation(callSite: CallSite): string {
if (callSite.isNative()) {
return "native";
}
let result = "";
const fileName = callSite.getFileName();
if (!fileName && callSite.isEval()) {
const evalOrigin = callSite.getEvalOrigin();
assert(evalOrigin != null);
result += `${evalOrigin}, `;
}
if (fileName) {
result += fileName;
} else {
result += "<anonymous>";
}
const lineNumber = callSite.getLineNumber();
if (lineNumber != null) {
result += `:${lineNumber}`;
const columnNumber = callSite.getColumnNumber();
if (columnNumber != null) {
result += `:${columnNumber}`;
}
}
return result;
}
/** Convert a CallSite to a string.
*
* This is adapted directly from V8.
*/
function callSiteToString(callSite: CallSite): string {
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 += "async ";
}
if (isPromiseAll) {
result += `Promise.all (index ${callSite.getPromiseIndex})`;
return result;
}
if (isMethodCall) {
result += getMethodCall(callSite);
} else if (isConstructor) {
result += "new ";
if (functionName) {
result += functionName;
} else {
result += "<anonymous>";
}
} else if (functionName) {
result += functionName;
} else {
result += getFileLocation(callSite);
return result;
}
result += ` (${getFileLocation(callSite)})`;
return result;
}
/** A replacement for the default stack trace preparer which will op into Rust
* to apply source maps to individual sites
*/
function prepareStackTrace(
error: Error,
structuredStackTrace: CallSite[]
): string {
return (
`${error.name}: ${error.message}\n` +
structuredStackTrace
.map(
(callSite): CallSite => {
const filename = callSite.getFileName();
const line = callSite.getLineNumber();
const column = callSite.getColumnNumber();
if (filename && line != null && column != null) {
return patchCallSite(
callSite,
applySourceMap({
filename,
line,
column
})
);
}
return callSite;
}
)
.map((callSite): string => ` at ${callSiteToString(callSite)}`)
.join("\n")
);
}
/** Sets the `prepareStackTrace` method on the Error constructor which will
* op into Rust to remap source code for caught errors where the `.stack` is
* being accessed.
*
* See: https://v8.dev/docs/stack-trace-api
*/
// @internal
export function setPrepareStackTrace(ErrorConstructor: typeof Error): void {
ErrorConstructor.prepareStackTrace = prepareStackTrace;
}
exposeForTest("setPrepareStackTrace", setPrepareStackTrace);