summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/BUILD.gn1
-rw-r--r--cli/msg.fbs7
-rw-r--r--cli/ops.rs46
-rw-r--r--cli/source_maps.rs6
-rw-r--r--js/deno.ts4
-rw-r--r--js/error_stack.ts298
-rw-r--r--js/error_stack_test.ts111
-rw-r--r--js/globals.ts23
-rw-r--r--js/main.ts3
-rw-r--r--js/unit_tests.ts1
-rw-r--r--tests/error_stack.test4
-rw-r--r--tests/error_stack.ts10
-rw-r--r--tests/error_stack.ts.out6
13 files changed, 517 insertions, 3 deletions
diff --git a/cli/BUILD.gn b/cli/BUILD.gn
index 7f19f8b95..84ffe6da4 100644
--- a/cli/BUILD.gn
+++ b/cli/BUILD.gn
@@ -83,6 +83,7 @@ ts_sources = [
"../js/dispatch_minimal.ts",
"../js/dom_types.ts",
"../js/dom_util.ts",
+ "../js/error_stack.ts",
"../js/errors.ts",
"../js/event.ts",
"../js/event_target.ts",
diff --git a/cli/msg.fbs b/cli/msg.fbs
index 2ab776ef3..014bfb7e9 100644
--- a/cli/msg.fbs
+++ b/cli/msg.fbs
@@ -1,5 +1,6 @@
union Any {
Accept,
+ ApplySourceMap,
Cache,
Chdir,
Chmod,
@@ -257,6 +258,12 @@ table FetchSourceFileRes {
data: [ubyte];
}
+table ApplySourceMap {
+ filename: string;
+ line: int;
+ column: int;
+}
+
table Cache {
extension: string;
module_id: string;
diff --git a/cli/ops.rs b/cli/ops.rs
index d4bc94f75..227bb03b3 100644
--- a/cli/ops.rs
+++ b/cli/ops.rs
@@ -20,6 +20,8 @@ use crate::resources;
use crate::resources::table_entries;
use crate::resources::Resource;
use crate::signal::kill;
+use crate::source_maps::get_orig_position;
+use crate::source_maps::CachedMaps;
use crate::startup_data;
use crate::state::ThreadSafeState;
use crate::tokio_util;
@@ -46,6 +48,7 @@ use log;
use rand::{thread_rng, Rng};
use remove_dir_all::remove_dir_all;
use std;
+use std::collections::HashMap;
use std::convert::From;
use std::fs;
use std::net::Shutdown;
@@ -194,6 +197,7 @@ pub fn dispatch_all_legacy(
pub fn op_selector_std(inner_type: msg::Any) -> Option<CliDispatchFn> {
match inner_type {
msg::Any::Accept => Some(op_accept),
+ msg::Any::ApplySourceMap => Some(op_apply_source_map),
msg::Any::Cache => Some(op_cache),
msg::Any::Chdir => Some(op_chdir),
msg::Any::Chmod => Some(op_chmod),
@@ -532,6 +536,48 @@ fn op_fetch_source_file(
Ok(Op::Sync(result_buf))
}
+fn op_apply_source_map(
+ state: &ThreadSafeState,
+ base: &msg::Base<'_>,
+ data: Option<PinnedBuf>,
+) -> CliOpResult {
+ if !base.sync() {
+ return Err(deno_error::no_async_support());
+ }
+ assert!(data.is_none());
+ let inner = base.inner_as_apply_source_map().unwrap();
+ let cmd_id = base.cmd_id();
+ let filename = inner.filename().unwrap();
+ let line = inner.line();
+ let column = inner.column();
+
+ let mut mappings_map: CachedMaps = HashMap::new();
+ let (orig_filename, orig_line, orig_column) = get_orig_position(
+ filename.to_owned(),
+ line.into(),
+ column.into(),
+ &mut mappings_map,
+ &state.ts_compiler,
+ );
+
+ let builder = &mut FlatBufferBuilder::new();
+ let msg_args = msg::ApplySourceMapArgs {
+ filename: Some(builder.create_string(&orig_filename)),
+ line: orig_line as i32,
+ column: orig_column as i32,
+ };
+ let res_inner = msg::ApplySourceMap::create(builder, &msg_args);
+ ok_buf(serialize_response(
+ cmd_id,
+ builder,
+ msg::BaseArgs {
+ inner: Some(res_inner.as_union_value()),
+ inner_type: msg::Any::ApplySourceMap,
+ ..Default::default()
+ },
+ ))
+}
+
fn op_chdir(
_state: &ThreadSafeState,
base: &msg::Base<'_>,
diff --git a/cli/source_maps.rs b/cli/source_maps.rs
index 6b453d883..a886c6afc 100644
--- a/cli/source_maps.rs
+++ b/cli/source_maps.rs
@@ -17,9 +17,9 @@ pub trait SourceMapGetter {
/// Cached filename lookups. The key can be None if a previous lookup failed to
/// find a SourceMap.
-type CachedMaps = HashMap<String, Option<SourceMap>>;
+pub type CachedMaps = HashMap<String, Option<SourceMap>>;
-struct SourceMap {
+pub struct SourceMap {
mappings: Mappings,
sources: Vec<String>,
}
@@ -202,7 +202,7 @@ fn get_maybe_orig_position<G: SourceMapGetter>(
}
}
-fn get_orig_position<G: SourceMapGetter>(
+pub fn get_orig_position<G: SourceMapGetter>(
script_name: String,
line: i64,
column: i64,
diff --git a/js/deno.ts b/js/deno.ts
index 32899e045..8dc4fd791 100644
--- a/js/deno.ts
+++ b/js/deno.ts
@@ -58,6 +58,7 @@ export { statSync, lstatSync, stat, lstat } from "./stat";
export { linkSync, link } from "./link";
export { symlinkSync, symlink } from "./symlink";
export { writeFileSync, writeFile, WriteFileOptions } from "./write_file";
+export { applySourceMap } from "./error_stack";
export { ErrorKind, DenoError } from "./errors";
export {
permissions,
@@ -88,6 +89,9 @@ export const args: string[] = [];
/** @internal */
export { core } from "./core";
+/** @internal */
+export { setPrepareStackTrace } from "./error_stack";
+
// TODO Don't expose Console nor stringifyArgs.
/** @internal */
export { Console, stringifyArgs } from "./console";
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;
+}
diff --git a/js/error_stack_test.ts b/js/error_stack_test.ts
new file mode 100644
index 000000000..0df491f39
--- /dev/null
+++ b/js/error_stack_test.ts
@@ -0,0 +1,111 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert } from "./test_util.ts";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const { setPrepareStackTrace } = Deno as any;
+
+interface CallSite {
+ getThis(): unknown;
+ getTypeName(): string;
+ getFunction(): Function;
+ getFunctionName(): string;
+ getMethodName(): string;
+ getFileName(): string;
+ getLineNumber(): number | null;
+ getColumnNumber(): number | null;
+ getEvalOrigin(): string | null;
+ isToplevel(): boolean;
+ isEval(): boolean;
+ isNative(): boolean;
+ isConstructor(): boolean;
+ isAsync(): boolean;
+ isPromiseAll(): boolean;
+ getPromiseIndex(): number | null;
+}
+
+function getMockCallSite(
+ filename: string,
+ line: number | null,
+ column: number | null
+): CallSite {
+ return {
+ getThis(): unknown {
+ return undefined;
+ },
+ getTypeName(): string {
+ return "";
+ },
+ getFunction(): Function {
+ return (): void => {};
+ },
+ getFunctionName(): string {
+ return "";
+ },
+ getMethodName(): string {
+ return "";
+ },
+ getFileName(): string {
+ return filename;
+ },
+ getLineNumber(): number | null {
+ return line;
+ },
+ getColumnNumber(): number | null {
+ return column;
+ },
+ getEvalOrigin(): null {
+ return null;
+ },
+ isToplevel(): false {
+ return false;
+ },
+ isEval(): false {
+ return false;
+ },
+ isNative(): false {
+ return false;
+ },
+ isConstructor(): false {
+ return false;
+ },
+ isAsync(): false {
+ return false;
+ },
+ isPromiseAll(): false {
+ return false;
+ },
+ getPromiseIndex(): null {
+ return null;
+ }
+ };
+}
+
+test(function prepareStackTrace(): void {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const MockError = {} as any;
+ setPrepareStackTrace(MockError);
+ assert(typeof MockError.prepareStackTrace === "function");
+ const prepareStackTrace: (
+ error: Error,
+ structuredStackTrace: CallSite[]
+ ) => string = MockError.prepareStackTrace;
+ const result = prepareStackTrace(new Error("foo"), [
+ getMockCallSite("gen/cli/bundle/main.js", 23, 0)
+ ]);
+ assert(result.startsWith("Error: foo\n"));
+ assert(
+ result.includes("<anonymous> (js/"),
+ "should remap to something in 'js/'"
+ );
+});
+
+test(function applySourceMap(): void {
+ const result = Deno.applySourceMap({
+ filename: "gen/cli/bundle/main.js",
+ line: 23,
+ column: 0
+ });
+ assert(result.filename.startsWith("js/"));
+ assert(result.line != null);
+ assert(result.column != null);
+});
diff --git a/js/globals.ts b/js/globals.ts
index cff4b7dd9..27cef3909 100644
--- a/js/globals.ts
+++ b/js/globals.ts
@@ -40,6 +40,29 @@ import { immutableDefine } from "./util";
declare global {
const console: consoleTypes.Console;
const setTimeout: typeof timers.setTimeout;
+
+ interface CallSite {
+ getThis(): unknown;
+ getTypeName(): string;
+ getFunction(): Function;
+ getFunctionName(): string;
+ getMethodName(): string;
+ getFileName(): string;
+ getLineNumber(): number | null;
+ getColumnNumber(): number | null;
+ getEvalOrigin(): string | null;
+ isToplevel(): boolean;
+ isEval(): boolean;
+ isNative(): boolean;
+ isConstructor(): boolean;
+ isAsync(): boolean;
+ isPromiseAll(): boolean;
+ getPromiseIndex(): number | null;
+ }
+
+ interface ErrorConstructor {
+ prepareStackTrace(error: Error, structuredStackTrace: CallSite[]): string;
+ }
}
// A self reference to the global object.
diff --git a/js/main.ts b/js/main.ts
index cb27690b5..5687a6926 100644
--- a/js/main.ts
+++ b/js/main.ts
@@ -8,6 +8,7 @@ import "./globals";
import { assert, log } from "./util";
import * as os from "./os";
import { args } from "./deno";
+import { setPrepareStackTrace } from "./error_stack";
import { replLoop } from "./repl";
import { xevalMain, XevalFunc } from "./xeval";
import { setVersions } from "./version";
@@ -30,6 +31,8 @@ export default function denoMain(name?: string): void {
os.exit(0);
}
+ setPrepareStackTrace(Error);
+
const mainModule = startResMsg.mainModule();
if (mainModule) {
assert(mainModule.length > 0);
diff --git a/js/unit_tests.ts b/js/unit_tests.ts
index ff9f459e5..b55c1954a 100644
--- a/js/unit_tests.ts
+++ b/js/unit_tests.ts
@@ -13,6 +13,7 @@ import "./console_test.ts";
import "./copy_file_test.ts";
import "./custom_event_test.ts";
import "./dir_test.ts";
+import "./error_stack_test.ts";
import "./event_test.ts";
import "./event_target_test.ts";
import "./fetch_test.ts";
diff --git a/tests/error_stack.test b/tests/error_stack.test
new file mode 100644
index 000000000..88de56958
--- /dev/null
+++ b/tests/error_stack.test
@@ -0,0 +1,4 @@
+args: run --reload tests/error_stack.ts
+check_stderr: true
+exit_code: 1
+output: tests/error_stack.ts.out
diff --git a/tests/error_stack.ts b/tests/error_stack.ts
new file mode 100644
index 000000000..f2125d662
--- /dev/null
+++ b/tests/error_stack.ts
@@ -0,0 +1,10 @@
+function foo(): never {
+ throw new Error("foo");
+}
+
+try {
+ foo();
+} catch (e) {
+ console.log(e);
+ throw e;
+}
diff --git a/tests/error_stack.ts.out b/tests/error_stack.ts.out
new file mode 100644
index 000000000..2bb629e2d
--- /dev/null
+++ b/tests/error_stack.ts.out
@@ -0,0 +1,6 @@
+[WILDCARD]Error: foo
+ at foo ([WILDCARD]tests/error_stack.ts:2:9)
+ at [WILDCARD]tests/error_stack.ts:6:3
+error: Uncaught Error: foo
+ at foo ([WILDCARD]tests/error_stack.ts:2:9)
+ at [WILDCARD]tests/error_stack.ts:6:3