diff options
author | Kitson Kelly <me@kitsonkelly.com> | 2019-07-29 19:11:08 +1000 |
---|---|---|
committer | Ryan Dahl <ry@tinyclouds.org> | 2019-07-29 09:11:08 +0000 |
commit | 5083f5fd908057cbc8649b79c13ab78a7f7ebf34 (patch) | |
tree | d0336672fd7760593695b2e0d351c401ed1d8c02 /js/error_stack.ts | |
parent | ff96e3dc637974c6f9853f3bf9565bfd63f22b17 (diff) |
Remap stack traces of unthrown errors. (#2693)
Diffstat (limited to 'js/error_stack.ts')
-rw-r--r-- | js/error_stack.ts | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/js/error_stack.ts b/js/error_stack.ts new file mode 100644 index 000000000..e97020687 --- /dev/null +++ b/js/error_stack.ts @@ -0,0 +1,298 @@ +// Copyright 2018-2019 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 msg from "gen/cli/msg_generated"; +import * as flatbuffers from "./flatbuffers"; +import * as dispatch from "./dispatch"; +import { assert } from "./util"; + +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; +} + +function req( + filename: string, + line: number, + column: number +): [flatbuffers.Builder, msg.Any.ApplySourceMap, flatbuffers.Offset] { + const builder = flatbuffers.createBuilder(); + const filename_ = builder.createString(filename); + const inner = msg.ApplySourceMap.createApplySourceMap( + builder, + filename_, + // 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 + line - 1, + column - 1 + ); + return [builder, msg.Any.ApplySourceMap, inner]; +} + +function res(baseRes: msg.Base | null): Location { + assert(baseRes != null); + assert(baseRes!.innerType() === msg.Any.ApplySourceMap); + const res = new msg.ApplySourceMap(); + assert(baseRes!.inner(res) != null); + const filename = res.filename()!; + assert(filename != null); + return { + filename, + // 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 + line: res.line() + 1, + column: res.column() + 1 + }; +} + +/** 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; + return res(dispatch.sendSync(...req(filename, line, column))); +} + +/** 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; +} |